[{"data":1,"prerenderedAt":19499},["ShallowReactive",2],{"\u002Fblog\u002Fangular-ngrx-architecture-for-agents":3},{"id":4,"title":5,"body":6,"canonicalUrl":19484,"date":19485,"description":19486,"extension":19487,"imageAlt":19488,"imageUrl":19489,"meta":19490,"navigation":987,"ogType":19491,"path":19492,"published":987,"readingTime":18355,"seo":19493,"stem":19494,"tags":19495,"__hash__":19498},"blog\u002Fblog\u002Fangular-ngrx-architecture-for-agents.md","Angular + NgRx Architecture for Agents",{"type":7,"value":8,"toc":19379},"minimark",[9,593,600,606,616,619,626,635,643,654,657,740,754,756,761,819,821,826,829,1223,1226,1233,1245,1255,1257,1262,1265,1271,1278,1329,1474,1477,1494,1583,1600,1619,1624,1631,1637,1644,1743,1756,1759,1792,1797,1800,1856,1859,1877,1880,1885,1888,2124,2275,2293,2325,2338,2392,2402,2412,2424,2429,2432,2474,2480,2776,2791,2804,2807,2816,2826,2926,2932,3020,3039,3044,3047,3052,3096,3112,3118,3142,3151,3176,3191,3196,3206,3212,3244,3247,3270,3280,3285,3288,3316,3319,3344,3351,3354,3533,3538,3623,3625,3630,3635,3641,3644,3656,3659,3676,3681,3688,4042,4152,4166,4172,4178,4307,4312,4324,4330,4333,4650,4662,4667,4787,4790,4996,5007,5012,5025,5034,5385,5410,5416,5641,5651,5660,5665,5668,5675,6083,6105,6114,6120,6427,6430,6458,6464,6532,6544,6575,6611,6614,6668,6681,6687,6819,6825,6830,6844,6963,6969,7062,7080,7085,7088,7223,7229,7243,7248,7301,7310,7315,7318,7324,7540,7546,7551,7554,8025,8040,8045,8185,8203,8211,8220,8433,8498,8539,8593,8598,8607,8624,8627,8703,8728,8730,8735,8738,8743,8746,9016,9025,9118,9121,9135,9233,9239,9284,9294,9355,9364,9381,9450,9468,9481,9497,9500,9508,9576,9587,9733,9742,9751,9764,10023,10030,10039,10044,10070,10084,10087,10095,10202,10209,10410,10417,10535,10554,10567,10578,10589,10594,10766,10790,10795,10801,10923,10935,10946,10954,10999,11010,11016,11078,11088,11093,11100,11125,11128,11214,11227,11233,11263,11268,11342,11344,11349,11355,11364,11421,11430,11439,11486,11579,11585,11669,11675,11738,11751,11800,11805,11812,11878,11881,11907,11918,11923,11995,12004,12027,12032,12210,12224,12233,12239,12369,12375,12406,12412,12417,12572,12584,12589,12592,12699,12709,12714,12769,12771,12776,12779,12784,12791,13281,13287,13329,13334,13351,13354,13383,13393,13398,13401,13509,13516,13712,13725,13730,13757,13833,13836,13891,13900,13950,13960,13965,13968,14023,14028,14048,14153,14167,14172,14178,14183,14227,14229,14234,14244,14249,14260,14473,14517,14524,14529,14539,14631,14642,14647,14656,14803,14816,14821,14824,14900,14903,14908,14920,14926,14931,14944,15142,15149,15154,15161,15166,15195,15197,15202,15205,15272,15278,15283,15286,15521,15579,15591,15605,15611,15640,15645,15648,15680,15685,15695,15913,15919,15921,15926,15931,15943,16056,16156,16161,16219,16224,16227,16235,16238,16278,16280,16285,16288,16297,16307,16320,16329,16380,16393,16399,16409,16417,16419,16424,16427,16517,16523,16528,16825,16832,16837,16882,16887,16960,16962,16967,16970,16975,17101,17106,17327,17331,17472,17477,17831,17836,18011,18013,18018,18028,19294,19296,19301,19308,19317,19325,19327,19332,19375],[10,11,13],"nav",{"className":12},"toc",[14,15,17,27,33,39,110,215,283,339,395,451,477,503,509,535,573,579],"ol",{"className":16},"toc-level toc-level-1",[18,19,21],"li",{"className":20},"toc-item toc-item-h2",[22,23,26],"a",{"className":24,"href":25},"toc-link toc-link-h2","#get-this-as-a-plugin-instead","Get this as a plugin instead",[18,28,29],{"className":20},[22,30,32],{"className":24,"href":31},"#contents","Contents",[18,34,35],{"className":20},[22,36,38],{"className":24,"href":37},"#why-this-document-exists","Why this document exists",[18,40,41,45],{"className":20},[22,42,44],{"className":24,"href":43},"#part-1--application-architecture","Part 1 — Application architecture",[14,46,48,56,62,68,74,80,86,92,98,104],{"className":47},"toc-level toc-level-2",[18,49,51],{"className":50},"toc-item toc-item-h3",[22,52,55],{"className":53,"href":54},"toc-link toc-link-h3","#11-bootstrap-and-application-wide-providers","1.1 Bootstrap and application-wide providers",[18,57,58],{"className":50},[22,59,61],{"className":53,"href":60},"#12-how-to-group-code","1.2 How to group code",[18,63,64],{"className":50},[22,65,67],{"className":53,"href":66},"#13-when-folders-arent-enough-real-library-boundaries","1.3 When folders aren't enough: real library boundaries",[18,69,70],{"className":50},[22,71,73],{"className":53,"href":72},"#14-routing-is-the-architectural-spine","1.4 Routing is the architectural spine",[18,75,76],{"className":50},[22,77,79],{"className":53,"href":78},"#15-feature-flags","1.5 Feature flags",[18,81,82],{"className":50},[22,83,85],{"className":53,"href":84},"#16-lazy-load-inside-templates-with-defer","1.6 Lazy-load inside templates with @defer",[18,87,88],{"className":50},[22,89,91],{"className":53,"href":90},"#17-internationalization-i18n","1.7 Internationalization (i18n)",[18,93,94],{"className":50},[22,95,97],{"className":53,"href":96},"#18-rendering-strategy-ssr-and-hydration","1.8 Rendering strategy: SSR and hydration",[18,99,100],{"className":50},[22,101,103],{"className":53,"href":102},"#19-change-detection-zoneless-is-the-default-now","1.9 Change detection: zoneless is the default now",[18,105,106],{"className":50},[22,107,109],{"className":53,"href":108},"#part-1-rules","Part 1 rules",[18,111,112,116],{"className":20},[22,113,115],{"className":24,"href":114},"#part-2--the-three-layers","Part 2 — The three layers",[14,117,118,124,130,195,201],{"className":47},[18,119,120],{"className":50},[22,121,123],{"className":53,"href":122},"#20-the-model-in-one-diagram","2.0 The model in one diagram",[18,125,126],{"className":50},[22,127,129],{"className":53,"href":128},"#21-services-are-boring-on-purpose","2.1 Services are boring on purpose",[18,131,132,136],{"className":50},[22,133,135],{"className":53,"href":134},"#22-the-store-is-where-the-feature-actually-lives","2.2 The store is where the feature actually lives",[14,137,139,147,153,159,165,171,177,183,189],{"className":138},"toc-level toc-level-3",[18,140,142],{"className":141},"toc-item toc-item-h4",[22,143,146],{"className":144,"href":145},"toc-link toc-link-h4","#anatomy","Anatomy",[18,148,149],{"className":141},[22,150,152],{"className":144,"href":151},"#state-design-flat-and-free-of-derivations","State design: flat, and free of derivations",[18,154,155],{"className":141},[22,156,158],{"className":144,"href":157},"#keeping-shared-entities-consistent-across-stores","Keeping shared entities consistent across stores",[18,160,161],{"className":141},[22,162,164],{"className":144,"href":163},"#loading-and-error-state-build-it-once-reuse-it-everywhere","Loading and error state: build it once, reuse it everywhere",[18,166,167],{"className":141},[22,168,170],{"className":144,"href":169},"#orchestration-rxmethod-owns-the-async-flow","Orchestration: rxMethod owns the async flow",[18,172,173],{"className":141},[22,174,176],{"className":144,"href":175},"#make-every-mutation-self-describing","Make every mutation self-describing",[18,178,179],{"className":141},[22,180,182],{"className":144,"href":181},"#lifecycle-belongs-to-the-store-not-the-component","Lifecycle belongs to the store, not the component",[18,184,185],{"className":141},[22,186,188],{"className":144,"href":187},"#scoping-singleton-or-component-scoped","Scoping: singleton or component-scoped?",[18,190,191],{"className":141},[22,192,194],{"className":144,"href":193},"#when-a-store-gets-big","When a store gets big",[18,196,197],{"className":50},[22,198,200],{"className":53,"href":199},"#23-components-are-a-projection-of-state","2.3 Components are a projection of state",[18,202,203,207],{"className":50},[22,204,206],{"className":53,"href":205},"#24-the-small-component-escape-hatch-rxresource","2.4 The small-component escape hatch: rxResource",[14,208,209],{"className":138},[18,210,211],{"className":141},[22,212,214],{"className":144,"href":213},"#when-to-graduate-to-a-store","When to graduate to a store",[18,216,217,221],{"className":20},[22,218,220],{"className":24,"href":219},"#part-3--reactivity-with-signals","Part 3 — Reactivity with signals",[14,222,223,229,235,241,247,253,259,265,271,277],{"className":47},[18,224,225],{"className":50},[22,226,228],{"className":53,"href":227},"#31-the-decision-table","3.1 The decision table",[18,230,231],{"className":50},[22,232,234],{"className":53,"href":233},"#32-signal--writable-state","3.2 signal — writable state",[18,236,237],{"className":50},[22,238,240],{"className":53,"href":239},"#33-computed--derived-state","3.3 computed — derived state",[18,242,243],{"className":50},[22,244,246],{"className":53,"href":245},"#34-reactive-contexts-untracked-and-the-await-trap","3.4 Reactive contexts, untracked, and the await trap",[18,248,249],{"className":50},[22,250,252],{"className":53,"href":251},"#35-linkedsignal--writable-state-with-a-source-of-truth","3.5 linkedSignal — writable state with a source of truth",[18,254,255],{"className":50},[22,256,258],{"className":53,"href":257},"#36-effect--the-last-resort","3.6 effect — the last resort",[18,260,261],{"className":50},[22,262,264],{"className":53,"href":263},"#37-dom-work-afternextrender-and-afterrendereffect","3.7 DOM work: afterNextRender and afterRenderEffect",[18,266,267],{"className":50},[22,268,270],{"className":53,"href":269},"#38-inputs-outputs-and-queries","3.8 Inputs, outputs, and queries",[18,272,273],{"className":50},[22,274,276],{"className":53,"href":275},"#39-rxjs-interop--and-when-to-stay-in-rxjs","3.9 RxJS interop — and when to stay in RxJS",[18,278,279],{"className":50},[22,280,282],{"className":53,"href":281},"#part-3-rules","Part 3 rules",[18,284,285,289],{"className":20},[22,286,288],{"className":24,"href":287},"#part-4--dependency-injection","Part 4 — Dependency injection",[14,290,291,297,303,309,315,321,327,333],{"className":47},[18,292,293],{"className":50},[22,294,296],{"className":53,"href":295},"#41-always-inject-always-in-an-injection-context","4.1 Always inject(), always in an injection context",[18,298,299],{"className":50},[22,300,302],{"className":53,"href":301},"#42-the-two-injector-hierarchies","4.2 The two injector hierarchies",[18,304,305],{"className":50},[22,306,308],{"className":53,"href":307},"#43-choosing-a-scope","4.3 Choosing a scope",[18,310,311],{"className":50},[22,312,314],{"className":53,"href":313},"#44-providers-beyond-the-shorthand","4.4 Providers beyond the shorthand",[18,316,317],{"className":50},[22,318,320],{"className":53,"href":319},"#45-injectiontoken--and-when-not-to-make-one","4.5 InjectionToken — and when not to make one",[18,322,323],{"className":50},[22,324,326],{"className":53,"href":325},"#46-resolution-modifiers","4.6 Resolution modifiers",[18,328,329],{"className":50},[22,330,332],{"className":53,"href":331},"#47-di-is-your-testing-seam","4.7 DI is your testing seam",[18,334,335],{"className":50},[22,336,338],{"className":53,"href":337},"#part-4-rules","Part 4 rules",[18,340,341,345],{"className":20},[22,342,344],{"className":24,"href":343},"#part-5--auth--security-architecture","Part 5 — Auth & security architecture",[14,346,347,353,359,365,371,377,383,389],{"className":47},[18,348,349],{"className":50},[22,350,352],{"className":53,"href":351},"#51-where-auth-state-lives","5.1 Where auth state lives",[18,354,355],{"className":50},[22,356,358],{"className":53,"href":357},"#52-token-storage-the-single-highest-stakes-decision-here","5.2 Token storage: the single highest-stakes decision here",[18,360,361],{"className":50},[22,362,364],{"className":53,"href":363},"#53-the-auth-interceptor-and-the-refresh-race-every-implementation-gets-wrong","5.3 The auth interceptor, and the refresh race every implementation gets wrong",[18,366,367],{"className":50},[22,368,370],{"className":53,"href":369},"#54-guards-enforce-ux-the-server-enforces-security","5.4 Guards enforce UX, the server enforces security",[18,372,373],{"className":50},[22,374,376],{"className":53,"href":375},"#55-csp-and-trusted-types","5.5 CSP and Trusted Types",[18,378,379],{"className":50},[22,380,382],{"className":53,"href":381},"#56-sanitization-and-untrusted-data--the-boundary-rule-applied","5.6 Sanitization and untrusted data — the boundary rule, applied",[18,384,385],{"className":50},[22,386,388],{"className":53,"href":387},"#57-secrets-never-ship-in-the-client-bundle","5.7 Secrets never ship in the client bundle",[18,390,391],{"className":50},[22,392,394],{"className":53,"href":393},"#part-5-rules","Part 5 rules",[18,396,397,401],{"className":20},[22,398,400],{"className":24,"href":399},"#part-6--production-observability","Part 6 — Production observability",[14,402,403,409,415,421,427,433,439,445],{"className":47},[18,404,405],{"className":50},[22,406,408],{"className":53,"href":407},"#61-global-error-handling-the-safety-net-under-everything","6.1 Global error handling: the safety net under everything",[18,410,411],{"className":50},[22,412,414],{"className":53,"href":413},"#62-normalize-http-errors-once-at-the-interceptor","6.2 Normalize HTTP errors once, at the interceptor",[18,416,417],{"className":50},[22,418,420],{"className":53,"href":419},"#63-report-from-the-stores-error-branch-too-not-only-the-global-handler","6.3 Report from the store's error branch too, not only the global handler",[18,422,423],{"className":50},[22,424,426],{"className":53,"href":425},"#64-correlation-ids-connect-a-report-back-to-what-the-user-was-doing","6.4 Correlation IDs: connect a report back to what the user was doing",[18,428,429],{"className":50},[22,430,432],{"className":53,"href":431},"#65-breadcrumbs-without-pii","6.5 Breadcrumbs, without PII",[18,434,435],{"className":50},[22,436,438],{"className":53,"href":437},"#66-real-user-monitoring-core-web-vitals","6.6 Real User Monitoring (Core Web Vitals)",[18,440,441],{"className":50},[22,442,444],{"className":53,"href":443},"#67-source-maps-ship-them-to-the-reporter-not-to-the-public","6.7 Source maps: ship them to the reporter, not to the public",[18,446,447],{"className":50},[22,448,450],{"className":53,"href":449},"#part-6-rules","Part 6 rules",[18,452,453,457],{"className":20},[22,454,456],{"className":24,"href":455},"#part-7--forms","Part 7 — Forms",[14,458,459,465,471],{"className":47},[18,460,461],{"className":50},[22,462,464],{"className":53,"href":463},"#71-signal-forms-v21-stable-since-v22","7.1 Signal Forms (v21+, stable since v22)",[18,466,467],{"className":50},[22,468,470],{"className":53,"href":469},"#72-reactive-forms-v20-or-existing-code","7.2 Reactive Forms (v20, or existing code)",[18,472,473],{"className":50},[22,474,476],{"className":53,"href":475},"#73-where-form-submission-lives","7.3 Where form submission lives",[18,478,479,483],{"className":20},[22,480,482],{"className":24,"href":481},"#part-8--templates-styling-accessibility","Part 8 — Templates, styling, accessibility",[14,484,485,491,497],{"className":47},[18,486,487],{"className":50},[22,488,490],{"className":53,"href":489},"#81-templates","8.1 Templates",[18,492,493],{"className":50},[22,494,496],{"className":53,"href":495},"#82-styling","8.2 Styling",[18,498,499],{"className":50},[22,500,502],{"className":53,"href":501},"#83-accessibility","8.3 Accessibility",[18,504,505],{"className":20},[22,506,508],{"className":24,"href":507},"#part-9--performance","Part 9 — Performance",[18,510,511,515],{"className":20},[22,512,514],{"className":24,"href":513},"#part-10--testing","Part 10 — Testing",[14,516,517,523,529],{"className":47},[18,518,519],{"className":50},[22,520,522],{"className":53,"href":521},"#101-store-tests-are-the-main-event","10.1 Store tests are the main event",[18,524,525],{"className":50},[22,526,528],{"className":53,"href":527},"#102-zoneless-testing-changes-the-rules","10.2 Zoneless testing changes the rules",[18,530,531],{"className":50},[22,532,534],{"className":53,"href":533},"#103-test-hygiene-that-prevents-cross-suite-flakiness","10.3 Test hygiene that prevents cross-suite flakiness",[18,536,537,541],{"className":20},[22,538,540],{"className":24,"href":539},"#anti-pattern-catalogue","Anti-pattern catalogue",[14,542,543,549,555,561,567],{"className":47},[18,544,545],{"className":50},[22,546,548],{"className":53,"href":547},"#architecture","Architecture",[18,550,551],{"className":50},[22,552,554],{"className":53,"href":553},"#reactivity","Reactivity",[18,556,557],{"className":50},[22,558,560],{"className":53,"href":559},"#state","State",[18,562,563],{"className":50},[22,564,566],{"className":53,"href":565},"#di-templates-and-the-rest","DI, templates, and the rest",[18,568,569],{"className":50},[22,570,572],{"className":53,"href":571},"#security-observability-and-scale","Security, observability, and scale",[18,574,575],{"className":20},[22,576,578],{"className":24,"href":577},"#appendix-rules-block-for-your-agent","Appendix: rules block for your agent",[18,580,581,585],{"className":20},[22,582,584],{"className":24,"href":583},"#closing-thought","Closing thought",[14,586,587],{"className":47},[18,588,589],{"className":50},[22,590,592],{"className":53,"href":591},"#sources","Sources",[594,595,596],"p",{},[597,598,599],"strong",{},"Services, signal stores, signal reactivity, dependency injection, security, observability,\nand everything else that determines whether an Angular app survives contact with a growing\nteam.",[594,601,602],{},[603,604,605],"em",{},"Written to be read by humans and pasted into an AI agent.",[594,607,608,609,615],{},"Targets Angular 20+ and NgRx Signals. Verified against the\n",[22,610,614],{"href":611,"rel":612},"https:\u002F\u002Fangular.dev",[613],"nofollow","angular.dev"," docs and the official Angular best-practices guide;\nversion-sensitive claims are flagged inline.",[617,618],"hr",{},[620,621,623],"h2",{"id":622},"get-this-as-a-plugin-instead",[22,624,26],{"title":625,"href":25},"Permalink to this section",[594,627,628,629,634],{},"Everything in this document is also packaged as a ",[22,630,633],{"href":631,"rel":632},"https:\u002F\u002Fclaude.com\u002Fclaude-code",[613],"Claude Code","\nplugin, available at:",[594,636,637],{},[597,638,639],{},[22,640,641],{"href":641,"rel":642},"https:\u002F\u002Fgithub.com\u002FKylerJohnsonDev\u002Fangular-architecture-skills",[613],[644,645,650],"pre",{"className":646,"code":648,"language":649},[647],"language-text","\u002Fplugin marketplace add KylerJohnsonDev\u002Fangular-architecture-skills\n\u002Fplugin install angular-architecture@angular-architecture\n","text",[651,652,648],"code",{"__ignoreMap":653},"",[594,655,656],{},"The plugin (and this document) cover:",[658,659,660,670,694,704,710,716,722,728,734],"ul",{},[18,661,662,665,666,669],{},[597,663,664],{},"Component → Store → Service layering"," — the non-negotiable data-flow rule and the one\nsanctioned exception (",[651,667,668],{},"rxResource",") and when it applies",[18,671,672,675,676,679,680,679,683,679,686,689,690,693],{},[597,673,674],{},"Signal reactivity"," — ",[651,677,678],{},"signal",", ",[651,681,682],{},"computed",[651,684,685],{},"linkedSignal",[651,687,688],{},"effect",", and the substitution\ntable that replaces almost every ",[651,691,692],{},"effect()"," an agent writes unprompted",[18,695,696,699,700,703],{},[597,697,698],{},"NgRx Signal Store"," — anatomy, call-state, ",[651,701,702],{},"rxMethod"," orchestration, and keeping shared\nentities consistent across multiple stores at scale",[18,705,706,709],{},[597,707,708],{},"Dependency injection"," — injection contexts, injector hierarchies, scoping, and DI as a\ntesting seam",[18,711,712,715],{},[597,713,714],{},"Auth & security architecture"," — token storage, the refresh-request race every\nimplementation gets wrong, CSRF\u002FCSP, and the sanitization boundary",[18,717,718,721],{},[597,719,720],{},"Production observability"," — global error handling, structured logging, correlation IDs,\nand real-user monitoring",[18,723,724,727],{},[597,725,726],{},"Application architecture at scale"," — routing, lazy loading, feature flags, i18n\u002Fl10n,\nSSR\u002Fhydration strategy, and monorepo\u002Flibrary boundaries",[18,729,730,733],{},[597,731,732],{},"Forms, templates, styling, accessibility, performance, and testing"," conventions",[18,735,736,739],{},[597,737,738],{},"An anti-pattern catalogue",", structured as a PR-review checklist",[594,741,742,745,746,749,750,753],{},[597,743,744],{},"Prefer the plugin when you can install one."," It loads only the reference file a task\nactually touches, which keeps an agent's context small and lets version-sensitive guidance\nstay current independently of this document. This article is the alternative for when you\n",[603,747,748],{},"can't"," install a plugin — a one-off session, a tool without Claude Code plugin support, a\nteammate who just wants the rules in one place — where you'd rather paste the whole thing\ninto an agent's context (a system prompt, ",[651,751,752],{},"CLAUDE.md",", or a single message) and query it\ndirectly. Same rules, same code, delivered as one file instead of a loaded skill.",[617,755],{},[620,757,759],{"id":758},"contents",[22,760,32],{"title":625,"href":31},[658,762,763,767,771,775,779,783,787,791,795,799,803,807,811,815],{},[18,764,765],{},[22,766,38],{"href":37},[18,768,769],{},[22,770,44],{"href":43},[18,772,773],{},[22,774,115],{"href":114},[18,776,777],{},[22,778,220],{"href":219},[18,780,781],{},[22,782,288],{"href":287},[18,784,785],{},[22,786,344],{"href":343},[18,788,789],{},[22,790,400],{"href":399},[18,792,793],{},[22,794,456],{"href":455},[18,796,797],{},[22,798,482],{"href":481},[18,800,801],{},[22,802,508],{"href":507},[18,804,805],{},[22,806,514],{"href":513},[18,808,809],{},[22,810,540],{"href":539},[18,812,813],{},[22,814,578],{"href":577},[18,816,817],{},[22,818,584],{"href":583},[617,820],{},[620,822,824],{"id":823},"why-this-document-exists",[22,825,38],{"title":625,"href":37},[594,827,828],{},"Ask a coding agent to \"add a widget that shows the top five shipping carriers\" and you will\nalmost always get back code that works. You will also usually get back code like this:",[644,830,834],{"className":831,"code":832,"language":833,"meta":653,"style":653},"language-ts shiki shiki-themes github-light catppuccin-mocha","\u002F\u002F ❌ Plausible. Works. Wrong.\n@Component({ selector: 'app-carrier-list', templateUrl: '.\u002Fcarrier-list.component.html' })\nexport class CarrierListComponent implements OnInit {\n  carriers: Carrier[] = [];\n  loading = false;\n  error: string | null = null;\n\n  constructor(private http: HttpClient) {}\n\n  ngOnInit() {\n    this.loading = true;\n    this.http.get\u003CCarrier[]>('\u002Fapi\u002Fcarriers').subscribe({\n      next: (carriers) => { this.carriers = carriers; this.loading = false; },\n      error: (err) => { this.error = err.message; this.loading = false; },\n    });\n  }\n}\n","ts",[651,835,836,845,892,915,942,957,982,989,1015,1020,1031,1051,1096,1149,1201,1211,1217],{"__ignoreMap":653},[837,838,841],"span",{"class":839,"line":840},"line",1,[837,842,844],{"class":843},"sv8KM","\u002F\u002F ❌ Plausible. Works. Wrong.\n",[837,846,848,852,856,860,864,867,871,875,878,881,883,886,889],{"class":839,"line":847},2,[837,849,851],{"class":850},"sH6be","@",[837,853,855],{"class":854},"stz6-","Component",[837,857,859],{"class":858},"sh09K","(",[837,861,863],{"class":862},"s04sq","{",[837,865,866],{"class":858}," selector",[837,868,870],{"class":869},"sT6rL",":",[837,872,874],{"class":873},"sW9l_"," 'app-carrier-list'",[837,876,877],{"class":862},",",[837,879,880],{"class":858}," templateUrl",[837,882,870],{"class":869},[837,884,885],{"class":873}," '.\u002Fcarrier-list.component.html'",[837,887,888],{"class":862}," }",[837,890,891],{"class":858},")\n",[837,893,895,899,902,906,909,912],{"class":839,"line":894},3,[837,896,898],{"class":897},"snIPh","export",[837,900,901],{"class":897}," class",[837,903,905],{"class":904},"sQ9xd"," CarrierListComponent",[837,907,908],{"class":897}," implements",[837,910,911],{"class":904}," OnInit",[837,913,914],{"class":862}," {\n",[837,916,918,922,925,928,932,935,939],{"class":839,"line":917},4,[837,919,921],{"class":920},"s9puj","  carriers",[837,923,870],{"class":924},"sPHL9",[837,926,927],{"class":904}," Carrier",[837,929,931],{"class":930},"smRSh","[] ",[837,933,934],{"class":924},"=",[837,936,938],{"class":937},"s8Ev2"," []",[837,940,941],{"class":862},";\n",[837,943,945,948,951,955],{"class":839,"line":944},5,[837,946,947],{"class":920},"  loading",[837,949,950],{"class":924}," =",[837,952,954],{"class":953},"sFYxX"," false",[837,956,941],{"class":862},[837,958,960,963,965,969,972,976,978,980],{"class":839,"line":959},6,[837,961,962],{"class":920},"  error",[837,964,870],{"class":924},[837,966,968],{"class":967},"s0fpT"," string",[837,970,971],{"class":924}," |",[837,973,975],{"class":974},"szDnw"," null",[837,977,950],{"class":924},[837,979,975],{"class":967},[837,981,941],{"class":862},[837,983,985],{"class":839,"line":984},7,[837,986,988],{"emptyLinePlaceholder":987},true,"\n",[837,990,992,995,997,1000,1004,1006,1009,1012],{"class":839,"line":991},8,[837,993,994],{"class":897},"  constructor",[837,996,859],{"class":862},[837,998,999],{"class":897},"private",[837,1001,1003],{"class":1002},"s02lc"," http",[837,1005,870],{"class":924},[837,1007,1008],{"class":904}," HttpClient",[837,1010,1011],{"class":862},")",[837,1013,1014],{"class":862}," {}\n",[837,1016,1018],{"class":839,"line":1017},9,[837,1019,988],{"emptyLinePlaceholder":987},[837,1021,1023,1026,1029],{"class":839,"line":1022},10,[837,1024,1025],{"class":854},"  ngOnInit",[837,1027,1028],{"class":862},"()",[837,1030,914],{"class":862},[837,1032,1034,1038,1041,1044,1046,1049],{"class":839,"line":1033},11,[837,1035,1037],{"class":1036},"saQYf","    this",[837,1039,1040],{"class":869},".",[837,1042,1043],{"class":937},"loading ",[837,1045,934],{"class":924},[837,1047,1048],{"class":953}," true",[837,1050,941],{"class":862},[837,1052,1054,1056,1058,1061,1063,1066,1070,1073,1076,1079,1081,1084,1086,1088,1091,1093],{"class":839,"line":1053},12,[837,1055,1037],{"class":1036},[837,1057,1040],{"class":869},[837,1059,1060],{"class":937},"http",[837,1062,1040],{"class":869},[837,1064,1065],{"class":854},"get",[837,1067,1069],{"class":1068},"seurA","\u003C",[837,1071,1072],{"class":904},"Carrier",[837,1074,1075],{"class":930},"[]",[837,1077,1078],{"class":1068},">",[837,1080,859],{"class":937},[837,1082,1083],{"class":873},"'\u002Fapi\u002Fcarriers'",[837,1085,1011],{"class":937},[837,1087,1040],{"class":869},[837,1089,1090],{"class":854},"subscribe",[837,1092,859],{"class":937},[837,1094,1095],{"class":862},"{\n",[837,1097,1099,1102,1104,1107,1110,1112,1115,1118,1121,1123,1126,1128,1131,1134,1136,1138,1140,1142,1144,1146],{"class":839,"line":1098},13,[837,1100,1101],{"class":854},"      next",[837,1103,870],{"class":869},[837,1105,1106],{"class":862}," (",[837,1108,1109],{"class":1002},"carriers",[837,1111,1011],{"class":862},[837,1113,1114],{"class":924}," =>",[837,1116,1117],{"class":862}," {",[837,1119,1120],{"class":1036}," this",[837,1122,1040],{"class":869},[837,1124,1125],{"class":937},"carriers ",[837,1127,934],{"class":924},[837,1129,1130],{"class":937}," carriers",[837,1132,1133],{"class":862},";",[837,1135,1120],{"class":1036},[837,1137,1040],{"class":869},[837,1139,1043],{"class":937},[837,1141,934],{"class":924},[837,1143,954],{"class":953},[837,1145,1133],{"class":862},[837,1147,1148],{"class":862}," },\n",[837,1150,1152,1155,1157,1159,1162,1164,1166,1168,1170,1172,1175,1177,1180,1182,1185,1187,1189,1191,1193,1195,1197,1199],{"class":839,"line":1151},14,[837,1153,1154],{"class":854},"      error",[837,1156,870],{"class":869},[837,1158,1106],{"class":862},[837,1160,1161],{"class":1002},"err",[837,1163,1011],{"class":862},[837,1165,1114],{"class":924},[837,1167,1117],{"class":862},[837,1169,1120],{"class":1036},[837,1171,1040],{"class":869},[837,1173,1174],{"class":937},"error ",[837,1176,934],{"class":924},[837,1178,1179],{"class":937}," err",[837,1181,1040],{"class":869},[837,1183,1184],{"class":937},"message",[837,1186,1133],{"class":862},[837,1188,1120],{"class":1036},[837,1190,1040],{"class":869},[837,1192,1043],{"class":937},[837,1194,934],{"class":924},[837,1196,954],{"class":953},[837,1198,1133],{"class":862},[837,1200,1148],{"class":862},[837,1202,1204,1207,1209],{"class":839,"line":1203},15,[837,1205,1206],{"class":862},"    }",[837,1208,1011],{"class":937},[837,1210,941],{"class":862},[837,1212,1214],{"class":839,"line":1213},16,[837,1215,1216],{"class":862},"  }\n",[837,1218,1220],{"class":839,"line":1219},17,[837,1221,1222],{"class":862},"}\n",[594,1224,1225],{},"Every line of that is defensible in isolation. Together they produce an application that\ncannot be reasoned about: state lives in twelve components, loading flags are hand-rolled\nand inconsistently reset, nothing is observable in devtools, and the only way to test the\ndata flow is to mount the component and mock HTTP. Scale that pattern to an enterprise app —\nmultiple teams, a dozen features, a login wall, a compliance audit — and the same defensible\nlines compound into an application nobody can safely change.",[594,1227,1228,1229,1232],{},"The fix is not \"use a state library.\" It is a set of ",[597,1230,1231],{},"explicit rules about which layer is\nallowed to do what",", extended past the component boundary into how the app boots, how auth\nand errors are handled, and how the codebase is bounded as it grows. Once the rules are\nwritten down, both humans and agents produce the same shape of code, and code review stops\nbeing an argument about taste.",[594,1234,1235,1236,679,1238,1241,1242,1040],{},"That's what follows. Ten parts, each ending in rules you can enforce. The appendix compresses\nall of it into a block you can drop into ",[651,1237,752],{},[651,1239,1240],{},"AGENTS.md",", or ",[651,1243,1244],{},".cursorrules",[594,1246,1247,1250,1251,1254],{},[597,1248,1249],{},"A note on the examples."," Everything runs on an invented shipping-carriers domain with an\n",[651,1252,1253],{},"app-"," selector prefix. Adapt the names; the shapes are the point.",[617,1256],{},[620,1258,1260],{"id":1259},"part-1-application-architecture",[22,1261,44],{"title":625,"href":43},[594,1263,1264],{},"Before any component gets written, several decisions determine how the application ages: how\nit boots, how code is grouped and bounded, how routing carries the structure, how rendering\nand translation strategy are chosen, and how change detection is driven.",[1266,1267,1269],"h3",{"id":1268},"_11-bootstrap-and-application-wide-providers",[22,1270,55],{"title":625,"href":54},[594,1272,1273,1274,1277],{},"Modern Angular has no ",[651,1275,1276],{},"NgModule",". An application is a root component plus a flat list of\nproviders.",[644,1279,1281],{"className":831,"code":1280,"language":833,"meta":653,"style":653},"\u002F\u002F main.ts\nbootstrapApplication(App, appConfig).catch((error) => console.error(error));\n",[651,1282,1283,1288],{"__ignoreMap":653},[837,1284,1285],{"class":839,"line":840},[837,1286,1287],{"class":843},"\u002F\u002F main.ts\n",[837,1289,1290,1293,1296,1298,1301,1303,1306,1308,1310,1313,1315,1317,1320,1322,1324,1327],{"class":839,"line":847},[837,1291,1292],{"class":854},"bootstrapApplication",[837,1294,1295],{"class":937},"(App",[837,1297,877],{"class":862},[837,1299,1300],{"class":937}," appConfig)",[837,1302,1040],{"class":869},[837,1304,1305],{"class":854},"catch",[837,1307,859],{"class":937},[837,1309,859],{"class":862},[837,1311,1312],{"class":1002},"error",[837,1314,1011],{"class":862},[837,1316,1114],{"class":924},[837,1318,1319],{"class":937}," console",[837,1321,1040],{"class":869},[837,1323,1312],{"class":854},[837,1325,1326],{"class":937},"(error))",[837,1328,941],{"class":862},[644,1330,1332],{"className":831,"code":1331,"language":833,"meta":653,"style":653},"\u002F\u002F app.config.ts\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes, withComponentInputBinding()),\n    provideHttpClient(withFetch(), withInterceptors([authInterceptor, retryInterceptor])),\n    provideAnimationsAsync(),\n    \u002F\u002F your own feature configuration, exposed the same way\n    provideAppConfig({ apiUrl: '\u002Fapi', pageSize: 50 }),\n  ],\n};\n",[651,1333,1334,1339,1359,1369,1388,1415,1424,1429,1462,1469],{"__ignoreMap":653},[837,1335,1336],{"class":839,"line":840},[837,1337,1338],{"class":843},"\u002F\u002F app.config.ts\n",[837,1340,1341,1343,1346,1350,1352,1355,1357],{"class":839,"line":847},[837,1342,898],{"class":897},[837,1344,1345],{"class":897}," const",[837,1347,1349],{"class":1348},"sY2Jy"," appConfig",[837,1351,870],{"class":924},[837,1353,1354],{"class":904}," ApplicationConfig",[837,1356,950],{"class":924},[837,1358,914],{"class":862},[837,1360,1361,1364,1366],{"class":839,"line":894},[837,1362,1363],{"class":937},"  providers",[837,1365,870],{"class":869},[837,1367,1368],{"class":937}," [\n",[837,1370,1371,1374,1377,1379,1382,1385],{"class":839,"line":917},[837,1372,1373],{"class":854},"    provideRouter",[837,1375,1376],{"class":937},"(routes",[837,1378,877],{"class":862},[837,1380,1381],{"class":854}," withComponentInputBinding",[837,1383,1384],{"class":937},"())",[837,1386,1387],{"class":862},",\n",[837,1389,1390,1393,1395,1398,1400,1402,1405,1408,1410,1413],{"class":839,"line":944},[837,1391,1392],{"class":854},"    provideHttpClient",[837,1394,859],{"class":937},[837,1396,1397],{"class":854},"withFetch",[837,1399,1028],{"class":937},[837,1401,877],{"class":862},[837,1403,1404],{"class":854}," withInterceptors",[837,1406,1407],{"class":937},"([authInterceptor",[837,1409,877],{"class":862},[837,1411,1412],{"class":937}," retryInterceptor]))",[837,1414,1387],{"class":862},[837,1416,1417,1420,1422],{"class":839,"line":959},[837,1418,1419],{"class":854},"    provideAnimationsAsync",[837,1421,1028],{"class":937},[837,1423,1387],{"class":862},[837,1425,1426],{"class":839,"line":984},[837,1427,1428],{"class":843},"    \u002F\u002F your own feature configuration, exposed the same way\n",[837,1430,1431,1434,1436,1438,1441,1443,1446,1448,1451,1453,1456,1458,1460],{"class":839,"line":991},[837,1432,1433],{"class":854},"    provideAppConfig",[837,1435,859],{"class":937},[837,1437,863],{"class":862},[837,1439,1440],{"class":937}," apiUrl",[837,1442,870],{"class":869},[837,1444,1445],{"class":873}," '\u002Fapi'",[837,1447,877],{"class":862},[837,1449,1450],{"class":937}," pageSize",[837,1452,870],{"class":869},[837,1454,1455],{"class":953}," 50",[837,1457,888],{"class":862},[837,1459,1011],{"class":937},[837,1461,1387],{"class":862},[837,1463,1464,1467],{"class":839,"line":1017},[837,1465,1466],{"class":937},"  ]",[837,1468,1387],{"class":862},[837,1470,1471],{"class":839,"line":1022},[837,1472,1473],{"class":862},"};\n",[594,1475,1476],{},"Three conventions worth adopting:",[594,1478,1479,1486,1487,679,1490,1493],{},[597,1480,1481,1482,1485],{},"Expose configuration through ",[651,1483,1484],{},"provide*"," functions, not raw provider arrays."," This is the\npattern every Angular library uses (",[651,1488,1489],{},"provideRouter",[651,1491,1492],{},"provideHttpClient",") and it is the right\npattern for your own feature libraries too. It keeps the token private and gives you one\nplace to validate or default the config:",[644,1495,1497],{"className":831,"code":1496,"language":833,"meta":653,"style":653},"export function provideAppConfig(config: AppConfig): EnvironmentProviders {\n  return makeEnvironmentProviders([\n    { provide: APP_CONFIG, useValue: config },\n    TelemetryService,\n  ]);\n}\n",[651,1498,1499,1528,1539,1565,1572,1579],{"__ignoreMap":653},[837,1500,1501,1503,1506,1509,1511,1514,1516,1519,1521,1523,1526],{"class":839,"line":840},[837,1502,898],{"class":897},[837,1504,1505],{"class":897}," function",[837,1507,1508],{"class":854}," provideAppConfig",[837,1510,859],{"class":862},[837,1512,1513],{"class":1002},"config",[837,1515,870],{"class":924},[837,1517,1518],{"class":904}," AppConfig",[837,1520,1011],{"class":862},[837,1522,870],{"class":924},[837,1524,1525],{"class":904}," EnvironmentProviders",[837,1527,914],{"class":862},[837,1529,1530,1533,1536],{"class":839,"line":847},[837,1531,1532],{"class":897},"  return",[837,1534,1535],{"class":854}," makeEnvironmentProviders",[837,1537,1538],{"class":937},"([\n",[837,1540,1541,1544,1547,1549,1552,1554,1557,1559,1562],{"class":839,"line":894},[837,1542,1543],{"class":862},"    {",[837,1545,1546],{"class":937}," provide",[837,1548,870],{"class":869},[837,1550,1551],{"class":1348}," APP_CONFIG",[837,1553,877],{"class":862},[837,1555,1556],{"class":937}," useValue",[837,1558,870],{"class":869},[837,1560,1561],{"class":937}," config ",[837,1563,1564],{"class":862},"},\n",[837,1566,1567,1570],{"class":839,"line":917},[837,1568,1569],{"class":937},"    TelemetryService",[837,1571,1387],{"class":862},[837,1573,1574,1577],{"class":839,"line":944},[837,1575,1576],{"class":937},"  ])",[837,1578,941],{"class":862},[837,1580,1581],{"class":839,"line":959},[837,1582,1222],{"class":862},[594,1584,1585,1591,1592,1595,1596,1599],{},[597,1586,1587,1590],{},[651,1588,1589],{},"withComponentInputBinding()"," is close to mandatory."," It binds route params, query params,\nand resolved data straight to signal ",[651,1593,1594],{},"input()","s on the routed component, which deletes an\nentire category of ",[651,1597,1598],{},"ActivatedRoute"," plumbing. More in §1.4.",[594,1601,1602,1610,1611,1614,1615,1618],{},[597,1603,1604,1607,1608,1040],{},[651,1605,1606],{},"withFetch()"," on ",[651,1609,1492],{}," Uses the ",[651,1612,1613],{},"fetch"," API instead of ",[651,1616,1617],{},"XMLHttpRequest",";\nbetter SSR behaviour and fewer polyfills.",[1266,1620,1622],{"id":1621},"_12-how-to-group-code",[22,1623,61],{"title":625,"href":60},[594,1625,1626,1627,1630],{},"Angular has no opinion here, which means you need one. The grouping that survives contact\nwith a growing team is by ",[597,1628,1629],{},"feature",", with a strict dependency direction:",[644,1632,1635],{"className":1633,"code":1634,"language":649},[647],"src\u002Fapp\u002F\n  app.ts                     ← root shell component\n  app.config.ts              ← bootstrap providers\n  app.routes.ts              ← top-level route table\n  core\u002F                      ← app-wide singletons: auth, interceptors, error handling\n  features\u002F\n    carriers\u002F\n      carriers.routes.ts     ← lazy entry point for the feature\n      carrier-list\u002F          ← component + store + template + styles, colocated\n      carrier-detail\u002F\n    shipments\u002F\n  data-access\u002F\n    carriers\u002F                ← CarriersApiService, shared CarriersStore, models\n    shipments\u002F\n  ui\u002F                        ← presentational components: buttons, cards, empty states\n  util\u002F                      ← pure functions: formatting, date math, comparators\n",[651,1636,1634],{"__ignoreMap":653},[594,1638,1639,1640,1643],{},"The layout matters less than the ",[597,1641,1642],{},"dependency rule",", which is what actually prevents rot:",[1645,1646,1647,1663],"table",{},[1648,1649,1650],"thead",{},[1651,1652,1653,1657,1660],"tr",{},[1654,1655,1656],"th",{},"Layer",[1654,1658,1659],{},"May depend on",[1654,1661,1662],{},"Must never depend on",[1664,1665,1666,1693,1713,1730],"tbody",{},[1651,1667,1668,1674,1688],{},[1669,1670,1671],"td",{},[651,1672,1673],{},"features\u002F*",[1669,1675,1676,679,1679,679,1682,679,1685],{},[651,1677,1678],{},"data-access",[651,1680,1681],{},"ui",[651,1683,1684],{},"util",[651,1686,1687],{},"core",[1669,1689,1690],{},[597,1691,1692],{},"another feature",[1651,1694,1695,1700,1706],{},[1669,1696,1697],{},[651,1698,1699],{},"data-access\u002F*",[1669,1701,1702,679,1704],{},[651,1703,1684],{},[651,1705,1687],{},[1669,1707,1708,679,1711],{},[651,1709,1710],{},"features",[651,1712,1681],{},[1651,1714,1715,1720,1724],{},[1669,1716,1717],{},[651,1718,1719],{},"ui\u002F*",[1669,1721,1722],{},[651,1723,1684],{},[1669,1725,1726,679,1728],{},[651,1727,1710],{},[651,1729,1678],{},[1651,1731,1732,1737,1740],{},[1669,1733,1734],{},[651,1735,1736],{},"util\u002F*",[1669,1738,1739],{},"nothing",[1669,1741,1742],{},"everything else",[594,1744,1745,1746,1749,1750,679,1752,1241,1754,1040],{},"Read that table as one sentence: ",[597,1747,1748],{},"dependencies point downward only, and features never\nimport each other."," Two features that need to share something are telling you that the\nshared thing belongs in ",[651,1751,1678],{},[651,1753,1681],{},[651,1755,1684],{},[594,1757,1758],{},"Two rules make this stick:",[658,1760,1761,1778],{},[18,1762,1763,1766,1767,679,1770,1773,1774,1777],{},[597,1764,1765],{},"Colocate by feature, not by type."," A component's class, template, styles, store, and\nspec live in one directory. Directories named ",[651,1768,1769],{},"components\u002F",[651,1771,1772],{},"services\u002F",", and ",[651,1775,1776],{},"stores\u002F","\neach holding forty unrelated files are an organisational smell — they guarantee that\nworking on one feature means touching four distant places.",[18,1779,1780,1783,1784,1787,1788,1791],{},[597,1781,1782],{},"Enforce the boundaries mechanically."," In an Nx workspace this is project tags plus the\n",[651,1785,1786],{},"enforce-module-boundaries"," ESLint rule. In a plain CLI workspace, ",[651,1789,1790],{},"eslint-plugin-boundaries","\nor an import-path lint rule does the job. An architecture rule that only lives in a\ndocument is a rule your agent will breach without noticing.",[1266,1793,1795],{"id":1794},"_13-when-folders-arent-enough-real-library-boundaries",[22,1796,67],{"title":625,"href":66},[594,1798,1799],{},"The layout above is folders in one app, lint-enforced. That's the right amount of structure\nfor a single team on a single deployable. Past a certain size — multiple teams owning\ndifferent features, a shared design system consumed by more than one app, or a CI pipeline\nslow enough that \"only test what changed\" stops being optional — folders stop being enough,\nbecause a lint rule can be disabled with a comment and an import path is still just a string.",[594,1801,1802,1803,679,1806,679,1809,1811,1812,1814,1815,1818,1819,1822,1823,1387,1825,679,1827,679,1829,1831,1832,1835,1836,1838,1839,1841,1842,679,1845,679,1848,1851,1852,1855],{},"The next step is real libraries: each of ",[651,1804,1805],{},"features\u002Fcarriers",[651,1807,1808],{},"data-access\u002Fcarriers",[651,1810,1681],{},",\nand ",[651,1813,1684],{}," becomes its own buildable unit with its own ",[651,1816,1817],{},"tsconfig"," path (Nx's\n",[651,1820,1821],{},"nx g @nx\u002Fangular:library"," generators map onto exactly these four types — ",[651,1824,1629],{},[651,1826,1678],{},[651,1828,1681],{},[651,1830,1684],{}," — which is not a coincidence; the directory layout above ",[603,1833,1834],{},"is"," the\nNx library taxonomy, one step before you need the tooling). A forbidden import (a feature\nreaching into another feature, ",[651,1837,1681],{}," reaching into ",[651,1840,1678],{},") becomes a build-time\nmodule-resolution error, not a lint warning someone silences. Tag each library\n(",[651,1843,1844],{},"scope:carriers",[651,1846,1847],{},"type:feature",[651,1849,1850],{},"type:data-access",", …) and encode the dependency-rule\ntable above as ",[651,1853,1854],{},"depConstraints"," — the table stops being documentation and becomes an\nenforced constraint.",[594,1857,1858],{},"Two practical notes:",[658,1860,1861,1867],{},[18,1862,1863,1866],{},[597,1864,1865],{},"Default to non-buildable libraries"," (Nx computes them as part of the app build) unless a\nlibrary is independently published (a shared design-system package consumed by other repos)\nor built by a separate pipeline. Buildable-by-default adds config overhead most libraries\nnever need.",[18,1868,1869,1872,1873,1876],{},[597,1870,1871],{},"Pair each top-level tag with a human owner"," (a ",[651,1874,1875],{},"CODEOWNERS"," entry matching the library's\npath) so the boundary has someone who gets pinged on a violation, not just a CI failure\nsomeone routes around.",[594,1878,1879],{},"Don't reach for this by default on a single-team app — folders plus a lint rule cost nothing\nand a premature Nx migration is ceremony with no payoff. Graduate when the pain (slow full-repo\nCI, cross-team merge conflicts on shared files, a design system that needs its own release\ncadence) actually shows up.",[1266,1881,1883],{"id":1882},"_14-routing-is-the-architectural-spine",[22,1884,73],{"title":625,"href":72},[594,1886,1887],{},"The route table is where lazy loading, access control, and data pre-fetching are declared.\nTreat it as architecture, not configuration.",[644,1889,1891],{"className":831,"code":1890,"language":833,"meta":653,"style":653},"\u002F\u002F app.routes.ts\nexport const routes: Routes = [\n  { path: '', pathMatch: 'full', redirectTo: 'carriers' },\n  {\n    path: 'carriers',\n    loadChildren: () => import('.\u002Ffeatures\u002Fcarriers\u002Fcarriers.routes'),\n    canMatch: [featureEnabledGuard('carriers')],\n  },\n  {\n    path: 'admin',\n    loadChildren: () => import('.\u002Ffeatures\u002Fadmin\u002Fadmin.routes'),\n    canActivate: [authGuard, roleGuard('admin')],\n  },\n  { path: '**', loadComponent: () => import('.\u002Ffeatures\u002Fnot-found\u002Fnot-found') },\n];\n",[651,1892,1893,1898,1916,1951,1956,1967,1991,2014,2019,2023,2034,2055,2079,2083,2117],{"__ignoreMap":653},[837,1894,1895],{"class":839,"line":840},[837,1896,1897],{"class":843},"\u002F\u002F app.routes.ts\n",[837,1899,1900,1902,1904,1907,1909,1912,1914],{"class":839,"line":847},[837,1901,898],{"class":897},[837,1903,1345],{"class":897},[837,1905,1906],{"class":1348}," routes",[837,1908,870],{"class":924},[837,1910,1911],{"class":904}," Routes",[837,1913,950],{"class":924},[837,1915,1368],{"class":937},[837,1917,1918,1921,1924,1926,1929,1931,1934,1936,1939,1941,1944,1946,1949],{"class":839,"line":894},[837,1919,1920],{"class":862},"  {",[837,1922,1923],{"class":937}," path",[837,1925,870],{"class":869},[837,1927,1928],{"class":873}," ''",[837,1930,877],{"class":862},[837,1932,1933],{"class":937}," pathMatch",[837,1935,870],{"class":869},[837,1937,1938],{"class":873}," 'full'",[837,1940,877],{"class":862},[837,1942,1943],{"class":937}," redirectTo",[837,1945,870],{"class":869},[837,1947,1948],{"class":873}," 'carriers'",[837,1950,1148],{"class":862},[837,1952,1953],{"class":839,"line":917},[837,1954,1955],{"class":862},"  {\n",[837,1957,1958,1961,1963,1965],{"class":839,"line":944},[837,1959,1960],{"class":937},"    path",[837,1962,870],{"class":869},[837,1964,1948],{"class":873},[837,1966,1387],{"class":862},[837,1968,1969,1972,1974,1977,1979,1982,1984,1987,1989],{"class":839,"line":959},[837,1970,1971],{"class":854},"    loadChildren",[837,1973,870],{"class":869},[837,1975,1976],{"class":862}," ()",[837,1978,1114],{"class":924},[837,1980,1981],{"class":924}," import",[837,1983,859],{"class":937},[837,1985,1986],{"class":873},"'.\u002Ffeatures\u002Fcarriers\u002Fcarriers.routes'",[837,1988,1011],{"class":937},[837,1990,1387],{"class":862},[837,1992,1993,1996,1998,2001,2004,2006,2009,2012],{"class":839,"line":984},[837,1994,1995],{"class":937},"    canMatch",[837,1997,870],{"class":869},[837,1999,2000],{"class":937}," [",[837,2002,2003],{"class":854},"featureEnabledGuard",[837,2005,859],{"class":937},[837,2007,2008],{"class":873},"'carriers'",[837,2010,2011],{"class":937},")]",[837,2013,1387],{"class":862},[837,2015,2016],{"class":839,"line":991},[837,2017,2018],{"class":862},"  },\n",[837,2020,2021],{"class":839,"line":1017},[837,2022,1955],{"class":862},[837,2024,2025,2027,2029,2032],{"class":839,"line":1022},[837,2026,1960],{"class":937},[837,2028,870],{"class":869},[837,2030,2031],{"class":873}," 'admin'",[837,2033,1387],{"class":862},[837,2035,2036,2038,2040,2042,2044,2046,2048,2051,2053],{"class":839,"line":1033},[837,2037,1971],{"class":854},[837,2039,870],{"class":869},[837,2041,1976],{"class":862},[837,2043,1114],{"class":924},[837,2045,1981],{"class":924},[837,2047,859],{"class":937},[837,2049,2050],{"class":873},"'.\u002Ffeatures\u002Fadmin\u002Fadmin.routes'",[837,2052,1011],{"class":937},[837,2054,1387],{"class":862},[837,2056,2057,2060,2062,2065,2067,2070,2072,2075,2077],{"class":839,"line":1053},[837,2058,2059],{"class":937},"    canActivate",[837,2061,870],{"class":869},[837,2063,2064],{"class":937}," [authGuard",[837,2066,877],{"class":862},[837,2068,2069],{"class":854}," roleGuard",[837,2071,859],{"class":937},[837,2073,2074],{"class":873},"'admin'",[837,2076,2011],{"class":937},[837,2078,1387],{"class":862},[837,2080,2081],{"class":839,"line":1098},[837,2082,2018],{"class":862},[837,2084,2085,2087,2089,2091,2094,2096,2099,2101,2103,2105,2107,2109,2112,2115],{"class":839,"line":1151},[837,2086,1920],{"class":862},[837,2088,1923],{"class":937},[837,2090,870],{"class":869},[837,2092,2093],{"class":873}," '**'",[837,2095,877],{"class":862},[837,2097,2098],{"class":854}," loadComponent",[837,2100,870],{"class":869},[837,2102,1976],{"class":862},[837,2104,1114],{"class":924},[837,2106,1981],{"class":924},[837,2108,859],{"class":937},[837,2110,2111],{"class":873},"'.\u002Ffeatures\u002Fnot-found\u002Fnot-found'",[837,2113,2114],{"class":937},") ",[837,2116,1564],{"class":862},[837,2118,2119,2122],{"class":839,"line":1203},[837,2120,2121],{"class":937},"]",[837,2123,941],{"class":862},[644,2125,2127],{"className":831,"code":2126,"language":833,"meta":653,"style":653},"\u002F\u002F features\u002Fcarriers\u002Fcarriers.routes.ts\nexport default [\n  {\n    path: '',\n    \u002F\u002F Feature-scoped providers: created on entry, destroyed on exit.\n    providers: [provideCarrierFilters()],\n    children: [\n      { path: '', loadComponent: () => import('.\u002Fcarrier-list\u002Fcarrier-list') },\n      { path: ':carrierId', loadComponent: () => import('.\u002Fcarrier-detail\u002Fcarrier-detail') },\n    ],\n  },\n] satisfies Routes;\n",[651,2128,2129,2134,2143,2147,2157,2162,2179,2188,2220,2252,2259,2263],{"__ignoreMap":653},[837,2130,2131],{"class":839,"line":840},[837,2132,2133],{"class":843},"\u002F\u002F features\u002Fcarriers\u002Fcarriers.routes.ts\n",[837,2135,2136,2138,2141],{"class":839,"line":847},[837,2137,898],{"class":897},[837,2139,2140],{"class":897}," default",[837,2142,1368],{"class":937},[837,2144,2145],{"class":839,"line":894},[837,2146,1955],{"class":862},[837,2148,2149,2151,2153,2155],{"class":839,"line":917},[837,2150,1960],{"class":937},[837,2152,870],{"class":869},[837,2154,1928],{"class":873},[837,2156,1387],{"class":862},[837,2158,2159],{"class":839,"line":944},[837,2160,2161],{"class":843},"    \u002F\u002F Feature-scoped providers: created on entry, destroyed on exit.\n",[837,2163,2164,2167,2169,2171,2174,2177],{"class":839,"line":959},[837,2165,2166],{"class":937},"    providers",[837,2168,870],{"class":869},[837,2170,2000],{"class":937},[837,2172,2173],{"class":854},"provideCarrierFilters",[837,2175,2176],{"class":937},"()]",[837,2178,1387],{"class":862},[837,2180,2181,2184,2186],{"class":839,"line":984},[837,2182,2183],{"class":937},"    children",[837,2185,870],{"class":869},[837,2187,1368],{"class":937},[837,2189,2190,2193,2195,2197,2199,2201,2203,2205,2207,2209,2211,2213,2216,2218],{"class":839,"line":991},[837,2191,2192],{"class":862},"      {",[837,2194,1923],{"class":937},[837,2196,870],{"class":869},[837,2198,1928],{"class":873},[837,2200,877],{"class":862},[837,2202,2098],{"class":854},[837,2204,870],{"class":869},[837,2206,1976],{"class":862},[837,2208,1114],{"class":924},[837,2210,1981],{"class":924},[837,2212,859],{"class":937},[837,2214,2215],{"class":873},"'.\u002Fcarrier-list\u002Fcarrier-list'",[837,2217,2114],{"class":937},[837,2219,1564],{"class":862},[837,2221,2222,2224,2226,2228,2231,2233,2235,2237,2239,2241,2243,2245,2248,2250],{"class":839,"line":1017},[837,2223,2192],{"class":862},[837,2225,1923],{"class":937},[837,2227,870],{"class":869},[837,2229,2230],{"class":873}," ':carrierId'",[837,2232,877],{"class":862},[837,2234,2098],{"class":854},[837,2236,870],{"class":869},[837,2238,1976],{"class":862},[837,2240,1114],{"class":924},[837,2242,1981],{"class":924},[837,2244,859],{"class":937},[837,2246,2247],{"class":873},"'.\u002Fcarrier-detail\u002Fcarrier-detail'",[837,2249,2114],{"class":937},[837,2251,1564],{"class":862},[837,2253,2254,2257],{"class":839,"line":1022},[837,2255,2256],{"class":937},"    ]",[837,2258,1387],{"class":862},[837,2260,2261],{"class":839,"line":1033},[837,2262,2018],{"class":862},[837,2264,2265,2268,2271,2273],{"class":839,"line":1053},[837,2266,2267],{"class":937},"] ",[837,2269,2270],{"class":897},"satisfies",[837,2272,1911],{"class":904},[837,2274,941],{"class":862},[594,2276,2277,2280,2281,2284,2285,2288,2289,2292],{},[597,2278,2279],{},"Lazy-load every feature."," Eager-load only the shell and the landing route. ",[651,2282,2283],{},"loadChildren","\nfor a feature's route table, ",[651,2286,2287],{},"loadComponent"," for a single routed component. Both loaders run\ninside an injection context, so you can ",[651,2290,2291],{},"inject()"," a feature-flag service and pick which\nchunk to load.",[594,2294,2295,2304,2305,2307,2308,2311,2312,2314,2315,2318,2319,2321,2322,2324],{},[597,2296,2297,2300,2301,1040],{},[651,2298,2299],{},"canMatch"," vs ",[651,2302,2303],{},"canActivate"," ",[651,2306,2299],{}," decides whether the route is ",[603,2309,2310],{},"considered at all"," —\nif it returns false the router keeps looking at later routes, so it's the correct guard for\nfeature flags and licensing (the route effectively doesn't exist). ",[651,2313,2303],{}," decides\nwhether ",[603,2316,2317],{},"this"," user may proceed, and typically redirects. Use ",[651,2320,2299],{}," for \"does this route\nexist for this tenant,\" ",[651,2323,2303],{}," for \"is this user allowed in.\"",[594,2326,2327,2330,2331,2333,2334,2337],{},[597,2328,2329],{},"Route params reach the component as signal inputs."," With ",[651,2332,1589],{},"\nenabled, a ",[651,2335,2336],{},":carrierId"," path param binds directly:",[644,2339,2341],{"className":831,"code":2340,"language":833,"meta":653,"style":653},"export class CarrierDetail {\n  \u002F\u002F Bound from the route. No ActivatedRoute, no snapshot, no subscription.\n  readonly carrierId = input.required\u003Cstring>();\n}\n",[651,2342,2343,2354,2359,2388],{"__ignoreMap":653},[837,2344,2345,2347,2349,2352],{"class":839,"line":840},[837,2346,898],{"class":897},[837,2348,901],{"class":897},[837,2350,2351],{"class":904}," CarrierDetail",[837,2353,914],{"class":862},[837,2355,2356],{"class":839,"line":847},[837,2357,2358],{"class":843},"  \u002F\u002F Bound from the route. No ActivatedRoute, no snapshot, no subscription.\n",[837,2360,2361,2364,2367,2369,2372,2374,2377,2379,2382,2384,2386],{"class":839,"line":894},[837,2362,2363],{"class":897},"  readonly",[837,2365,2366],{"class":920}," carrierId",[837,2368,950],{"class":924},[837,2370,2371],{"class":937}," input",[837,2373,1040],{"class":869},[837,2375,2376],{"class":854},"required",[837,2378,1069],{"class":1068},[837,2380,2381],{"class":967},"string",[837,2383,1078],{"class":1068},[837,2385,1028],{"class":937},[837,2387,941],{"class":862},[837,2389,2390],{"class":839,"line":917},[837,2391,1222],{"class":862},[594,2393,2394,2395,2398,2399,2401],{},"This is a genuinely important simplification. The pre-signals pattern —\n",[651,2396,2397],{},"route.paramMap.subscribe(...)"," — was the most common source of subscriptions in components,\nand it is now gone. Anything that reads route state through ",[651,2400,1598],{}," in new code\nshould be questioned.",[594,2403,2404,2407,2408,2411],{},[597,2405,2406],{},"Resolvers: use sparingly."," A ",[651,2409,2410],{},"ResolveFn"," blocks navigation until data arrives, which\nmeans a slow endpoint becomes a frozen UI with no feedback. Prefer navigating immediately and\nletting the store's call state render a skeleton. Resolvers earn their place when the route\ngenuinely cannot render without the data (a 404 decision, a permission-shaped redirect).",[594,2413,2414,2417,2418,2420,2421,1040],{},[597,2415,2416],{},"Guards are UX, not security."," They hide what a user shouldn't reach; they do not protect\nanything. Every rule enforced in a guard must also be enforced server-side. An agent will\nhappily write a ",[651,2419,2303],{}," and consider the feature secured — it isn't. More in\n",[22,2422,2423],{"href":343},"Part 5",[1266,2425,2427],{"id":2426},"_15-feature-flags",[22,2428,79],{"title":625,"href":78},[594,2430,2431],{},"A flag gates three different layers, and treating it as a single ad hoc check scattered\nacross all three is how flags become untestable and permanent. The three layers:",[1645,2433,2434,2443],{},[1648,2435,2436],{},[1651,2437,2438,2440],{},[1654,2439,1656],{},[1654,2441,2442],{},"Mechanism",[1664,2444,2445,2456,2466],{},[1651,2446,2447,2450],{},[1669,2448,2449],{},"Route existence",[1669,2451,2452,2455],{},[651,2453,2454],{},"canMatch: [featureEnabledGuard('carriers-v2')]"," — shown above",[1651,2457,2458,2461],{},[1669,2459,2460],{},"Rendered UI",[1669,2462,2463],{},[651,2464,2465],{},"@if (flags.isEnabled('carriers-v2')) { ... }",[1651,2467,2468,2471],{},[1669,2469,2470],{},"Store\u002Fasync behaviour",[1669,2472,2473],{},"The flag value passed to the store, or read via normal DI in a method",[594,2475,2476,2477,2479],{},"One service, signal-backed, loaded before the app becomes interactive so the first ",[651,2478,2299],{},"\ncheck already has data:",[644,2481,2483],{"className":831,"code":2482,"language":833,"meta":653,"style":653},"@Injectable({ providedIn: 'root' })\nexport class FeatureFlagsService {\n  private readonly flags = signal\u003CRecord\u003Cstring, boolean>>({});\n  isEnabled(flag: string): boolean { return this.flags()[flag] ?? false; }\n  setAll(flags: Record\u003Cstring, boolean>): void { this.flags.set(flags); }\n}\n\nexport function provideFeatureFlags(): EnvironmentProviders {\n  return makeEnvironmentProviders([\n    provideAppInitializer(async () => {\n      const api = inject(FeatureFlagsApiService);\n      inject(FeatureFlagsService).setAll(await firstValueFrom(api.loadFlags()));\n    }),\n  ]);\n}\n",[651,2484,2485,2508,2519,2561,2606,2657,2661,2665,2682,2690,2706,2724,2758,2766,2772],{"__ignoreMap":653},[837,2486,2487,2489,2492,2494,2496,2499,2501,2504,2506],{"class":839,"line":840},[837,2488,851],{"class":850},[837,2490,2491],{"class":854},"Injectable",[837,2493,859],{"class":858},[837,2495,863],{"class":862},[837,2497,2498],{"class":858}," providedIn",[837,2500,870],{"class":869},[837,2502,2503],{"class":873}," 'root'",[837,2505,888],{"class":862},[837,2507,891],{"class":858},[837,2509,2510,2512,2514,2517],{"class":839,"line":847},[837,2511,898],{"class":897},[837,2513,901],{"class":897},[837,2515,2516],{"class":904}," FeatureFlagsService",[837,2518,914],{"class":862},[837,2520,2521,2524,2527,2530,2532,2535,2537,2540,2542,2544,2546,2549,2552,2554,2557,2559],{"class":839,"line":894},[837,2522,2523],{"class":897},"  private",[837,2525,2526],{"class":897}," readonly",[837,2528,2529],{"class":920}," flags",[837,2531,950],{"class":924},[837,2533,2534],{"class":854}," signal",[837,2536,1069],{"class":1068},[837,2538,2539],{"class":904},"Record",[837,2541,1069],{"class":1068},[837,2543,2381],{"class":967},[837,2545,877],{"class":862},[837,2547,2548],{"class":967}," boolean",[837,2550,2551],{"class":1068},">>",[837,2553,859],{"class":937},[837,2555,2556],{"class":862},"{}",[837,2558,1011],{"class":937},[837,2560,941],{"class":862},[837,2562,2563,2566,2568,2571,2573,2575,2577,2579,2581,2583,2586,2588,2590,2593,2596,2599,2601,2603],{"class":839,"line":917},[837,2564,2565],{"class":854},"  isEnabled",[837,2567,859],{"class":862},[837,2569,2570],{"class":1002},"flag",[837,2572,870],{"class":924},[837,2574,968],{"class":967},[837,2576,1011],{"class":862},[837,2578,870],{"class":924},[837,2580,2548],{"class":967},[837,2582,1117],{"class":862},[837,2584,2585],{"class":897}," return",[837,2587,1120],{"class":1036},[837,2589,1040],{"class":869},[837,2591,2592],{"class":854},"flags",[837,2594,2595],{"class":937},"()[flag] ",[837,2597,2598],{"class":924},"??",[837,2600,954],{"class":953},[837,2602,1133],{"class":862},[837,2604,2605],{"class":862}," }\n",[837,2607,2608,2611,2613,2615,2617,2620,2622,2624,2626,2628,2630,2632,2634,2637,2639,2641,2643,2645,2647,2650,2653,2655],{"class":839,"line":944},[837,2609,2610],{"class":854},"  setAll",[837,2612,859],{"class":862},[837,2614,2592],{"class":1002},[837,2616,870],{"class":924},[837,2618,2619],{"class":904}," Record",[837,2621,1069],{"class":1068},[837,2623,2381],{"class":967},[837,2625,877],{"class":862},[837,2627,2548],{"class":967},[837,2629,1078],{"class":1068},[837,2631,1011],{"class":862},[837,2633,870],{"class":924},[837,2635,2636],{"class":967}," void",[837,2638,1117],{"class":862},[837,2640,1120],{"class":1036},[837,2642,1040],{"class":869},[837,2644,2592],{"class":937},[837,2646,1040],{"class":869},[837,2648,2649],{"class":854},"set",[837,2651,2652],{"class":937},"(flags)",[837,2654,1133],{"class":862},[837,2656,2605],{"class":862},[837,2658,2659],{"class":839,"line":959},[837,2660,1222],{"class":862},[837,2662,2663],{"class":839,"line":984},[837,2664,988],{"emptyLinePlaceholder":987},[837,2666,2667,2669,2671,2674,2676,2678,2680],{"class":839,"line":991},[837,2668,898],{"class":897},[837,2670,1505],{"class":897},[837,2672,2673],{"class":854}," provideFeatureFlags",[837,2675,1028],{"class":862},[837,2677,870],{"class":924},[837,2679,1525],{"class":904},[837,2681,914],{"class":862},[837,2683,2684,2686,2688],{"class":839,"line":1017},[837,2685,1532],{"class":897},[837,2687,1535],{"class":854},[837,2689,1538],{"class":937},[837,2691,2692,2695,2697,2700,2702,2704],{"class":839,"line":1022},[837,2693,2694],{"class":854},"    provideAppInitializer",[837,2696,859],{"class":937},[837,2698,2699],{"class":897},"async",[837,2701,1976],{"class":862},[837,2703,1114],{"class":924},[837,2705,914],{"class":862},[837,2707,2708,2711,2714,2716,2719,2722],{"class":839,"line":1033},[837,2709,2710],{"class":897},"      const",[837,2712,2713],{"class":1348}," api",[837,2715,950],{"class":924},[837,2717,2718],{"class":854}," inject",[837,2720,2721],{"class":937},"(FeatureFlagsApiService)",[837,2723,941],{"class":862},[837,2725,2726,2729,2732,2734,2737,2739,2742,2745,2748,2750,2753,2756],{"class":839,"line":1053},[837,2727,2728],{"class":854},"      inject",[837,2730,2731],{"class":937},"(FeatureFlagsService)",[837,2733,1040],{"class":869},[837,2735,2736],{"class":854},"setAll",[837,2738,859],{"class":937},[837,2740,2741],{"class":897},"await",[837,2743,2744],{"class":854}," firstValueFrom",[837,2746,2747],{"class":937},"(api",[837,2749,1040],{"class":869},[837,2751,2752],{"class":854},"loadFlags",[837,2754,2755],{"class":937},"()))",[837,2757,941],{"class":862},[837,2759,2760,2762,2764],{"class":839,"line":1098},[837,2761,1206],{"class":862},[837,2763,1011],{"class":937},[837,2765,1387],{"class":862},[837,2767,2768,2770],{"class":839,"line":1151},[837,2769,1576],{"class":937},[837,2771,941],{"class":862},[837,2773,2774],{"class":839,"line":1203},[837,2775,1222],{"class":862},[594,2777,2778,2779,2782,2783,2786,2787,2790],{},"A store that genuinely branches on a flag injects ",[651,2780,2781],{},"FeatureFlagsService"," like any other\ndependency, as a default parameter in ",[651,2784,2785],{},"withMethods"," — it's a service, not a special case. What\nit should ",[603,2788,2789],{},"not"," do is become the way every store discovers its own on\u002Foff state for\nunrelated reasons; if half a codebase's stores inject the flag service, that's a sign flags\nare being used as informal configuration rather than a rollout mechanism.",[594,2792,2793,2796,2797,2800,2801,2803],{},[597,2794,2795],{},"Every flag is debt on a timer."," Give it an owner and a removal condition (\"delete both\nbranches once at 100% for two weeks\") when it's created. A flag with no removal plan is a\npermanent second code path that gets tested half as often as the primary one. In ",[651,2798,2799],{},"TestBed",",\noverride ",[651,2802,2781],{}," and exercise both branches explicitly — an untested \"flag off\"\npath is exactly where the next regression hides.",[594,2805,2806],{},"For gradual rollout, bucket by a stable identifier (user or session ID) hashed against the\ntarget percentage once, at flag-load time — not re-rolled on every check, which would flip a\nuser between variants mid-session.",[1266,2808,2810],{"id":2809},"_16-lazy-load-inside-templates-with-defer",[22,2811,2812,2813],{"title":625,"href":84},"1.6 Lazy-load inside templates with ",[651,2814,2815],{},"@defer",[594,2817,2818,2819,2821,2822,2825],{},"Route-level lazy loading splits pages. ",[651,2820,2815],{}," splits ",[603,2823,2824],{},"within"," a page — the right tool for\nheavy below-the-fold content: charts, comment threads, rich editors, maps.",[644,2827,2831],{"className":2828,"code":2829,"language":2830,"meta":653,"style":653},"language-html shiki shiki-themes github-light catppuccin-mocha","@defer (on viewport) {\n  \u003Capp-carrier-trend-chart [carrierId]=\"carrierId()\" \u002F>\n} @placeholder (minimum 300ms) {\n  \u003Cdiv class=\"chart-placeholder\">\u003C\u002Fdiv>\n} @loading (after 100ms; minimum 300ms) {\n  \u003Capp-spinner \u002F>\n} @error {\n  \u003Capp-error-banner message=\"Chart unavailable\" \u002F>\n}\n","html",[651,2832,2833,2838,2859,2864,2886,2891,2900,2905,2922],{"__ignoreMap":653},[837,2834,2835],{"class":839,"line":840},[837,2836,2837],{"class":937},"@defer (on viewport) {\n",[837,2839,2840,2843,2847,2851,2853,2856],{"class":839,"line":847},[837,2841,2842],{"class":869},"  \u003C",[837,2844,2846],{"class":2845},"sXECg","app-carrier-trend-chart",[837,2848,2850],{"class":2849},"sNGvK"," [carrierId]",[837,2852,934],{"class":869},[837,2854,2855],{"class":873},"\"carrierId()\"",[837,2857,2858],{"class":869}," \u002F>\n",[837,2860,2861],{"class":839,"line":894},[837,2862,2863],{"class":937},"} @placeholder (minimum 300ms) {\n",[837,2865,2866,2868,2871,2873,2875,2878,2881,2883],{"class":839,"line":917},[837,2867,2842],{"class":869},[837,2869,2870],{"class":2845},"div",[837,2872,901],{"class":2849},[837,2874,934],{"class":869},[837,2876,2877],{"class":873},"\"chart-placeholder\"",[837,2879,2880],{"class":869},">\u003C\u002F",[837,2882,2870],{"class":2845},[837,2884,2885],{"class":869},">\n",[837,2887,2888],{"class":839,"line":944},[837,2889,2890],{"class":937},"} @loading (after 100ms; minimum 300ms) {\n",[837,2892,2893,2895,2898],{"class":839,"line":959},[837,2894,2842],{"class":869},[837,2896,2897],{"class":2845},"app-spinner",[837,2899,2858],{"class":869},[837,2901,2902],{"class":839,"line":984},[837,2903,2904],{"class":937},"} @error {\n",[837,2906,2907,2909,2912,2915,2917,2920],{"class":839,"line":991},[837,2908,2842],{"class":869},[837,2910,2911],{"class":2845},"app-error-banner",[837,2913,2914],{"class":2849}," message",[837,2916,934],{"class":869},[837,2918,2919],{"class":873},"\"Chart unavailable\"",[837,2921,2858],{"class":869},[837,2923,2924],{"class":839,"line":1017},[837,2925,1222],{"class":937},[594,2927,2928,2929,2931],{},"Everything inside ",[651,2930,2815],{}," — component, directives, pipes, and their transitive imports —\nmoves to a separate chunk that isn't requested until the trigger fires.",[1645,2933,2934,2944],{},[1648,2935,2936],{},[1651,2937,2938,2941],{},[1654,2939,2940],{},"Trigger",[1654,2942,2943],{},"Fires when",[1664,2945,2946,2956,2966,2980,2990,3000,3010],{},[1651,2947,2948,2953],{},[1669,2949,2950],{},[651,2951,2952],{},"on idle",[1669,2954,2955],{},"Browser is idle (the default)",[1651,2957,2958,2963],{},[1669,2959,2960],{},[651,2961,2962],{},"on viewport",[1669,2964,2965],{},"Block scrolls into view — best default for below-the-fold",[1651,2967,2968,2977],{},[1669,2969,2970,2973,2974],{},[651,2971,2972],{},"on interaction"," \u002F ",[651,2975,2976],{},"on hover",[1669,2978,2979],{},"User interacts with or hovers the placeholder",[1651,2981,2982,2987],{},[1669,2983,2984],{},[651,2985,2986],{},"on immediate",[1669,2988,2989],{},"Right after render — still off the critical path",[1651,2991,2992,2997],{},[1669,2993,2994],{},[651,2995,2996],{},"on timer(5s)",[1669,2998,2999],{},"After a delay",[1651,3001,3002,3007],{},[1669,3003,3004],{},[651,3005,3006],{},"when \u003Cexpr>",[1669,3008,3009],{},"A condition becomes true",[1651,3011,3012,3017],{},[1669,3013,3014],{},[651,3015,3016],{},"prefetch on ...",[1669,3018,3019],{},"Fetch the chunk early, render later — combine with any trigger",[594,3021,3022,3023,3026,3027,3030,3031,3034,3035,3038],{},"The ",[651,3024,3025],{},"@placeholder","\u002F",[651,3028,3029],{},"@loading"," timings exist to prevent flicker: ",[651,3032,3033],{},"after"," delays showing a\nspinner for fast loads, ",[651,3036,3037],{},"minimum"," stops it flashing. Use them, or fast connections get a\nvisible flash on every deferred block.",[1266,3040,3042],{"id":3041},"_17-internationalization-i18n",[22,3043,91],{"title":625,"href":90},[594,3045,3046],{},"Decide the translation strategy before the first component ships. Retrofitting locale\nawareness into a codebase full of hardcoded English strings and hand-formatted dates is one of\nthe most expensive refactors an enterprise app takes on, and it's entirely avoidable by\ndeciding early.",[594,3048,3049],{},[597,3050,3051],{},"Two real options — pick one, don't run both:",[1645,3053,3054,3066],{},[1648,3055,3056],{},[1651,3057,3058,3061,3063],{},[1654,3059,3060],{},"Approach",[1654,3062,2442],{},[1654,3064,3065],{},"Fit",[1664,3067,3068,3085],{},[1651,3069,3070,3079,3082],{},[1669,3071,3072,3073,679,3076,1011],{},"Compile-time (",[651,3074,3075],{},"@angular\u002Flocalize",[651,3077,3078],{},"$localize",[1669,3080,3081],{},"A separate build artifact per locale",[1669,3083,3084],{},"No runtime language switch needed; ships in CI per market. Zero runtime cost.",[1651,3086,3087,3090,3093],{},[1669,3088,3089],{},"Runtime library (Transloco, ngx-translate)",[1669,3091,3092],{},"Loads locale JSON, switches without rebuild",[1669,3094,3095],{},"A user-facing language switcher; one build serves every locale.",[594,3097,3098,3101,3102,3104,3105,3107,3108,3111],{},[597,3099,3100],{},"Where translations live."," Alongside ",[651,3103,1678],{}," and ",[651,3106,1681],{}," — translation keys are consumed\nacross features, so they're shared infrastructure, not a feature concern. A dedicated\n",[651,3109,3110],{},"i18n\u002F"," area, not inline JSON scattered per feature.",[594,3113,3114,3117],{},[597,3115,3116],{},"Loading strategy for runtime i18n."," Lazy-load only the active locale's bundle — don't ship\nevery language up front — and treat \"translations not yet loaded\" as a real loading state,\nthe same way any other async data is handled, rather than a flash of raw translation keys.",[594,3119,3120,3123,3124,2973,3127,2973,3130,3133,3134,3137,3138,3141],{},[597,3121,3122],{},"Formatting goes through the platform, not string templates."," Dates, numbers, and currency\nroute through ",[651,3125,3126],{},"DatePipe",[651,3128,3129],{},"CurrencyPipe",[651,3131,3132],{},"Intl.*",", driven by the active ",[651,3135,3136],{},"LOCALE_ID"," — never\n",[651,3139,3140],{},"`${amount} ${currency}`",", which breaks the moment a locale puts the symbol somewhere else\nor uses a different decimal separator.",[594,3143,3144,2304,3147,3150],{},[597,3145,3146],{},"Never concatenate translated fragments.",[651,3148,3149],{},"t('you have') + count + t('items')"," breaks for\nany language with different word order or pluralization rules. Use the library's\ninterpolation\u002Fpluralization support instead of assembling a sentence from parts.",[594,3152,3153,3156,3157,3160,3161,3164,3165,3168,3169,1607,3172,3175],{},[597,3154,3155],{},"RTL falls out of the logical-properties rule for free."," A component styled with\n",[651,3158,3159],{},"margin-inline-start"," (see ",[22,3162,3163],{"href":495},"Part 8",") needs zero changes for an RTL locale; one\nstyled with ",[651,3166,3167],{},"margin-left"," needs a parallel stylesheet. Set ",[651,3170,3171],{},"dir",[651,3173,3174],{},"\u003Chtml>"," from the active\nlocale at bootstrap and on every locale change.",[594,3177,3178,3181,3182,3185,3186,3026,3188,3190],{},[597,3179,3180],{},"Locale switch is an app-wide event."," Route it through the same kind of root\nconfiguration\u002Fservice as ",[651,3183,3184],{},"provideAppConfig"," — reload the active translation bundle and update\n",[651,3187,3136],{},[651,3189,3171],{}," in one place, not per component.",[1266,3192,3194],{"id":3193},"_18-rendering-strategy-ssr-and-hydration",[22,3195,97],{"title":625,"href":96},[594,3197,3198,3201,3202,3205],{},[597,3199,3200],{},"Decide per route, not per app."," A marketing or content page wants server-rendered or\nprerendered HTML for first paint and crawlability. An authenticated internal tool behind a\nlogin wall gets little from SSR — there's nothing to crawl, and first paint is already gated\non auth — while paying the operational cost of a render server. Angular supports mixing\nrender modes (server \u002F client \u002F prerender) per route rather than an all-or-nothing choice;\ncheck the current ",[651,3203,3204],{},"@angular\u002Fssr"," route-configuration API against your installed version, since\nthis surface has changed across recent releases.",[594,3207,3208,3211],{},[597,3209,3210],{},"Hydration mismatches share one root cause: server and client computing different output for\nthe same render."," The usual culprits:",[658,3213,3214,3230,3241],{},[18,3215,3216,3217,2973,3220,2973,3223,3226,3227,3229],{},"Reading ",[651,3218,3219],{},"window",[651,3221,3222],{},"document",[651,3224,3225],{},"localStorage"," during render (see \"don't assume globals\" in\n",[22,3228,3163],{"href":489},").",[18,3231,3232,2973,3235,3238,3239,1040],{},[651,3233,3234],{},"Date.now()",[651,3236,3237],{},"Math.random()"," in a template or ",[651,3240,682],{},[18,3242,3243],{},"Locale-dependent formatting where the server's locale isn't pinned to match the client's.",[594,3245,3246],{},"A mismatch doesn't just render wrong — Angular detects it and re-renders the affected subtree\nclient-side, silently discarding the SSR benefit for exactly that content.",[594,3248,3249,1106,3252,3254,3255,3258,3259,3261,3262,3265,3266,3269],{},[597,3250,3251],{},"Incremental hydration",[651,3253,2815],{}," with a ",[651,3256,3257],{},"hydrate"," trigger, where available on your version)\nextends the ",[651,3260,2815],{}," mental model from lazy ",[603,3263,3264],{},"loading"," to lazy ",[603,3267,3268],{},"hydration",": below-the-fold or\nlow-priority content can ship as server-rendered, visible-but-non-interactive HTML that stays\ndehydrated until its trigger fires, rather than hydrating the entire page eagerly. Same\ntriggers as the table above; the payoff is deferred JS execution rather than a deferred fetch.",[594,3271,3272,3275,3276,3279],{},[651,3273,3274],{},"PendingTasks"," (§1.9) doesn't decide ",[603,3277,3278],{},"whether"," a route is SSR'd — it makes the routes you do\nSSR correctness-safe, by making serialization wait for async work that must complete first.",[1266,3281,3283],{"id":3282},"_19-change-detection-zoneless-is-the-default-now",[22,3284,103],{"title":625,"href":102},[594,3286,3287],{},"This is the biggest recent shift, and agents trained on older material get it wrong.",[3289,3290,3291],"blockquote",{},[594,3292,3293,3296,3297,3300,3301,3304,3305,3308,3309,3104,3312,3315],{},[597,3294,3295],{},"Angular 21+ runs zoneless by default."," In v20 you opt in with\n",[651,3298,3299],{},"provideZonelessChangeDetection()",". Zone.js should be removed from ",[651,3302,3303],{},"polyfills"," in\n",[651,3306,3307],{},"angular.json"," (both ",[651,3310,3311],{},"build",[651,3313,3314],{},"test",") and uninstalled.",[594,3317,3318],{},"Without Zone.js, Angular no longer discovers changes by monkey-patching browser APIs. It\nrelies on explicit notifications, of which the ones that matter are:",[658,3320,3321,3327,3336,3341],{},[18,3322,3323,3326],{},[597,3324,3325],{},"updating a signal that a template reads"," ← this is the mechanism you should be using",[18,3328,3329,3332,3333,3335],{},[651,3330,3331],{},"ChangeDetectorRef.markForCheck()"," (the ",[651,3334,2699],{}," pipe calls it for you)",[18,3337,3338],{},[651,3339,3340],{},"ComponentRef.setInput()",[18,3342,3343],{},"host and template listener callbacks",[594,3345,3346,3347,3350],{},"The architectural consequence is direct: ",[597,3348,3349],{},"signals stop being a style preference and become\nthe notification mechanism."," A component that mutates a plain field and expects the view to\nupdate worked under Zone.js and silently does not now.",[594,3352,3353],{},"Practical fallout:",[658,3355,3356,3367,3406,3425,3526],{},[18,3357,3358,3359,3362,3363,3366],{},"Every component runs under ",[651,3360,3361],{},"OnPush"," — the ",[597,3364,3365],{},"default since v22",", set explicitly on v20\u002Fv21.\nIt's the recommended path to zoneless-compatibility and it makes accidental reliance on\neager change detection impossible.",[18,3368,3369,2973,3372,2973,3375,3378,3379,3382,3383,3386,3387,3390,3391,3394,3395,3398,3399,3104,3402,3405],{},[651,3370,3371],{},"NgZone.onMicrotaskEmpty",[651,3373,3374],{},"onStable",[651,3376,3377],{},"onUnstable"," never emit; ",[651,3380,3381],{},"isStable"," is always\n",[651,3384,3385],{},"true",". Replace with ",[651,3388,3389],{},"afterNextRender"," (one pass) or ",[651,3392,3393],{},"afterEveryRender"," (spanning passes),\nor a direct DOM API like ",[651,3396,3397],{},"MutationObserver",". (",[651,3400,3401],{},"NgZone.run",[651,3403,3404],{},"runOutsideAngular"," remain\nfine.)",[18,3407,3408,2304,3411,679,3414,679,3417,3420,3421,3424],{},[597,3409,3410],{},"Reactive forms don't self-notify.",[651,3412,3413],{},"setValue",[651,3415,3416],{},"patchValue",[651,3418,3419],{},"FormArray.push"," update\nform state and emit on form observables but do not schedule change detection. If a template\ndepends on reactive-forms state, bridge it — ",[651,3422,3423],{},"markForCheck()",", or reflect the value through\na signal. This is a real, easy-to-miss bug source in migrated apps.",[18,3426,3427,3432,3433,3510,3513,3514,3517,3518,3521,3522,3525],{},[597,3428,3429,3430,1040],{},"SSR needs ",[651,3431,3274],{}," Serialization used to wait on the zone. Now async work that\nmust complete before serializing has to say so:",[644,3434,3436],{"className":831,"code":3435,"language":833,"meta":653,"style":653},"const tasks = inject(PendingTasks);\nawait tasks.run(async () => {\n  this.state.set(await loadCriticalData());\n});\n",[651,3437,3438,3455,3476,3501],{"__ignoreMap":653},[837,3439,3440,3443,3446,3448,3450,3453],{"class":839,"line":840},[837,3441,3442],{"class":897},"const",[837,3444,3445],{"class":1348}," tasks",[837,3447,950],{"class":924},[837,3449,2718],{"class":854},[837,3451,3452],{"class":937},"(PendingTasks)",[837,3454,941],{"class":862},[837,3456,3457,3459,3461,3463,3466,3468,3470,3472,3474],{"class":839,"line":847},[837,3458,2741],{"class":897},[837,3460,3445],{"class":937},[837,3462,1040],{"class":869},[837,3464,3465],{"class":854},"run",[837,3467,859],{"class":937},[837,3469,2699],{"class":897},[837,3471,1976],{"class":862},[837,3473,1114],{"class":924},[837,3475,914],{"class":862},[837,3477,3478,3481,3483,3486,3488,3490,3492,3494,3497,3499],{"class":839,"line":894},[837,3479,3480],{"class":1036},"  this",[837,3482,1040],{"class":869},[837,3484,3485],{"class":937},"state",[837,3487,1040],{"class":869},[837,3489,2649],{"class":854},[837,3491,859],{"class":937},[837,3493,2741],{"class":897},[837,3495,3496],{"class":854}," loadCriticalData",[837,3498,1384],{"class":937},[837,3500,941],{"class":862},[837,3502,3503,3506,3508],{"class":839,"line":917},[837,3504,3505],{"class":862},"}",[837,3507,1011],{"class":937},[837,3509,941],{"class":862},[3511,3512],"br",{},"The framework already does this for in-flight router navigations and ",[651,3515,3516],{},"HttpClient"," requests;\nit's your own async work that needs wiring. ",[651,3519,3520],{},"pendingUntilEvent()"," from\n",[651,3523,3524],{},"@angular\u002Fcore\u002Frxjs-interop"," does the same for an Observable.",[18,3527,3528,3529,3532],{},"In development, ",[651,3530,3531],{},"provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 })"," will\nperiodically verify that no binding changed without a notification. Turn it on while\nmigrating; it finds exactly the bugs described above.",[1266,3534,3536],{"id":3535},"part-1-rules",[22,3537,109],{"title":625,"href":108},[658,3539,3540,3549,3558,3567,3570,3576,3585,3591,3604,3611,3614],{},[18,3541,3542,3543,3545,3546,3548],{},"Bootstrap with ",[651,3544,1292],{},"; no ",[651,3547,1276],{}," in new code.",[18,3550,3551,3552,3554,3555,1040],{},"Expose library\u002Ffeature configuration via ",[651,3553,1484],{}," functions returning\n",[651,3556,3557],{},"EnvironmentProviders",[18,3559,3560,3561,3563,3564,3566],{},"Enable ",[651,3562,1589],{}," and bind route params as signal ",[651,3565,1594],{},"s.",[18,3568,3569],{},"Group by feature and colocate; dependencies point downward; features never import features.",[18,3571,3572,3573,3575],{},"Enforce boundaries with lint, not documentation; graduate to real Nx libraries (with\n",[651,3574,1854],{},") once folders and lint stop being enough.",[18,3577,3578,3579,3581,3582,3584],{},"Lazy-load every feature route. ",[651,3580,2299],{}," for existence, ",[651,3583,2303],{}," for permission.",[18,3586,3587,3588,3590],{},"Gate a feature flag at all three layers it affects (route, template, store) through one\n",[651,3589,2781],{},"; every flag ships with an owner and a removal condition.",[18,3592,3593,3594,3597,3598,3600,3601,3603],{},"Use ",[651,3595,3596],{},"@defer (on viewport)"," for heavy below-the-fold content, with ",[651,3599,3025],{}," and\n",[651,3602,3029],{}," timings.",[18,3605,3606,3607,3610],{},"Pick a translation strategy (compile-time vs runtime) before the first component ships;\nformat dates\u002Fnumbers\u002Fcurrency through ",[651,3608,3609],{},"Intl","\u002Fpipes, never string templates.",[18,3612,3613],{},"Decide SSR per route, not per app; avoid reading browser globals during render to prevent\nhydration mismatches.",[18,3615,3616,3617,3619,3620,3622],{},"Assume zoneless: drive the view with signals, ",[651,3618,3361],{}," everywhere, ",[651,3621,3274],{}," for SSR\nasync work.",[617,3624],{},[620,3626,3628],{"id":3627},"part-2-the-three-layers",[22,3629,115],{"title":625,"href":114},[1266,3631,3633],{"id":3632},"_20-the-model-in-one-diagram",[22,3634,123],{"title":625,"href":122},[644,3636,3639],{"className":3637,"code":3638,"language":649},[647],"┌─────────────────────────────────────────────────────────────┐\n│  Component  — view only                                     │\n│  · reads signals from the store                             │\n│  · derives with computed \u002F linkedSignal                     │\n│  · calls store methods on user interaction                  │\n│  · NEVER subscribes, NEVER holds fetched state              │\n└──────────────────────────┬──────────────────────────────────┘\n                           │ calls methods, reads signals\n┌──────────────────────────▼──────────────────────────────────┐\n│  Store  — state + orchestration                             │\n│  · owns all feature state                                   │\n│  · owns loading \u002F error state                               │\n│  · owns fetch, poll, retry, cancel, debounce                │\n│  · injects services, never HttpClient directly              │\n└──────────────────────────┬──────────────────────────────────┘\n                           │ calls, receives Observable\n┌──────────────────────────▼──────────────────────────────────┐\n│  Service  — transport only                                  │\n│  · one method per endpoint \u002F query                          │\n│  · maps wire shape → domain model                           │\n│  · returns a cold Observable                                │\n│  · holds NO state, sets NO flags                            │\n└─────────────────────────────────────────────────────────────┘\n",[651,3640,3638],{"__ignoreMap":653},[594,3642,3643],{},"The single most important rule in the whole document:",[3289,3645,3646],{},[594,3647,3648,3651,3652,3655],{},[597,3649,3650],{},"A component may not call a service that performs I\u002FO.","\nData flows ",[651,3653,3654],{},"Component → Store → Service",". No shortcuts.",[594,3657,3658],{},"Everything else in this part follows from that.",[594,3660,3661,3662,3665,3666,3668,3669,3672,3673,3675],{},"There is exactly one sanctioned exception, and it is narrow: a component whose ",[603,3663,3664],{},"entire"," data\nstory is one read-only fetch may use ",[651,3667,668],{}," and skip the store. That case, and the\nprecise point at which it stops applying, gets its own section\n(",[22,3670,3671],{"href":205},"§2.4",") — but read the three layers first,\nbecause the store is the default and ",[651,3674,668],{}," is the thing you justify.",[1266,3677,3679],{"id":3678},"_21-services-are-boring-on-purpose",[22,3680,129],{"title":625,"href":128},[594,3682,3683,3684,3687],{},"A service that talks to the network is a ",[597,3685,3686],{},"transport adapter",". Its entire job is to turn a\ntyped request into a typed response. It should be so dull that reading it tells you nothing\nabout the feature.",[644,3689,3691],{"className":831,"code":3690,"language":833,"meta":653,"style":653},"\u002F\u002F carriers-api.service.ts\n@Injectable({ providedIn: 'root' })\nexport class CarriersApiService {\n  private readonly http = inject(HttpClient);\n\n  \u002F** Ranked server-side; every field the view needs comes back on the row. *\u002F\n  loadTopCarriers(limit: number, range: DateRange): Observable\u003CCarrier[]> {\n    return this.http\n      .get\u003C{ carriers: CarrierDto[] }>('\u002Fapi\u002Fcarriers\u002Ftop', {\n        params: { limit, from: range.from, to: range.to },\n      })\n      .pipe(map((response) => response.carriers.map(toCarrier)));\n  }\n}\n\n\u002F** Wire shape → domain model. Keeps DTO leakage out of the store and view. *\u002F\nfunction toCarrier(dto: CarrierDto): Carrier {\n  return { id: dto.id, name: dto.name, shipmentCount: dto.shipment_count, risk: dto.risk_level };\n}\n",[651,3692,3693,3698,3718,3729,3746,3750,3755,3797,3809,3843,3885,3892,3931,3935,3939,3943,3948,3973,4037],{"__ignoreMap":653},[837,3694,3695],{"class":839,"line":840},[837,3696,3697],{"class":843},"\u002F\u002F carriers-api.service.ts\n",[837,3699,3700,3702,3704,3706,3708,3710,3712,3714,3716],{"class":839,"line":847},[837,3701,851],{"class":850},[837,3703,2491],{"class":854},[837,3705,859],{"class":858},[837,3707,863],{"class":862},[837,3709,2498],{"class":858},[837,3711,870],{"class":869},[837,3713,2503],{"class":873},[837,3715,888],{"class":862},[837,3717,891],{"class":858},[837,3719,3720,3722,3724,3727],{"class":839,"line":894},[837,3721,898],{"class":897},[837,3723,901],{"class":897},[837,3725,3726],{"class":904}," CarriersApiService",[837,3728,914],{"class":862},[837,3730,3731,3733,3735,3737,3739,3741,3744],{"class":839,"line":917},[837,3732,2523],{"class":897},[837,3734,2526],{"class":897},[837,3736,1003],{"class":920},[837,3738,950],{"class":924},[837,3740,2718],{"class":854},[837,3742,3743],{"class":937},"(HttpClient)",[837,3745,941],{"class":862},[837,3747,3748],{"class":839,"line":944},[837,3749,988],{"emptyLinePlaceholder":987},[837,3751,3752],{"class":839,"line":959},[837,3753,3754],{"class":843},"  \u002F** Ranked server-side; every field the view needs comes back on the row. *\u002F\n",[837,3756,3757,3760,3762,3765,3767,3770,3772,3775,3777,3780,3782,3784,3787,3789,3791,3793,3795],{"class":839,"line":984},[837,3758,3759],{"class":854},"  loadTopCarriers",[837,3761,859],{"class":862},[837,3763,3764],{"class":1002},"limit",[837,3766,870],{"class":924},[837,3768,3769],{"class":967}," number",[837,3771,877],{"class":862},[837,3773,3774],{"class":1002}," range",[837,3776,870],{"class":924},[837,3778,3779],{"class":904}," DateRange",[837,3781,1011],{"class":862},[837,3783,870],{"class":924},[837,3785,3786],{"class":904}," Observable",[837,3788,1069],{"class":1068},[837,3790,1072],{"class":904},[837,3792,1075],{"class":930},[837,3794,1078],{"class":1068},[837,3796,914],{"class":862},[837,3798,3799,3802,3804,3806],{"class":839,"line":991},[837,3800,3801],{"class":897},"    return",[837,3803,1120],{"class":1036},[837,3805,1040],{"class":869},[837,3807,3808],{"class":937},"http\n",[837,3810,3811,3814,3816,3818,3820,3823,3825,3828,3830,3832,3834,3836,3839,3841],{"class":839,"line":1017},[837,3812,3813],{"class":869},"      .",[837,3815,1065],{"class":854},[837,3817,1069],{"class":1068},[837,3819,863],{"class":862},[837,3821,1130],{"class":3822},"sv8vG",[837,3824,870],{"class":924},[837,3826,3827],{"class":904}," CarrierDto",[837,3829,931],{"class":930},[837,3831,3505],{"class":862},[837,3833,1078],{"class":1068},[837,3835,859],{"class":937},[837,3837,3838],{"class":873},"'\u002Fapi\u002Fcarriers\u002Ftop'",[837,3840,877],{"class":862},[837,3842,914],{"class":862},[837,3844,3845,3848,3850,3852,3855,3857,3860,3862,3864,3866,3869,3871,3874,3876,3878,3880,3883],{"class":839,"line":1022},[837,3846,3847],{"class":937},"        params",[837,3849,870],{"class":869},[837,3851,1117],{"class":862},[837,3853,3854],{"class":937}," limit",[837,3856,877],{"class":862},[837,3858,3859],{"class":937}," from",[837,3861,870],{"class":869},[837,3863,3774],{"class":937},[837,3865,1040],{"class":869},[837,3867,3868],{"class":937},"from",[837,3870,877],{"class":862},[837,3872,3873],{"class":937}," to",[837,3875,870],{"class":869},[837,3877,3774],{"class":937},[837,3879,1040],{"class":869},[837,3881,3882],{"class":937},"to ",[837,3884,1564],{"class":862},[837,3886,3887,3890],{"class":839,"line":1033},[837,3888,3889],{"class":862},"      }",[837,3891,891],{"class":937},[837,3893,3894,3896,3899,3901,3904,3906,3908,3911,3913,3915,3918,3920,3922,3924,3926,3929],{"class":839,"line":1053},[837,3895,3813],{"class":869},[837,3897,3898],{"class":854},"pipe",[837,3900,859],{"class":937},[837,3902,3903],{"class":854},"map",[837,3905,859],{"class":937},[837,3907,859],{"class":862},[837,3909,3910],{"class":1002},"response",[837,3912,1011],{"class":862},[837,3914,1114],{"class":924},[837,3916,3917],{"class":937}," response",[837,3919,1040],{"class":869},[837,3921,1109],{"class":937},[837,3923,1040],{"class":869},[837,3925,3903],{"class":854},[837,3927,3928],{"class":937},"(toCarrier)))",[837,3930,941],{"class":862},[837,3932,3933],{"class":839,"line":1098},[837,3934,1216],{"class":862},[837,3936,3937],{"class":839,"line":1151},[837,3938,1222],{"class":862},[837,3940,3941],{"class":839,"line":1203},[837,3942,988],{"emptyLinePlaceholder":987},[837,3944,3945],{"class":839,"line":1213},[837,3946,3947],{"class":843},"\u002F** Wire shape → domain model. Keeps DTO leakage out of the store and view. *\u002F\n",[837,3949,3950,3953,3956,3958,3961,3963,3965,3967,3969,3971],{"class":839,"line":1219},[837,3951,3952],{"class":897},"function",[837,3954,3955],{"class":854}," toCarrier",[837,3957,859],{"class":862},[837,3959,3960],{"class":1002},"dto",[837,3962,870],{"class":924},[837,3964,3827],{"class":904},[837,3966,1011],{"class":862},[837,3968,870],{"class":924},[837,3970,927],{"class":904},[837,3972,914],{"class":862},[837,3974,3976,3978,3980,3983,3985,3988,3990,3993,3995,3998,4000,4002,4004,4007,4009,4012,4014,4016,4018,4021,4023,4026,4028,4030,4032,4035],{"class":839,"line":3975},18,[837,3977,1532],{"class":897},[837,3979,1117],{"class":862},[837,3981,3982],{"class":937}," id",[837,3984,870],{"class":869},[837,3986,3987],{"class":937}," dto",[837,3989,1040],{"class":869},[837,3991,3992],{"class":937},"id",[837,3994,877],{"class":862},[837,3996,3997],{"class":937}," name",[837,3999,870],{"class":869},[837,4001,3987],{"class":937},[837,4003,1040],{"class":869},[837,4005,4006],{"class":937},"name",[837,4008,877],{"class":862},[837,4010,4011],{"class":937}," shipmentCount",[837,4013,870],{"class":869},[837,4015,3987],{"class":937},[837,4017,1040],{"class":869},[837,4019,4020],{"class":937},"shipment_count",[837,4022,877],{"class":862},[837,4024,4025],{"class":937}," risk",[837,4027,870],{"class":869},[837,4029,3987],{"class":937},[837,4031,1040],{"class":869},[837,4033,4034],{"class":937},"risk_level ",[837,4036,1473],{"class":862},[837,4038,4040],{"class":839,"line":4039},19,[837,4041,1222],{"class":862},[1645,4043,4044,4054],{},[1648,4045,4046],{},[1651,4047,4048,4051],{},[1654,4049,4050],{},"Rule",[1654,4052,4053],{},"Why",[1664,4055,4056,4072,4084,4092,4106,4114,4125,4140],{},[1651,4057,4058,4069],{},[1669,4059,4060,4061,4064,4065,4068],{},"Return a cold ",[651,4062,4063],{},"Observable"," (or ",[651,4066,4067],{},"Promise","), never a signal",[1669,4070,4071],{},"The caller decides subscription semantics — cancellation is the store's business",[1651,4073,4074,4081],{},[1669,4075,4076,4077,4080],{},"No ",[651,4078,4079],{},"BehaviorSubject"," state, no cached last value",[1669,4082,4083],{},"Two sources of truth is how staleness bugs are born",[1651,4085,4086,4089],{},[1669,4087,4088],{},"No loading\u002Ferror flags",[1669,4090,4091],{},"The store owns call state; a service that sets flags has hidden coupling",[1651,4093,4094,4097],{},[1669,4095,4096],{},"One method per operation, named for the operation",[1669,4098,4099,4102,4103],{},[651,4100,4101],{},"loadTopCarriers",", not ",[651,4104,4105],{},"getData",[1651,4107,4108,4111],{},[1669,4109,4110],{},"Map DTO → domain at the boundary",[1669,4112,4113],{},"External data is untrusted; narrow the type once, here",[1651,4115,4116,4122],{},[1669,4117,4118,4121],{},[651,4119,4120],{},"providedIn: 'root'"," unless it genuinely needs scoping",[1669,4123,4124],{},"Tree-shakable, no provider boilerplate",[1651,4126,4127,4130],{},[1669,4128,4129],{},"Design around a single responsibility",[1669,4131,4132,4133,4136,4137,4139],{},"A service that fetches carriers ",[603,4134,4135],{},"and"," formats them ",[603,4138,4135],{}," caches them is three services",[1651,4141,4142,4149],{},[1669,4143,4144,4145,4148],{},"Name the file for its role — ",[651,4146,4147],{},"*-api.service.ts"," for I\u002FO services",[1669,4150,4151],{},"Makes the \"components may not inject these\" rule mechanically checkable",[594,4153,4154,4155,4165],{},"That last row is worth dwelling on. If your I\u002FO services are identifiable by filename, the\ncentral architectural rule stops being a code-review convention and becomes an ESLint rule:\n",[603,4156,4157,4158,4161,4162],{},"no ",[651,4159,4160],{},"*-api.service"," may be injected into a ",[651,4163,4164],{},"*.component.ts",". Rules an agent can violate\nsilently are rules you don't have. Rules CI enforces are rules you have.",[594,4167,4168,4171],{},[597,4169,4170],{},"Not every service is an API service."," A pure formatter, a date-math helper, a permissions\ncalculator — these hold no state and perform no I\u002FO, and components may inject them freely.\nThe prohibition is specifically about I\u002FO and state, not about the word \"service.\"",[594,4173,4174,4177],{},[597,4175,4176],{},"Cross-cutting HTTP concerns belong in interceptors, not services."," Auth headers, retry\npolicy, error normalisation, correlation IDs, loading telemetry — write a functional\ninterceptor once and register it at bootstrap. Every API service re-implementing retry is the\nduplication this exists to prevent.",[644,4179,4181],{"className":831,"code":4180,"language":833,"meta":653,"style":653},"export const authInterceptor: HttpInterceptorFn = (req, next) => {\n  const token = inject(AuthStore).accessToken();\n  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);\n};\n",[651,4182,4183,4215,4239,4303],{"__ignoreMap":653},[837,4184,4185,4187,4189,4192,4194,4197,4199,4201,4204,4206,4209,4211,4213],{"class":839,"line":840},[837,4186,898],{"class":897},[837,4188,1345],{"class":897},[837,4190,4191],{"class":854}," authInterceptor",[837,4193,870],{"class":924},[837,4195,4196],{"class":904}," HttpInterceptorFn",[837,4198,950],{"class":924},[837,4200,1106],{"class":862},[837,4202,4203],{"class":1002},"req",[837,4205,877],{"class":862},[837,4207,4208],{"class":1002}," next",[837,4210,1011],{"class":862},[837,4212,1114],{"class":924},[837,4214,914],{"class":862},[837,4216,4217,4220,4223,4225,4227,4230,4232,4235,4237],{"class":839,"line":847},[837,4218,4219],{"class":897},"  const",[837,4221,4222],{"class":1348}," token",[837,4224,950],{"class":924},[837,4226,2718],{"class":854},[837,4228,4229],{"class":937},"(AuthStore)",[837,4231,1040],{"class":869},[837,4233,4234],{"class":854},"accessToken",[837,4236,1028],{"class":937},[837,4238,941],{"class":862},[837,4240,4241,4243,4245,4248,4251,4254,4256,4259,4261,4263,4266,4268,4270,4273,4275,4278,4282,4285,4287,4290,4292,4294,4296,4298,4301],{"class":839,"line":894},[837,4242,1532],{"class":897},[837,4244,4208],{"class":854},[837,4246,4247],{"class":937},"(token ",[837,4249,4250],{"class":924},"?",[837,4252,4253],{"class":937}," req",[837,4255,1040],{"class":869},[837,4257,4258],{"class":854},"clone",[837,4260,859],{"class":937},[837,4262,863],{"class":862},[837,4264,4265],{"class":937}," setHeaders",[837,4267,870],{"class":869},[837,4269,1117],{"class":862},[837,4271,4272],{"class":937}," Authorization",[837,4274,870],{"class":869},[837,4276,4277],{"class":873}," `Bearer ",[837,4279,4281],{"class":4280},"s5eaX","${",[837,4283,4284],{"class":937},"token",[837,4286,3505],{"class":4280},[837,4288,4289],{"class":873},"`",[837,4291,888],{"class":862},[837,4293,888],{"class":862},[837,4295,2114],{"class":937},[837,4297,870],{"class":924},[837,4299,4300],{"class":937}," req)",[837,4302,941],{"class":862},[837,4304,4305],{"class":839,"line":917},[837,4306,1473],{"class":862},[1266,4308,4310],{"id":4309},"_22-the-store-is-where-the-feature-actually-lives",[22,4311,135],{"title":625,"href":134},[594,4313,4314,4315,679,4317,1773,4320,4323],{},"The store owns three things: ",[597,4316,3485],{},[597,4318,4319],{},"derivations",[597,4321,4322],{},"orchestration",". If you can\nname a piece of feature behaviour, it belongs here.",[4325,4326,4328],"h4",{"id":4327},"anatomy",[22,4329,146],{"title":625,"href":145},[594,4331,4332],{},"Compose store features in a fixed order. Fixed order means every store in the codebase is\nskimmable in five seconds, and an agent has no decisions to make.",[644,4334,4336],{"className":831,"code":4335,"language":833,"meta":653,"style":653},"\u002F\u002F carriers.store.ts\n\n\u002F\u002F ─── Constants ────────────────────────────────────────────────\nconst TOP_CARRIER_COUNT = 5;\n\n\u002F\u002F ─── State ────────────────────────────────────────────────────\ninterface CarriersState {\n  carriers: Carrier[];\n  selectedId: string | null;\n}\n\nconst initialState: CarriersState = {\n  carriers: [],\n  selectedId: null,\n};\n\n\u002F\u002F ─── Store ────────────────────────────────────────────────────\nexport const CarriersStore = signalStore(\n  withState\u003CCarriersState>(initialState),   \u002F\u002F 1. state first, always\n  withCallState(),                          \u002F\u002F 2. loading\u002Ferror, if async\n  withEntities({ ... }),                    \u002F\u002F 3. only if you need a normalized collection\n  withComputed((store) => ({ ... })),       \u002F\u002F 4. exactly one block\n  withMethods((store) => ({ ... })),        \u002F\u002F 5. exactly one block\n  withHooks({ ... }),                       \u002F\u002F 6. lifecycle, optional\n  withLogger('CarriersStore'),              \u002F\u002F 7. last, always, named\n);\n",[651,4337,4338,4343,4347,4352,4366,4370,4375,4385,4397,4412,4416,4420,4435,4445,4455,4459,4463,4468,4485,4505,4518,4540,4573,4604,4625,4643],{"__ignoreMap":653},[837,4339,4340],{"class":839,"line":840},[837,4341,4342],{"class":843},"\u002F\u002F carriers.store.ts\n",[837,4344,4345],{"class":839,"line":847},[837,4346,988],{"emptyLinePlaceholder":987},[837,4348,4349],{"class":839,"line":894},[837,4350,4351],{"class":843},"\u002F\u002F ─── Constants ────────────────────────────────────────────────\n",[837,4353,4354,4356,4359,4361,4364],{"class":839,"line":917},[837,4355,3442],{"class":897},[837,4357,4358],{"class":1348}," TOP_CARRIER_COUNT",[837,4360,950],{"class":924},[837,4362,4363],{"class":953}," 5",[837,4365,941],{"class":862},[837,4367,4368],{"class":839,"line":944},[837,4369,988],{"emptyLinePlaceholder":987},[837,4371,4372],{"class":839,"line":959},[837,4373,4374],{"class":843},"\u002F\u002F ─── State ────────────────────────────────────────────────────\n",[837,4376,4377,4380,4383],{"class":839,"line":984},[837,4378,4379],{"class":897},"interface",[837,4381,4382],{"class":904}," CarriersState",[837,4384,914],{"class":862},[837,4386,4387,4389,4391,4393,4395],{"class":839,"line":991},[837,4388,921],{"class":920},[837,4390,870],{"class":924},[837,4392,927],{"class":904},[837,4394,1075],{"class":930},[837,4396,941],{"class":862},[837,4398,4399,4402,4404,4406,4408,4410],{"class":839,"line":1017},[837,4400,4401],{"class":920},"  selectedId",[837,4403,870],{"class":924},[837,4405,968],{"class":967},[837,4407,971],{"class":924},[837,4409,975],{"class":974},[837,4411,941],{"class":862},[837,4413,4414],{"class":839,"line":1022},[837,4415,1222],{"class":862},[837,4417,4418],{"class":839,"line":1033},[837,4419,988],{"emptyLinePlaceholder":987},[837,4421,4422,4424,4427,4429,4431,4433],{"class":839,"line":1053},[837,4423,3442],{"class":897},[837,4425,4426],{"class":1348}," initialState",[837,4428,870],{"class":924},[837,4430,4382],{"class":904},[837,4432,950],{"class":924},[837,4434,914],{"class":862},[837,4436,4437,4439,4441,4443],{"class":839,"line":1098},[837,4438,921],{"class":937},[837,4440,870],{"class":869},[837,4442,938],{"class":937},[837,4444,1387],{"class":862},[837,4446,4447,4449,4451,4453],{"class":839,"line":1151},[837,4448,4401],{"class":937},[837,4450,870],{"class":869},[837,4452,975],{"class":967},[837,4454,1387],{"class":862},[837,4456,4457],{"class":839,"line":1203},[837,4458,1473],{"class":862},[837,4460,4461],{"class":839,"line":1213},[837,4462,988],{"emptyLinePlaceholder":987},[837,4464,4465],{"class":839,"line":1219},[837,4466,4467],{"class":843},"\u002F\u002F ─── Store ────────────────────────────────────────────────────\n",[837,4469,4470,4472,4474,4477,4479,4482],{"class":839,"line":3975},[837,4471,898],{"class":897},[837,4473,1345],{"class":897},[837,4475,4476],{"class":1348}," CarriersStore",[837,4478,950],{"class":924},[837,4480,4481],{"class":854}," signalStore",[837,4483,4484],{"class":937},"(\n",[837,4486,4487,4490,4492,4495,4497,4500,4502],{"class":839,"line":4039},[837,4488,4489],{"class":854},"  withState",[837,4491,1069],{"class":1068},[837,4493,4494],{"class":904},"CarriersState",[837,4496,1078],{"class":1068},[837,4498,4499],{"class":937},"(initialState)",[837,4501,877],{"class":862},[837,4503,4504],{"class":843},"   \u002F\u002F 1. state first, always\n",[837,4506,4508,4511,4513,4515],{"class":839,"line":4507},20,[837,4509,4510],{"class":854},"  withCallState",[837,4512,1028],{"class":937},[837,4514,877],{"class":862},[837,4516,4517],{"class":843},"                          \u002F\u002F 2. loading\u002Ferror, if async\n",[837,4519,4521,4524,4526,4528,4531,4533,4535,4537],{"class":839,"line":4520},21,[837,4522,4523],{"class":854},"  withEntities",[837,4525,859],{"class":937},[837,4527,863],{"class":862},[837,4529,4530],{"class":924}," ...",[837,4532,888],{"class":862},[837,4534,1011],{"class":937},[837,4536,877],{"class":862},[837,4538,4539],{"class":843},"                    \u002F\u002F 3. only if you need a normalized collection\n",[837,4541,4543,4546,4548,4550,4553,4555,4557,4559,4561,4563,4565,4568,4570],{"class":839,"line":4542},22,[837,4544,4545],{"class":854},"  withComputed",[837,4547,859],{"class":937},[837,4549,859],{"class":862},[837,4551,4552],{"class":1002},"store",[837,4554,1011],{"class":862},[837,4556,1114],{"class":924},[837,4558,1106],{"class":937},[837,4560,863],{"class":862},[837,4562,4530],{"class":924},[837,4564,888],{"class":862},[837,4566,4567],{"class":937},"))",[837,4569,877],{"class":862},[837,4571,4572],{"class":843},"       \u002F\u002F 4. exactly one block\n",[837,4574,4576,4579,4581,4583,4585,4587,4589,4591,4593,4595,4597,4599,4601],{"class":839,"line":4575},23,[837,4577,4578],{"class":854},"  withMethods",[837,4580,859],{"class":937},[837,4582,859],{"class":862},[837,4584,4552],{"class":1002},[837,4586,1011],{"class":862},[837,4588,1114],{"class":924},[837,4590,1106],{"class":937},[837,4592,863],{"class":862},[837,4594,4530],{"class":924},[837,4596,888],{"class":862},[837,4598,4567],{"class":937},[837,4600,877],{"class":862},[837,4602,4603],{"class":843},"        \u002F\u002F 5. exactly one block\n",[837,4605,4607,4610,4612,4614,4616,4618,4620,4622],{"class":839,"line":4606},24,[837,4608,4609],{"class":854},"  withHooks",[837,4611,859],{"class":937},[837,4613,863],{"class":862},[837,4615,4530],{"class":924},[837,4617,888],{"class":862},[837,4619,1011],{"class":937},[837,4621,877],{"class":862},[837,4623,4624],{"class":843},"                       \u002F\u002F 6. lifecycle, optional\n",[837,4626,4628,4631,4633,4636,4638,4640],{"class":839,"line":4627},25,[837,4629,4630],{"class":854},"  withLogger",[837,4632,859],{"class":937},[837,4634,4635],{"class":873},"'CarriersStore'",[837,4637,1011],{"class":937},[837,4639,877],{"class":862},[837,4641,4642],{"class":843},"              \u002F\u002F 7. last, always, named\n",[837,4644,4646,4648],{"class":839,"line":4645},26,[837,4647,1011],{"class":937},[837,4649,941],{"class":862},[594,4651,4652,4661],{},[597,4653,4654,4655,4658,4659,1040],{},"One ",[651,4656,4657],{},"withComputed",", one ",[651,4660,2785],{}," Splitting them into multiple blocks creates\nordering dependencies between blocks that are invisible at the call site and break in\nconfusing ways when someone reorders them. If a block gets long, that's a signal to extract\npure functions — not to add another block.",[4325,4663,4665],{"id":4664},"state-design-flat-and-free-of-derivations",[22,4666,152],{"title":625,"href":151},[644,4668,4670],{"className":831,"code":4669,"language":833,"meta":653,"style":653},"\u002F\u002F ✅ flat — one patch updates one thing\ninterface CarriersState {\n  carriers: Carrier[];\n  selectedId: string | null;\n  isPanelOpen: boolean;\n}\n\n\u002F\u002F ❌ nested — every update needs a spread of a spread\ninterface CarriersState {\n  ui: { selection: { id: string | null; open: boolean } };\n}\n",[651,4671,4672,4677,4685,4697,4711,4722,4726,4730,4735,4743,4783],{"__ignoreMap":653},[837,4673,4674],{"class":839,"line":840},[837,4675,4676],{"class":843},"\u002F\u002F ✅ flat — one patch updates one thing\n",[837,4678,4679,4681,4683],{"class":839,"line":847},[837,4680,4379],{"class":897},[837,4682,4382],{"class":904},[837,4684,914],{"class":862},[837,4686,4687,4689,4691,4693,4695],{"class":839,"line":894},[837,4688,921],{"class":920},[837,4690,870],{"class":924},[837,4692,927],{"class":904},[837,4694,1075],{"class":930},[837,4696,941],{"class":862},[837,4698,4699,4701,4703,4705,4707,4709],{"class":839,"line":917},[837,4700,4401],{"class":920},[837,4702,870],{"class":924},[837,4704,968],{"class":967},[837,4706,971],{"class":924},[837,4708,975],{"class":974},[837,4710,941],{"class":862},[837,4712,4713,4716,4718,4720],{"class":839,"line":944},[837,4714,4715],{"class":920},"  isPanelOpen",[837,4717,870],{"class":924},[837,4719,2548],{"class":967},[837,4721,941],{"class":862},[837,4723,4724],{"class":839,"line":959},[837,4725,1222],{"class":862},[837,4727,4728],{"class":839,"line":984},[837,4729,988],{"emptyLinePlaceholder":987},[837,4731,4732],{"class":839,"line":991},[837,4733,4734],{"class":843},"\u002F\u002F ❌ nested — every update needs a spread of a spread\n",[837,4736,4737,4739,4741],{"class":839,"line":1017},[837,4738,4379],{"class":897},[837,4740,4382],{"class":904},[837,4742,914],{"class":862},[837,4744,4745,4748,4750,4752,4755,4757,4759,4761,4763,4765,4767,4769,4771,4774,4776,4778,4780],{"class":839,"line":1022},[837,4746,4747],{"class":920},"  ui",[837,4749,870],{"class":924},[837,4751,1117],{"class":862},[837,4753,4754],{"class":3822}," selection",[837,4756,870],{"class":924},[837,4758,1117],{"class":862},[837,4760,3982],{"class":3822},[837,4762,870],{"class":924},[837,4764,968],{"class":967},[837,4766,971],{"class":924},[837,4768,975],{"class":974},[837,4770,1133],{"class":862},[837,4772,4773],{"class":3822}," open",[837,4775,870],{"class":924},[837,4777,2548],{"class":967},[837,4779,888],{"class":862},[837,4781,4782],{"class":862}," };\n",[837,4784,4785],{"class":839,"line":1033},[837,4786,1222],{"class":862},[594,4788,4789],{},"And the rule that prevents the most bugs per character:",[644,4791,4793],{"className":831,"code":4792,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ derived values in state — two sources of truth, guaranteed to drift\ninterface CarriersState {\n  carriers: Carrier[];\n  hasCarriers: boolean;       \u002F\u002F ← who updates this? every writer? forever?\n  selectedCarrier: Carrier;   \u002F\u002F ← stale the moment carriers changes\n}\n\n\u002F\u002F ✅ derived values in computed — structurally impossible to be stale\nwithComputed((store) => ({\n  hasCarriers: computed(() => store.carriers().length > 0),\n  selectedCarrier: computed(() =>\n    store.carriers().find((carrier) => carrier.id === store.selectedId())\n  ),\n}));\n",[651,4794,4795,4800,4808,4820,4834,4848,4852,4856,4861,4879,4918,4933,4981,4988],{"__ignoreMap":653},[837,4796,4797],{"class":839,"line":840},[837,4798,4799],{"class":843},"\u002F\u002F ❌ derived values in state — two sources of truth, guaranteed to drift\n",[837,4801,4802,4804,4806],{"class":839,"line":847},[837,4803,4379],{"class":897},[837,4805,4382],{"class":904},[837,4807,914],{"class":862},[837,4809,4810,4812,4814,4816,4818],{"class":839,"line":894},[837,4811,921],{"class":920},[837,4813,870],{"class":924},[837,4815,927],{"class":904},[837,4817,1075],{"class":930},[837,4819,941],{"class":862},[837,4821,4822,4825,4827,4829,4831],{"class":839,"line":917},[837,4823,4824],{"class":920},"  hasCarriers",[837,4826,870],{"class":924},[837,4828,2548],{"class":967},[837,4830,1133],{"class":862},[837,4832,4833],{"class":843},"       \u002F\u002F ← who updates this? every writer? forever?\n",[837,4835,4836,4839,4841,4843,4845],{"class":839,"line":944},[837,4837,4838],{"class":920},"  selectedCarrier",[837,4840,870],{"class":924},[837,4842,927],{"class":904},[837,4844,1133],{"class":862},[837,4846,4847],{"class":843},"   \u002F\u002F ← stale the moment carriers changes\n",[837,4849,4850],{"class":839,"line":959},[837,4851,1222],{"class":862},[837,4853,4854],{"class":839,"line":984},[837,4855,988],{"emptyLinePlaceholder":987},[837,4857,4858],{"class":839,"line":991},[837,4859,4860],{"class":843},"\u002F\u002F ✅ derived values in computed — structurally impossible to be stale\n",[837,4862,4863,4865,4867,4869,4871,4873,4875,4877],{"class":839,"line":1017},[837,4864,4657],{"class":854},[837,4866,859],{"class":937},[837,4868,859],{"class":862},[837,4870,4552],{"class":1002},[837,4872,1011],{"class":862},[837,4874,1114],{"class":924},[837,4876,1106],{"class":937},[837,4878,1095],{"class":862},[837,4880,4881,4883,4885,4888,4890,4892,4894,4897,4899,4901,4903,4905,4908,4911,4914,4916],{"class":839,"line":1022},[837,4882,4824],{"class":937},[837,4884,870],{"class":869},[837,4886,4887],{"class":854}," computed",[837,4889,859],{"class":937},[837,4891,1028],{"class":862},[837,4893,1114],{"class":924},[837,4895,4896],{"class":937}," store",[837,4898,1040],{"class":869},[837,4900,1109],{"class":854},[837,4902,1028],{"class":937},[837,4904,1040],{"class":869},[837,4906,4907],{"class":1348},"length",[837,4909,4910],{"class":924}," >",[837,4912,4913],{"class":953}," 0",[837,4915,1011],{"class":937},[837,4917,1387],{"class":862},[837,4919,4920,4922,4924,4926,4928,4930],{"class":839,"line":1033},[837,4921,4838],{"class":937},[837,4923,870],{"class":869},[837,4925,4887],{"class":854},[837,4927,859],{"class":937},[837,4929,1028],{"class":862},[837,4931,4932],{"class":924}," =>\n",[837,4934,4935,4938,4940,4942,4944,4946,4949,4951,4953,4956,4958,4960,4963,4965,4968,4971,4973,4975,4978],{"class":839,"line":1053},[837,4936,4937],{"class":937},"    store",[837,4939,1040],{"class":869},[837,4941,1109],{"class":854},[837,4943,1028],{"class":937},[837,4945,1040],{"class":869},[837,4947,4948],{"class":854},"find",[837,4950,859],{"class":937},[837,4952,859],{"class":862},[837,4954,4955],{"class":1002},"carrier",[837,4957,1011],{"class":862},[837,4959,1114],{"class":924},[837,4961,4962],{"class":937}," carrier",[837,4964,1040],{"class":869},[837,4966,4967],{"class":937},"id ",[837,4969,4970],{"class":924},"===",[837,4972,4896],{"class":937},[837,4974,1040],{"class":869},[837,4976,4977],{"class":854},"selectedId",[837,4979,4980],{"class":937},"())\n",[837,4982,4983,4986],{"class":839,"line":1098},[837,4984,4985],{"class":937},"  )",[837,4987,1387],{"class":862},[837,4989,4990,4992,4994],{"class":839,"line":1151},[837,4991,3505],{"class":862},[837,4993,4567],{"class":937},[837,4995,941],{"class":862},[594,4997,4998,4999,5002,5003,5006],{},"There's a corollary agents get wrong constantly: ",[597,5000,5001],{},"don't park transient values in state."," If\na value is only needed by one updater, pass it as a method parameter. A search term that only\n",[651,5004,5005],{},"search()"," reads is an argument, not state. State is what the view observes.",[4325,5008,5010],{"id":5009},"keeping-shared-entities-consistent-across-stores",[22,5011,158],{"title":625,"href":157},[594,5013,5014,5015,3104,5018,5021,5022,5024],{},"Component-scoped stores (the recommended default — §2.2's \"Scoping\") create a problem that\nonly shows up at scale: if ",[651,5016,5017],{},"CarriersStore",[651,5019,5020],{},"ShipmentsStore"," both hold their own\ndenormalized copy of the same ",[651,5023,1072],{},", updating one leaves the other stale. This is\ninvisible in a small app — one feature, one store, no duplication — and becomes the most\ncommon data-integrity bug in a large one, because nothing in the type system catches it.",[594,5026,5027,5030,5031,5033],{},[597,5028,5029],{},"Default: one entity store per domain concept owns the canonical copy."," Feature stores hold\nIDs and derive the object from the shared store via ",[651,5032,682],{},", rather than duplicating it:",[644,5035,5037],{"className":831,"code":5036,"language":833,"meta":653,"style":653},"\u002F\u002F carrier-entities.store.ts — root-provided, the single source of truth for Carrier records\nexport const CarrierEntitiesStore = signalStore(\n  { providedIn: 'root' },\n  withEntities\u003CCarrier>(),   \u002F\u002F normalized: entityMap keyed by id, via a selectId you configure\n  withMethods((store, api = inject(CarriersApiService)) => ({\n    loadMany: rxMethod\u003CCarrierIds>(\u002F* ... setAllEntities \u002F upsertEntities on response *\u002F),\n    updateOne: rxMethod\u003CCarrierPatch>(\u002F* ... updateEntity on success, so every reader updates at once *\u002F),\n  })),\n);\n\n\u002F\u002F carrier-list.store.ts — feature-scoped, holds only what's specific to this feature\nexport const CarrierListStore = signalStore(\n  withState\u003C{ visibleIds: string[]; selectedId: string | null }>({ visibleIds: [], selectedId: null }),\n  withComputed((store, entities = inject(CarrierEntitiesStore)) => ({\n    \u002F\u002F Derived, not duplicated — reading entities.entityMap() here means a write from\n    \u002F\u002F ANY feature is visible here without this store doing anything.\n    visibleCarriers: computed(() =>\n      store.visibleIds().map((id) => entities.entityMap()[id]).filter(Boolean)\n    ),\n  })),\n);\n",[651,5038,5039,5044,5059,5071,5088,5117,5143,5168,5177,5183,5187,5192,5207,5265,5295,5300,5305,5320,5364,5371,5379],{"__ignoreMap":653},[837,5040,5041],{"class":839,"line":840},[837,5042,5043],{"class":843},"\u002F\u002F carrier-entities.store.ts — root-provided, the single source of truth for Carrier records\n",[837,5045,5046,5048,5050,5053,5055,5057],{"class":839,"line":847},[837,5047,898],{"class":897},[837,5049,1345],{"class":897},[837,5051,5052],{"class":1348}," CarrierEntitiesStore",[837,5054,950],{"class":924},[837,5056,4481],{"class":854},[837,5058,4484],{"class":937},[837,5060,5061,5063,5065,5067,5069],{"class":839,"line":894},[837,5062,1920],{"class":862},[837,5064,2498],{"class":937},[837,5066,870],{"class":869},[837,5068,2503],{"class":873},[837,5070,1148],{"class":862},[837,5072,5073,5075,5077,5079,5081,5083,5085],{"class":839,"line":917},[837,5074,4523],{"class":854},[837,5076,1069],{"class":1068},[837,5078,1072],{"class":904},[837,5080,1078],{"class":1068},[837,5082,1028],{"class":937},[837,5084,877],{"class":862},[837,5086,5087],{"class":843},"   \u002F\u002F normalized: entityMap keyed by id, via a selectId you configure\n",[837,5089,5090,5092,5094,5096,5098,5100,5102,5104,5106,5109,5111,5113,5115],{"class":839,"line":944},[837,5091,4578],{"class":854},[837,5093,859],{"class":937},[837,5095,859],{"class":862},[837,5097,4552],{"class":1002},[837,5099,877],{"class":862},[837,5101,2713],{"class":1002},[837,5103,950],{"class":924},[837,5105,2718],{"class":854},[837,5107,5108],{"class":937},"(CarriersApiService)",[837,5110,1011],{"class":862},[837,5112,1114],{"class":924},[837,5114,1106],{"class":937},[837,5116,1095],{"class":862},[837,5118,5119,5122,5124,5127,5129,5132,5134,5136,5139,5141],{"class":839,"line":959},[837,5120,5121],{"class":937},"    loadMany",[837,5123,870],{"class":869},[837,5125,5126],{"class":854}," rxMethod",[837,5128,1069],{"class":1068},[837,5130,5131],{"class":904},"CarrierIds",[837,5133,1078],{"class":1068},[837,5135,859],{"class":937},[837,5137,5138],{"class":843},"\u002F* ... setAllEntities \u002F upsertEntities on response *\u002F",[837,5140,1011],{"class":937},[837,5142,1387],{"class":862},[837,5144,5145,5148,5150,5152,5154,5157,5159,5161,5164,5166],{"class":839,"line":984},[837,5146,5147],{"class":937},"    updateOne",[837,5149,870],{"class":869},[837,5151,5126],{"class":854},[837,5153,1069],{"class":1068},[837,5155,5156],{"class":904},"CarrierPatch",[837,5158,1078],{"class":1068},[837,5160,859],{"class":937},[837,5162,5163],{"class":843},"\u002F* ... updateEntity on success, so every reader updates at once *\u002F",[837,5165,1011],{"class":937},[837,5167,1387],{"class":862},[837,5169,5170,5173,5175],{"class":839,"line":991},[837,5171,5172],{"class":862},"  }",[837,5174,4567],{"class":937},[837,5176,1387],{"class":862},[837,5178,5179,5181],{"class":839,"line":1017},[837,5180,1011],{"class":937},[837,5182,941],{"class":862},[837,5184,5185],{"class":839,"line":1022},[837,5186,988],{"emptyLinePlaceholder":987},[837,5188,5189],{"class":839,"line":1033},[837,5190,5191],{"class":843},"\u002F\u002F carrier-list.store.ts — feature-scoped, holds only what's specific to this feature\n",[837,5193,5194,5196,5198,5201,5203,5205],{"class":839,"line":1053},[837,5195,898],{"class":897},[837,5197,1345],{"class":897},[837,5199,5200],{"class":1348}," CarrierListStore",[837,5202,950],{"class":924},[837,5204,4481],{"class":854},[837,5206,4484],{"class":937},[837,5208,5209,5211,5213,5215,5218,5220,5222,5224,5226,5229,5231,5233,5235,5237,5239,5241,5243,5245,5247,5249,5251,5253,5255,5257,5259,5261,5263],{"class":839,"line":1098},[837,5210,4489],{"class":854},[837,5212,1069],{"class":1068},[837,5214,863],{"class":862},[837,5216,5217],{"class":3822}," visibleIds",[837,5219,870],{"class":924},[837,5221,968],{"class":967},[837,5223,1075],{"class":930},[837,5225,1133],{"class":862},[837,5227,5228],{"class":3822}," selectedId",[837,5230,870],{"class":924},[837,5232,968],{"class":967},[837,5234,971],{"class":924},[837,5236,975],{"class":974},[837,5238,888],{"class":862},[837,5240,1078],{"class":1068},[837,5242,859],{"class":937},[837,5244,863],{"class":862},[837,5246,5217],{"class":937},[837,5248,870],{"class":869},[837,5250,938],{"class":937},[837,5252,877],{"class":862},[837,5254,5228],{"class":937},[837,5256,870],{"class":869},[837,5258,975],{"class":967},[837,5260,888],{"class":862},[837,5262,1011],{"class":937},[837,5264,1387],{"class":862},[837,5266,5267,5269,5271,5273,5275,5277,5280,5282,5284,5287,5289,5291,5293],{"class":839,"line":1151},[837,5268,4545],{"class":854},[837,5270,859],{"class":937},[837,5272,859],{"class":862},[837,5274,4552],{"class":1002},[837,5276,877],{"class":862},[837,5278,5279],{"class":1002}," entities",[837,5281,950],{"class":924},[837,5283,2718],{"class":854},[837,5285,5286],{"class":937},"(CarrierEntitiesStore)",[837,5288,1011],{"class":862},[837,5290,1114],{"class":924},[837,5292,1106],{"class":937},[837,5294,1095],{"class":862},[837,5296,5297],{"class":839,"line":1203},[837,5298,5299],{"class":843},"    \u002F\u002F Derived, not duplicated — reading entities.entityMap() here means a write from\n",[837,5301,5302],{"class":839,"line":1213},[837,5303,5304],{"class":843},"    \u002F\u002F ANY feature is visible here without this store doing anything.\n",[837,5306,5307,5310,5312,5314,5316,5318],{"class":839,"line":1219},[837,5308,5309],{"class":937},"    visibleCarriers",[837,5311,870],{"class":869},[837,5313,4887],{"class":854},[837,5315,859],{"class":937},[837,5317,1028],{"class":862},[837,5319,4932],{"class":924},[837,5321,5322,5325,5327,5330,5332,5334,5336,5338,5340,5342,5344,5346,5348,5350,5353,5356,5358,5361],{"class":839,"line":3975},[837,5323,5324],{"class":937},"      store",[837,5326,1040],{"class":869},[837,5328,5329],{"class":854},"visibleIds",[837,5331,1028],{"class":937},[837,5333,1040],{"class":869},[837,5335,3903],{"class":854},[837,5337,859],{"class":937},[837,5339,859],{"class":862},[837,5341,3992],{"class":1002},[837,5343,1011],{"class":862},[837,5345,1114],{"class":924},[837,5347,5279],{"class":937},[837,5349,1040],{"class":869},[837,5351,5352],{"class":854},"entityMap",[837,5354,5355],{"class":937},"()[id])",[837,5357,1040],{"class":869},[837,5359,5360],{"class":854},"filter",[837,5362,5363],{"class":937},"(Boolean)\n",[837,5365,5366,5369],{"class":839,"line":4039},[837,5367,5368],{"class":937},"    )",[837,5370,1387],{"class":862},[837,5372,5373,5375,5377],{"class":839,"line":4507},[837,5374,5172],{"class":862},[837,5376,4567],{"class":937},[837,5378,1387],{"class":862},[837,5380,5381,5383],{"class":839,"line":4520},[837,5382,1011],{"class":937},[837,5384,941],{"class":862},[594,5386,5387,5388,5390,5391,5394,5395,5398,5399,5402,5403,5405,5406,5409],{},"A write from ",[651,5389,5020],{}," that also touches a carrier goes through\n",[651,5392,5393],{},"CarrierEntitiesStore.updateOne",", not through its own ",[651,5396,5397],{},"patchState"," on a locally-held copy —\nso ",[651,5400,5401],{},"CarrierListStore","'s ",[651,5404,682],{}," picks up the change automatically, with no message passed\nbetween the two feature stores and no feature importing another (the dependency rule from\n",[22,5407,5408],{"href":60},"§1.2"," holds: both depend downward on the shared entity store, not on\neach other).",[594,5411,5412,5415],{},[597,5413,5414],{},"When duplication is intentional"," — a feature genuinely needs a denormalized,\nshape-specific read model, and round-tripping through a shared entity store would mean\nreshaping on every read — accept the duplication but make invalidation explicit rather than\nhoping the copies stay in sync. A minimal shared invalidation signal is enough:",[644,5417,5419],{"className":831,"code":5418,"language":833,"meta":653,"style":653},"\u002F\u002F entity-invalidation.ts — a root-provided counter per domain concept\nexport const CARRIER_INVALIDATED = new InjectionToken\u003CWritableSignal\u003Cnumber>>(\n  'carrier-invalidated',\n  { providedIn: 'root', factory: () => signal(0) },\n);\n\n\u002F\u002F any store that mutates a carrier bumps it on success\npatchState(store, { carrier: saved }, setSuccess());\ninject(CARRIER_INVALIDATED).update((n) => n + 1);\n\n\u002F\u002F any store that caches carriers refetches when it changes\nloadCarrier: rxMethod\u003Cstring>(\n  pipe(switchMap((id) => \u002F* combine id with the invalidation signal, refetch on either *\u002F)),\n),\n",[651,5420,5421,5426,5458,5465,5497,5503,5507,5512,5540,5581,5585,5590,5608,5635],{"__ignoreMap":653},[837,5422,5423],{"class":839,"line":840},[837,5424,5425],{"class":843},"\u002F\u002F entity-invalidation.ts — a root-provided counter per domain concept\n",[837,5427,5428,5430,5432,5435,5437,5441,5444,5446,5449,5451,5454,5456],{"class":839,"line":847},[837,5429,898],{"class":897},[837,5431,1345],{"class":897},[837,5433,5434],{"class":1348}," CARRIER_INVALIDATED",[837,5436,950],{"class":924},[837,5438,5440],{"class":5439},"sGr-V"," new",[837,5442,5443],{"class":854}," InjectionToken",[837,5445,1069],{"class":1068},[837,5447,5448],{"class":904},"WritableSignal",[837,5450,1069],{"class":1068},[837,5452,5453],{"class":967},"number",[837,5455,2551],{"class":1068},[837,5457,4484],{"class":937},[837,5459,5460,5463],{"class":839,"line":894},[837,5461,5462],{"class":873},"  'carrier-invalidated'",[837,5464,1387],{"class":862},[837,5466,5467,5469,5471,5473,5475,5477,5480,5482,5484,5486,5488,5490,5493,5495],{"class":839,"line":917},[837,5468,1920],{"class":862},[837,5470,2498],{"class":937},[837,5472,870],{"class":869},[837,5474,2503],{"class":873},[837,5476,877],{"class":862},[837,5478,5479],{"class":854}," factory",[837,5481,870],{"class":869},[837,5483,1976],{"class":862},[837,5485,1114],{"class":924},[837,5487,2534],{"class":854},[837,5489,859],{"class":937},[837,5491,5492],{"class":953},"0",[837,5494,2114],{"class":937},[837,5496,1564],{"class":862},[837,5498,5499,5501],{"class":839,"line":944},[837,5500,1011],{"class":937},[837,5502,941],{"class":862},[837,5504,5505],{"class":839,"line":959},[837,5506,988],{"emptyLinePlaceholder":987},[837,5508,5509],{"class":839,"line":984},[837,5510,5511],{"class":843},"\u002F\u002F any store that mutates a carrier bumps it on success\n",[837,5513,5514,5516,5519,5521,5523,5525,5527,5530,5533,5536,5538],{"class":839,"line":991},[837,5515,5397],{"class":854},[837,5517,5518],{"class":937},"(store",[837,5520,877],{"class":862},[837,5522,1117],{"class":862},[837,5524,4962],{"class":937},[837,5526,870],{"class":869},[837,5528,5529],{"class":937}," saved ",[837,5531,5532],{"class":862},"},",[837,5534,5535],{"class":854}," setSuccess",[837,5537,1384],{"class":937},[837,5539,941],{"class":862},[837,5541,5542,5545,5547,5550,5552,5554,5557,5559,5561,5564,5566,5568,5571,5574,5577,5579],{"class":839,"line":1017},[837,5543,5544],{"class":854},"inject",[837,5546,859],{"class":937},[837,5548,5549],{"class":1348},"CARRIER_INVALIDATED",[837,5551,1011],{"class":937},[837,5553,1040],{"class":869},[837,5555,5556],{"class":854},"update",[837,5558,859],{"class":937},[837,5560,859],{"class":862},[837,5562,5563],{"class":1002},"n",[837,5565,1011],{"class":862},[837,5567,1114],{"class":924},[837,5569,5570],{"class":937}," n ",[837,5572,5573],{"class":924},"+",[837,5575,5576],{"class":953}," 1",[837,5578,1011],{"class":937},[837,5580,941],{"class":862},[837,5582,5583],{"class":839,"line":1022},[837,5584,988],{"emptyLinePlaceholder":987},[837,5586,5587],{"class":839,"line":1033},[837,5588,5589],{"class":843},"\u002F\u002F any store that caches carriers refetches when it changes\n",[837,5591,5592,5596,5598,5600,5602,5604,5606],{"class":839,"line":1053},[837,5593,5595],{"class":5594},"sj8sA","loadCarrier",[837,5597,870],{"class":862},[837,5599,5126],{"class":854},[837,5601,1069],{"class":1068},[837,5603,2381],{"class":967},[837,5605,1078],{"class":1068},[837,5607,4484],{"class":937},[837,5609,5610,5613,5615,5618,5620,5622,5624,5626,5628,5631,5633],{"class":839,"line":1098},[837,5611,5612],{"class":854},"  pipe",[837,5614,859],{"class":937},[837,5616,5617],{"class":854},"switchMap",[837,5619,859],{"class":937},[837,5621,859],{"class":862},[837,5623,3992],{"class":1002},[837,5625,1011],{"class":862},[837,5627,1114],{"class":924},[837,5629,5630],{"class":843}," \u002F* combine id with the invalidation signal, refetch on either *\u002F",[837,5632,4567],{"class":937},[837,5634,1387],{"class":862},[837,5636,5637,5639],{"class":839,"line":1151},[837,5638,1011],{"class":937},[837,5640,1387],{"class":862},[594,5642,5643,5646,5647,5650],{},[597,5644,5645],{},"The cheapest fallback, and often the right call:"," for read-mostly data where staleness for\nthe length of one navigation is an acceptable cost, don't build cache-invalidation machinery\nat all — refetch on route entry. A component-scoped store that reloads its data in\n",[651,5648,5649],{},"withHooks.onInit"," sidesteps the whole consistency problem at the price of one extra request\nper navigation. Reach for the entity-store or invalidation-signal patterns only once that\ncost is measured and too high, not by default.",[594,5652,5653,5656,5657,5659],{},[597,5654,5655],{},"The rule that prevents the bug in the first place:"," never let two stores independently\n",[651,5658,5397],{}," their own copy of the same conceptual entity. One store (or a shared entity\nstore) owns the write; every reader selects from it.",[4325,5661,5663],{"id":5662},"loading-and-error-state-build-it-once-reuse-it-everywhere",[22,5664,164],{"title":625,"href":163},[594,5666,5667],{},"Hand-rolled loading flags are the single most repeated code in Angular applications, and the\nmost consistently buggy — the failure mode is always the same, an error that is set on\nfailure and never cleared on retry.",[594,5669,5670,5671,5674],{},"Build a small reusable store feature and never write the flags again. NgRx's\n",[651,5672,5673],{},"signalStoreFeature"," exists exactly for this:",[644,5676,5678],{"className":831,"code":5677,"language":833,"meta":653,"style":653},"\u002F\u002F call-state.feature.ts — write this once for your whole codebase\ntype CallState = 'init' | 'pending' | 'success' | { error: string };\n\nexport function withCallState() {\n  return signalStoreFeature(\n    withState\u003C{ callState: CallState }>({ callState: 'init' }),\n    withComputed(({ callState }) => ({\n      pending: computed(() => callState() === 'pending'),\n      success: computed(() => callState() === 'success'),\n      error: computed(() => {\n        const state = callState();\n        return typeof state === 'object' ? state.error : null;\n      }),\n    }))\n  );\n}\n\nexport const setPending = () => ({ callState: 'pending' as const });\nexport const setSuccess = () => ({ callState: 'success' as const });\nexport const setError = (error: string) => ({ callState: { error } });\n",[651,5679,5680,5685,5736,5740,5753,5762,5799,5820,5849,5877,5893,5909,5940,5948,5955,5961,5965,5969,6005,6039],{"__ignoreMap":653},[837,5681,5682],{"class":839,"line":840},[837,5683,5684],{"class":843},"\u002F\u002F call-state.feature.ts — write this once for your whole codebase\n",[837,5686,5687,5690,5693,5695,5698,5702,5705,5707,5709,5712,5714,5716,5718,5721,5723,5725,5727,5730,5732,5734],{"class":839,"line":847},[837,5688,5689],{"class":897},"type",[837,5691,5692],{"class":904}," CallState",[837,5694,950],{"class":924},[837,5696,5697],{"class":873}," '",[837,5699,5701],{"class":5700},"sksAd","init",[837,5703,5704],{"class":873},"'",[837,5706,971],{"class":924},[837,5708,5697],{"class":873},[837,5710,5711],{"class":5700},"pending",[837,5713,5704],{"class":873},[837,5715,971],{"class":924},[837,5717,5697],{"class":873},[837,5719,5720],{"class":5700},"success",[837,5722,5704],{"class":873},[837,5724,971],{"class":924},[837,5726,1117],{"class":862},[837,5728,5729],{"class":3822}," error",[837,5731,870],{"class":924},[837,5733,968],{"class":967},[837,5735,4782],{"class":862},[837,5737,5738],{"class":839,"line":894},[837,5739,988],{"emptyLinePlaceholder":987},[837,5741,5742,5744,5746,5749,5751],{"class":839,"line":917},[837,5743,898],{"class":897},[837,5745,1505],{"class":897},[837,5747,5748],{"class":854}," withCallState",[837,5750,1028],{"class":862},[837,5752,914],{"class":862},[837,5754,5755,5757,5760],{"class":839,"line":944},[837,5756,1532],{"class":897},[837,5758,5759],{"class":854}," signalStoreFeature",[837,5761,4484],{"class":937},[837,5763,5764,5767,5769,5771,5774,5776,5778,5780,5782,5784,5786,5788,5790,5793,5795,5797],{"class":839,"line":959},[837,5765,5766],{"class":854},"    withState",[837,5768,1069],{"class":1068},[837,5770,863],{"class":862},[837,5772,5773],{"class":3822}," callState",[837,5775,870],{"class":924},[837,5777,5692],{"class":904},[837,5779,888],{"class":862},[837,5781,1078],{"class":1068},[837,5783,859],{"class":937},[837,5785,863],{"class":862},[837,5787,5773],{"class":937},[837,5789,870],{"class":869},[837,5791,5792],{"class":873}," 'init'",[837,5794,888],{"class":862},[837,5796,1011],{"class":937},[837,5798,1387],{"class":862},[837,5800,5801,5804,5806,5809,5811,5814,5816,5818],{"class":839,"line":984},[837,5802,5803],{"class":854},"    withComputed",[837,5805,859],{"class":937},[837,5807,5808],{"class":862},"({",[837,5810,5773],{"class":1002},[837,5812,5813],{"class":862}," })",[837,5815,1114],{"class":924},[837,5817,1106],{"class":937},[837,5819,1095],{"class":862},[837,5821,5822,5825,5827,5829,5831,5833,5835,5837,5840,5842,5845,5847],{"class":839,"line":991},[837,5823,5824],{"class":937},"      pending",[837,5826,870],{"class":869},[837,5828,4887],{"class":854},[837,5830,859],{"class":937},[837,5832,1028],{"class":862},[837,5834,1114],{"class":924},[837,5836,5773],{"class":854},[837,5838,5839],{"class":937},"() ",[837,5841,4970],{"class":924},[837,5843,5844],{"class":873}," 'pending'",[837,5846,1011],{"class":937},[837,5848,1387],{"class":862},[837,5850,5851,5854,5856,5858,5860,5862,5864,5866,5868,5870,5873,5875],{"class":839,"line":1017},[837,5852,5853],{"class":937},"      success",[837,5855,870],{"class":869},[837,5857,4887],{"class":854},[837,5859,859],{"class":937},[837,5861,1028],{"class":862},[837,5863,1114],{"class":924},[837,5865,5773],{"class":854},[837,5867,5839],{"class":937},[837,5869,4970],{"class":924},[837,5871,5872],{"class":873}," 'success'",[837,5874,1011],{"class":937},[837,5876,1387],{"class":862},[837,5878,5879,5881,5883,5885,5887,5889,5891],{"class":839,"line":1022},[837,5880,1154],{"class":937},[837,5882,870],{"class":869},[837,5884,4887],{"class":854},[837,5886,859],{"class":937},[837,5888,1028],{"class":862},[837,5890,1114],{"class":924},[837,5892,914],{"class":862},[837,5894,5895,5898,5901,5903,5905,5907],{"class":839,"line":1033},[837,5896,5897],{"class":897},"        const",[837,5899,5900],{"class":1348}," state",[837,5902,950],{"class":924},[837,5904,5773],{"class":854},[837,5906,1028],{"class":937},[837,5908,941],{"class":862},[837,5910,5911,5914,5917,5920,5922,5925,5928,5930,5932,5934,5936,5938],{"class":839,"line":1053},[837,5912,5913],{"class":897},"        return",[837,5915,5916],{"class":897}," typeof",[837,5918,5919],{"class":937}," state ",[837,5921,4970],{"class":924},[837,5923,5924],{"class":873}," 'object'",[837,5926,5927],{"class":924}," ?",[837,5929,5900],{"class":937},[837,5931,1040],{"class":869},[837,5933,1174],{"class":937},[837,5935,870],{"class":924},[837,5937,975],{"class":967},[837,5939,941],{"class":862},[837,5941,5942,5944,5946],{"class":839,"line":1098},[837,5943,3889],{"class":862},[837,5945,1011],{"class":937},[837,5947,1387],{"class":862},[837,5949,5950,5952],{"class":839,"line":1151},[837,5951,1206],{"class":862},[837,5953,5954],{"class":937},"))\n",[837,5956,5957,5959],{"class":839,"line":1203},[837,5958,4985],{"class":937},[837,5960,941],{"class":862},[837,5962,5963],{"class":839,"line":1213},[837,5964,1222],{"class":862},[837,5966,5967],{"class":839,"line":1219},[837,5968,988],{"emptyLinePlaceholder":987},[837,5970,5971,5973,5975,5978,5980,5982,5984,5986,5988,5990,5992,5994,5997,5999,6001,6003],{"class":839,"line":3975},[837,5972,898],{"class":897},[837,5974,1345],{"class":897},[837,5976,5977],{"class":854}," setPending",[837,5979,950],{"class":924},[837,5981,1976],{"class":862},[837,5983,1114],{"class":924},[837,5985,1106],{"class":937},[837,5987,863],{"class":862},[837,5989,5773],{"class":937},[837,5991,870],{"class":869},[837,5993,5844],{"class":873},[837,5995,5996],{"class":897}," as",[837,5998,1345],{"class":897},[837,6000,888],{"class":862},[837,6002,1011],{"class":937},[837,6004,941],{"class":862},[837,6006,6007,6009,6011,6013,6015,6017,6019,6021,6023,6025,6027,6029,6031,6033,6035,6037],{"class":839,"line":4039},[837,6008,898],{"class":897},[837,6010,1345],{"class":897},[837,6012,5535],{"class":854},[837,6014,950],{"class":924},[837,6016,1976],{"class":862},[837,6018,1114],{"class":924},[837,6020,1106],{"class":937},[837,6022,863],{"class":862},[837,6024,5773],{"class":937},[837,6026,870],{"class":869},[837,6028,5872],{"class":873},[837,6030,5996],{"class":897},[837,6032,1345],{"class":897},[837,6034,888],{"class":862},[837,6036,1011],{"class":937},[837,6038,941],{"class":862},[837,6040,6041,6043,6045,6048,6050,6052,6054,6056,6058,6060,6062,6064,6066,6068,6070,6072,6075,6077,6079,6081],{"class":839,"line":4507},[837,6042,898],{"class":897},[837,6044,1345],{"class":897},[837,6046,6047],{"class":854}," setError",[837,6049,950],{"class":924},[837,6051,1106],{"class":862},[837,6053,1312],{"class":1002},[837,6055,870],{"class":924},[837,6057,968],{"class":967},[837,6059,1011],{"class":862},[837,6061,1114],{"class":924},[837,6063,1106],{"class":937},[837,6065,863],{"class":862},[837,6067,5773],{"class":937},[837,6069,870],{"class":869},[837,6071,1117],{"class":862},[837,6073,6074],{"class":937}," error ",[837,6076,3505],{"class":862},[837,6078,888],{"class":862},[837,6080,1011],{"class":937},[837,6082,941],{"class":862},[594,6084,6085,6086,6089,6090,679,6093,6096,6097,6100,6101,6104],{},"Real versions take a ",[651,6087,6088],{},"collection"," name so one store can track several independent operations\n(",[651,6091,6092],{},"carriersPending()",[651,6094,6095],{},"regionsPending()","), and add a ",[651,6098,6099],{},"canceled"," state. The principle is what\nmatters: ",[597,6102,6103],{},"loading state is infrastructure, not feature code."," Write it once, and make it\nthe only accepted way to express \"this is loading.\"",[4325,6106,6108],{"id":6107},"orchestration-rxmethod-owns-the-async-flow",[22,6109,6110,6111,6113],{"title":625,"href":169},"Orchestration: ",[651,6112,702],{}," owns the async flow",[594,6115,6116,6117,6119],{},"Fetching, polling, retrying, cancelling, debouncing — all of it lives in the store, in an\n",[651,6118,702],{},". The component's involvement is one call.",[644,6121,6123],{"className":831,"code":6122,"language":833,"meta":653,"style":653},"withMethods((store, carriersApi = inject(CarriersApiService)) => ({\n  loadTopCarriers: rxMethod\u003CDateRange>(\n    pipe(\n      switchMap((range) => {\n        patchState(store, { carriers: [] }, setPending());\n\n        return carriersApi.loadTopCarriers(TOP_CARRIER_COUNT, range).pipe(\n          tapResponse({\n            next: (carriers) => patchState(store, { carriers }, setSuccess()),\n            error: (error: Error) => patchState(store, { carriers: [] }, setError(error.message)),\n          })\n        );\n      })\n    )\n  ),\n\n  selectCarrier: (id: string) => patchState(store, { selectedId: id }),\n})),\n",[651,6124,6125,6154,6171,6178,6196,6222,6226,6252,6261,6296,6344,6351,6358,6364,6369,6375,6379,6419],{"__ignoreMap":653},[837,6126,6127,6129,6131,6133,6135,6137,6140,6142,6144,6146,6148,6150,6152],{"class":839,"line":840},[837,6128,2785],{"class":854},[837,6130,859],{"class":937},[837,6132,859],{"class":862},[837,6134,4552],{"class":1002},[837,6136,877],{"class":862},[837,6138,6139],{"class":1002}," carriersApi",[837,6141,950],{"class":924},[837,6143,2718],{"class":854},[837,6145,5108],{"class":937},[837,6147,1011],{"class":862},[837,6149,1114],{"class":924},[837,6151,1106],{"class":937},[837,6153,1095],{"class":862},[837,6155,6156,6158,6160,6162,6164,6167,6169],{"class":839,"line":847},[837,6157,3759],{"class":937},[837,6159,870],{"class":869},[837,6161,5126],{"class":854},[837,6163,1069],{"class":1068},[837,6165,6166],{"class":904},"DateRange",[837,6168,1078],{"class":1068},[837,6170,4484],{"class":937},[837,6172,6173,6176],{"class":839,"line":894},[837,6174,6175],{"class":854},"    pipe",[837,6177,4484],{"class":937},[837,6179,6180,6183,6185,6187,6190,6192,6194],{"class":839,"line":917},[837,6181,6182],{"class":854},"      switchMap",[837,6184,859],{"class":937},[837,6186,859],{"class":862},[837,6188,6189],{"class":1002},"range",[837,6191,1011],{"class":862},[837,6193,1114],{"class":924},[837,6195,914],{"class":862},[837,6197,6198,6201,6203,6205,6207,6209,6211,6214,6216,6218,6220],{"class":839,"line":944},[837,6199,6200],{"class":854},"        patchState",[837,6202,5518],{"class":937},[837,6204,877],{"class":862},[837,6206,1117],{"class":862},[837,6208,1130],{"class":937},[837,6210,870],{"class":869},[837,6212,6213],{"class":937}," [] ",[837,6215,5532],{"class":862},[837,6217,5977],{"class":854},[837,6219,1384],{"class":937},[837,6221,941],{"class":862},[837,6223,6224],{"class":839,"line":959},[837,6225,988],{"emptyLinePlaceholder":987},[837,6227,6228,6230,6232,6234,6236,6238,6241,6243,6246,6248,6250],{"class":839,"line":984},[837,6229,5913],{"class":897},[837,6231,6139],{"class":937},[837,6233,1040],{"class":869},[837,6235,4101],{"class":854},[837,6237,859],{"class":937},[837,6239,6240],{"class":1348},"TOP_CARRIER_COUNT",[837,6242,877],{"class":862},[837,6244,6245],{"class":937}," range)",[837,6247,1040],{"class":869},[837,6249,3898],{"class":854},[837,6251,4484],{"class":937},[837,6253,6254,6257,6259],{"class":839,"line":991},[837,6255,6256],{"class":854},"          tapResponse",[837,6258,859],{"class":937},[837,6260,1095],{"class":862},[837,6262,6263,6266,6268,6270,6272,6274,6276,6279,6281,6283,6285,6288,6290,6292,6294],{"class":839,"line":1017},[837,6264,6265],{"class":854},"            next",[837,6267,870],{"class":869},[837,6269,1106],{"class":862},[837,6271,1109],{"class":1002},[837,6273,1011],{"class":862},[837,6275,1114],{"class":924},[837,6277,6278],{"class":854}," patchState",[837,6280,5518],{"class":937},[837,6282,877],{"class":862},[837,6284,1117],{"class":862},[837,6286,6287],{"class":937}," carriers ",[837,6289,5532],{"class":862},[837,6291,5535],{"class":854},[837,6293,1384],{"class":937},[837,6295,1387],{"class":862},[837,6297,6298,6301,6303,6305,6307,6309,6312,6314,6316,6318,6320,6322,6324,6326,6328,6330,6332,6334,6337,6339,6342],{"class":839,"line":1022},[837,6299,6300],{"class":854},"            error",[837,6302,870],{"class":869},[837,6304,1106],{"class":862},[837,6306,1312],{"class":1002},[837,6308,870],{"class":924},[837,6310,6311],{"class":904}," Error",[837,6313,1011],{"class":862},[837,6315,1114],{"class":924},[837,6317,6278],{"class":854},[837,6319,5518],{"class":937},[837,6321,877],{"class":862},[837,6323,1117],{"class":862},[837,6325,1130],{"class":937},[837,6327,870],{"class":869},[837,6329,6213],{"class":937},[837,6331,5532],{"class":862},[837,6333,6047],{"class":854},[837,6335,6336],{"class":937},"(error",[837,6338,1040],{"class":869},[837,6340,6341],{"class":937},"message))",[837,6343,1387],{"class":862},[837,6345,6346,6349],{"class":839,"line":1033},[837,6347,6348],{"class":862},"          }",[837,6350,891],{"class":937},[837,6352,6353,6356],{"class":839,"line":1053},[837,6354,6355],{"class":937},"        )",[837,6357,941],{"class":862},[837,6359,6360,6362],{"class":839,"line":1098},[837,6361,3889],{"class":862},[837,6363,891],{"class":937},[837,6365,6366],{"class":839,"line":1151},[837,6367,6368],{"class":937},"    )\n",[837,6370,6371,6373],{"class":839,"line":1203},[837,6372,4985],{"class":937},[837,6374,1387],{"class":862},[837,6376,6377],{"class":839,"line":1213},[837,6378,988],{"emptyLinePlaceholder":987},[837,6380,6381,6384,6386,6388,6390,6392,6394,6396,6398,6400,6402,6404,6406,6408,6410,6413,6415,6417],{"class":839,"line":1219},[837,6382,6383],{"class":854},"  selectCarrier",[837,6385,870],{"class":869},[837,6387,1106],{"class":862},[837,6389,3992],{"class":1002},[837,6391,870],{"class":924},[837,6393,968],{"class":967},[837,6395,1011],{"class":862},[837,6397,1114],{"class":924},[837,6399,6278],{"class":854},[837,6401,5518],{"class":937},[837,6403,877],{"class":862},[837,6405,1117],{"class":862},[837,6407,5228],{"class":937},[837,6409,870],{"class":869},[837,6411,6412],{"class":937}," id ",[837,6414,3505],{"class":862},[837,6416,1011],{"class":937},[837,6418,1387],{"class":862},[837,6420,6421,6423,6425],{"class":839,"line":3975},[837,6422,3505],{"class":862},[837,6424,4567],{"class":937},[837,6426,1387],{"class":862},[594,6428,6429],{},"Three things are load-bearing here.",[594,6431,6432,6440,6441,6444,6445,6448,6449,5402,6451,6454,6455,6457],{},[597,6433,6434,6437,6438,1040],{},[651,6435,6436],{},"tapResponse",", never ",[651,6439,1090],{}," From ",[651,6442,6443],{},"@ngrx\u002Foperators",". A bare ",[651,6446,6447],{},"subscribe()"," inside a\nstore method leaks, and — worse — an unhandled error in ",[651,6450,1090],{},[651,6452,6453],{},"next"," kills the outer\nsubscription permanently, so the method silently stops working for the rest of the session.\n",[651,6456,6436],{}," handles both.",[594,6459,6460,6463],{},[597,6461,6462],{},"The flattening operator is a semantic decision, not a default."," Getting this wrong\nproduces race conditions that only appear under load:",[1645,6465,6466,6479],{},[1648,6467,6468],{},[1651,6469,6470,6473,6476],{},[1654,6471,6472],{},"Operator",[1654,6474,6475],{},"Semantics",[1654,6477,6478],{},"Use for",[1664,6480,6481,6493,6506,6519],{},[1651,6482,6483,6487,6490],{},[1669,6484,6485],{},[651,6486,5617],{},[1669,6488,6489],{},"Cancel in-flight, keep latest",[1669,6491,6492],{},"Searches, filter changes, navigation-triggered loads",[1651,6494,6495,6500,6503],{},[1669,6496,6497],{},[651,6498,6499],{},"exhaustMap",[1669,6501,6502],{},"Ignore new while one is running",[1669,6504,6505],{},"Form submits, anything irreversible",[1651,6507,6508,6513,6516],{},[1669,6509,6510],{},[651,6511,6512],{},"concatMap",[1669,6514,6515],{},"Queue, preserve order",[1669,6517,6518],{},"Optimistic updates, ordered mutations",[1651,6520,6521,6526,6529],{},[1669,6522,6523],{},[651,6524,6525],{},"mergeMap",[1669,6527,6528],{},"All in parallel, no ordering",[1669,6530,6531],{},"Rare — usually a bug in disguise",[594,6533,3022,6534,6536,6537,6540,6541,6543],{},[651,6535,5617],{}," above isn't decoration. It guarantees that when the user changes the date\nrange twice quickly, a response for the ",[603,6538,6539],{},"old"," range can never overwrite state for the new\none. That correctness property is why the fetch belongs in the store: a component that owns\nits own ",[651,6542,1090],{}," has no cancellation story at all.",[594,6545,6546,6552,6553,6556,6557,6560,6561,679,6564,6567,6568,6571,6572,6574],{},[597,6547,6548,6549,6551],{},"An ",[651,6550,702],{}," accepts a signal, and that is how you make a fetch reactive."," This is the\nthird load-bearing property and the one most often missed. ",[651,6554,6555],{},"rxMethod\u003CT>"," can be called with a\nplain ",[651,6558,6559],{},"T",", an ",[651,6562,6563],{},"Observable\u003CT>",[603,6565,6566],{},"or"," a ",[651,6569,6570],{},"Signal\u003CT>",". Hand it a signal and it subscribes to that\nsignal for the lifetime of its injection context: every time the value changes, the pipeline\nre-runs — and because the pipeline starts with ",[651,6573,5617],{},", the superseded request is\ncancelled automatically.",[644,6576,6578],{"className":831,"code":6577,"language":833,"meta":653,"style":653},"\u002F\u002F ✅ One line. Reactive re-fetch + cancellation + call state, all wired.\nthis.store.loadTopCarriers(this.range);   \u002F\u002F `range` is a Signal\u003CDateRange>\n",[651,6579,6580,6585],{"__ignoreMap":653},[837,6581,6582],{"class":839,"line":840},[837,6583,6584],{"class":843},"\u002F\u002F ✅ One line. Reactive re-fetch + cancellation + call state, all wired.\n",[837,6586,6587,6589,6591,6593,6595,6597,6599,6601,6603,6606,6608],{"class":839,"line":847},[837,6588,2317],{"class":1036},[837,6590,1040],{"class":869},[837,6592,4552],{"class":937},[837,6594,1040],{"class":869},[837,6596,4101],{"class":854},[837,6598,859],{"class":937},[837,6600,2317],{"class":1036},[837,6602,1040],{"class":869},[837,6604,6605],{"class":937},"range)",[837,6607,1133],{"class":862},[837,6609,6610],{"class":843},"   \u002F\u002F `range` is a Signal\u003CDateRange>\n",[594,6612,6613],{},"Read that against what you'd otherwise write:",[644,6615,6617],{"className":831,"code":6616,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ An effect that reinvents rxMethod's signal handling, minus the cancellation\neffect(() => {\n  this.store.loadTopCarriers(this.range());\n});\n",[651,6618,6619,6624,6636,6660],{"__ignoreMap":653},[837,6620,6621],{"class":839,"line":840},[837,6622,6623],{"class":843},"\u002F\u002F ❌ An effect that reinvents rxMethod's signal handling, minus the cancellation\n",[837,6625,6626,6628,6630,6632,6634],{"class":839,"line":847},[837,6627,688],{"class":854},[837,6629,859],{"class":937},[837,6631,1028],{"class":862},[837,6633,1114],{"class":924},[837,6635,914],{"class":862},[837,6637,6638,6640,6642,6644,6646,6648,6650,6652,6654,6656,6658],{"class":839,"line":894},[837,6639,3480],{"class":1036},[837,6641,1040],{"class":869},[837,6643,4552],{"class":937},[837,6645,1040],{"class":869},[837,6647,4101],{"class":854},[837,6649,859],{"class":937},[837,6651,2317],{"class":1036},[837,6653,1040],{"class":869},[837,6655,6189],{"class":854},[837,6657,1384],{"class":937},[837,6659,941],{"class":862},[837,6661,6662,6664,6666],{"class":839,"line":917},[837,6663,3505],{"class":862},[837,6665,1011],{"class":937},[837,6667,941],{"class":862},[594,6669,6670,6671,6680],{},"The effect version is longer, adds a node to the reactive graph, drops the automatic\nteardown, and — most damagingly — establishes a place where a future contributor will feel\ninvited to write state. ",[597,6672,6673,6674,6676,6677,6679],{},"Whenever you catch yourself reaching for ",[651,6675,692],{}," to trigger a\nfetch, the answer is almost always to pass the signal to the ",[651,6678,702],{}," instead."," This is\nthe single highest-leverage substitution in the entire guide: it eliminates the large majority\nof effects that get written in signal-based Angular codebases.",[594,6682,6683,6684,6686],{},"The same trick works for derived arguments. If the fetch depends on several signals, combine\nthem into one ",[651,6685,682],{}," and pass that:",[644,6688,6690],{"className":831,"code":6689,"language":833,"meta":653,"style":653},"private readonly query = computed(() => ({\n  range: this.range(),\n  region: this.store.selectedRegion(),\n  page: this.page(),\n}));\n\nconstructor() {\n  this.store.loadTopCarriers(this.query);  \u002F\u002F re-fetches when any input changes\n}\n",[651,6691,6692,6711,6728,6750,6768,6776,6780,6789,6815],{"__ignoreMap":653},[837,6693,6694,6697,6699,6701,6703,6705,6707,6709],{"class":839,"line":840},[837,6695,6696],{"class":937},"private readonly query ",[837,6698,934],{"class":924},[837,6700,4887],{"class":854},[837,6702,859],{"class":937},[837,6704,1028],{"class":862},[837,6706,1114],{"class":924},[837,6708,1106],{"class":937},[837,6710,1095],{"class":862},[837,6712,6713,6716,6718,6720,6722,6724,6726],{"class":839,"line":847},[837,6714,6715],{"class":937},"  range",[837,6717,870],{"class":869},[837,6719,1120],{"class":1036},[837,6721,1040],{"class":869},[837,6723,6189],{"class":854},[837,6725,1028],{"class":937},[837,6727,1387],{"class":862},[837,6729,6730,6733,6735,6737,6739,6741,6743,6746,6748],{"class":839,"line":894},[837,6731,6732],{"class":937},"  region",[837,6734,870],{"class":869},[837,6736,1120],{"class":1036},[837,6738,1040],{"class":869},[837,6740,4552],{"class":937},[837,6742,1040],{"class":869},[837,6744,6745],{"class":854},"selectedRegion",[837,6747,1028],{"class":937},[837,6749,1387],{"class":862},[837,6751,6752,6755,6757,6759,6761,6764,6766],{"class":839,"line":917},[837,6753,6754],{"class":937},"  page",[837,6756,870],{"class":869},[837,6758,1120],{"class":1036},[837,6760,1040],{"class":869},[837,6762,6763],{"class":854},"page",[837,6765,1028],{"class":937},[837,6767,1387],{"class":862},[837,6769,6770,6772,6774],{"class":839,"line":944},[837,6771,3505],{"class":862},[837,6773,4567],{"class":937},[837,6775,941],{"class":862},[837,6777,6778],{"class":839,"line":959},[837,6779,988],{"emptyLinePlaceholder":987},[837,6781,6782,6785,6787],{"class":839,"line":984},[837,6783,6784],{"class":854},"constructor",[837,6786,5839],{"class":937},[837,6788,1095],{"class":862},[837,6790,6791,6793,6795,6797,6799,6801,6803,6805,6807,6810,6812],{"class":839,"line":991},[837,6792,3480],{"class":1036},[837,6794,1040],{"class":869},[837,6796,4552],{"class":937},[837,6798,1040],{"class":869},[837,6800,4101],{"class":854},[837,6802,859],{"class":937},[837,6804,2317],{"class":1036},[837,6806,1040],{"class":869},[837,6808,6809],{"class":937},"query)",[837,6811,1133],{"class":862},[837,6813,6814],{"class":843},"  \u002F\u002F re-fetches when any input changes\n",[837,6816,6817],{"class":839,"line":1017},[837,6818,1222],{"class":862},[594,6820,6821,6822,1040],{},"One caveat: the signal overload registers a reactive subscription, so it must be called inside\nan injection context — a field initialiser or the constructor. Calling it from a click handler\nworks fine with a plain value, but pass a signal there and you'll need an explicit ",[651,6823,6824],{},"injector",[4325,6826,6828],{"id":6827},"make-every-mutation-self-describing",[22,6829,176],{"title":625,"href":175},[594,6831,6832,6835,6836,6839,6840,6843],{},[651,6833,6834],{},"patchState(store, { selectedId: id })"," tells devtools ",[603,6837,6838],{},"what"," changed. It tells you nothing\nabout ",[603,6841,6842],{},"why",", which is what you actually need at 2am. Wrap it:",[644,6845,6847],{"className":831,"code":6846,"language":833,"meta":653,"style":653},"\u002F\u002F action-patch-state.ts\nexport function actionPatchState\u003CState extends object>(\n  action: string,\n  store: WritableStateSource\u003CState>,\n  ...updaters: PartialStateUpdater\u003CState>[]\n): void {\n  \u002F\u002F log `action` to your devtools bridge, then delegate\n  patchState(store, ...updaters);\n}\n",[651,6848,6849,6854,6877,6888,6906,6928,6938,6943,6959],{"__ignoreMap":653},[837,6850,6851],{"class":839,"line":840},[837,6852,6853],{"class":843},"\u002F\u002F action-patch-state.ts\n",[837,6855,6856,6858,6860,6863,6865,6867,6870,6873,6875],{"class":839,"line":847},[837,6857,898],{"class":897},[837,6859,1505],{"class":897},[837,6861,6862],{"class":854}," actionPatchState",[837,6864,1069],{"class":1068},[837,6866,560],{"class":904},[837,6868,6869],{"class":897}," extends",[837,6871,6872],{"class":974}," object",[837,6874,1078],{"class":1068},[837,6876,4484],{"class":862},[837,6878,6879,6882,6884,6886],{"class":839,"line":894},[837,6880,6881],{"class":1002},"  action",[837,6883,870],{"class":924},[837,6885,968],{"class":967},[837,6887,1387],{"class":862},[837,6889,6890,6893,6895,6898,6900,6902,6904],{"class":839,"line":917},[837,6891,6892],{"class":1002},"  store",[837,6894,870],{"class":924},[837,6896,6897],{"class":904}," WritableStateSource",[837,6899,1069],{"class":1068},[837,6901,560],{"class":904},[837,6903,1078],{"class":1068},[837,6905,1387],{"class":862},[837,6907,6908,6911,6914,6916,6919,6921,6923,6925],{"class":839,"line":944},[837,6909,6910],{"class":924},"  ...",[837,6912,6913],{"class":1002},"updaters",[837,6915,870],{"class":924},[837,6917,6918],{"class":904}," PartialStateUpdater",[837,6920,1069],{"class":1068},[837,6922,560],{"class":904},[837,6924,1078],{"class":1068},[837,6926,6927],{"class":930},"[]\n",[837,6929,6930,6932,6934,6936],{"class":839,"line":959},[837,6931,1011],{"class":862},[837,6933,870],{"class":924},[837,6935,2636],{"class":967},[837,6937,914],{"class":862},[837,6939,6940],{"class":839,"line":984},[837,6941,6942],{"class":843},"  \u002F\u002F log `action` to your devtools bridge, then delegate\n",[837,6944,6945,6948,6950,6952,6954,6957],{"class":839,"line":991},[837,6946,6947],{"class":854},"  patchState",[837,6949,5518],{"class":937},[837,6951,877],{"class":862},[837,6953,4530],{"class":924},[837,6955,6956],{"class":937},"updaters)",[837,6958,941],{"class":862},[837,6960,6961],{"class":839,"line":1017},[837,6962,1222],{"class":862},[594,6964,6965,6966,870],{},"Then every mutation reads as a sentence, with a convention of ",[651,6967,6968],{},"'methodName(): what happened'",[644,6970,6972],{"className":831,"code":6971,"language":833,"meta":653,"style":653},"actionPatchState('loadTopCarriers(): fetching', store, { carriers: [] }, setPending());\nactionPatchState('loadTopCarriers(): success', store, { carriers }, setSuccess());\nactionPatchState('selectCarrier(): selection changed', store, { selectedId: id });\n",[651,6973,6974,7006,7033],{"__ignoreMap":653},[837,6975,6976,6979,6981,6984,6986,6988,6990,6992,6994,6996,6998,7000,7002,7004],{"class":839,"line":840},[837,6977,6978],{"class":854},"actionPatchState",[837,6980,859],{"class":937},[837,6982,6983],{"class":873},"'loadTopCarriers(): fetching'",[837,6985,877],{"class":862},[837,6987,4896],{"class":937},[837,6989,877],{"class":862},[837,6991,1117],{"class":862},[837,6993,1130],{"class":937},[837,6995,870],{"class":869},[837,6997,6213],{"class":937},[837,6999,5532],{"class":862},[837,7001,5977],{"class":854},[837,7003,1384],{"class":937},[837,7005,941],{"class":862},[837,7007,7008,7010,7012,7015,7017,7019,7021,7023,7025,7027,7029,7031],{"class":839,"line":847},[837,7009,6978],{"class":854},[837,7011,859],{"class":937},[837,7013,7014],{"class":873},"'loadTopCarriers(): success'",[837,7016,877],{"class":862},[837,7018,4896],{"class":937},[837,7020,877],{"class":862},[837,7022,1117],{"class":862},[837,7024,6287],{"class":937},[837,7026,5532],{"class":862},[837,7028,5535],{"class":854},[837,7030,1384],{"class":937},[837,7032,941],{"class":862},[837,7034,7035,7037,7039,7042,7044,7046,7048,7050,7052,7054,7056,7058,7060],{"class":839,"line":894},[837,7036,6978],{"class":854},[837,7038,859],{"class":937},[837,7040,7041],{"class":873},"'selectCarrier(): selection changed'",[837,7043,877],{"class":862},[837,7045,4896],{"class":937},[837,7047,877],{"class":862},[837,7049,1117],{"class":862},[837,7051,5228],{"class":937},[837,7053,870],{"class":869},[837,7055,6412],{"class":937},[837,7057,3505],{"class":862},[837,7059,1011],{"class":937},[837,7061,941],{"class":862},[594,7063,7064,7065,7068,7069,7072,7073,7076,7077,1040],{},"Pair it with a ",[651,7066,7067],{},"withLogger('CarriersStore')"," feature — always last, always named to match the\nexport — and your devtools timeline becomes a readable narrative of what the application did,\nper store. The cost is one string per mutation. It is the cheapest debugging affordance in the\nstack, and the reason to make it mandatory rather than optional is that a ",[603,7070,7071],{},"partially","\ninstrumented timeline is nearly as useless as none. ",[597,7074,7075],{},"This is devtools-only"," — it disappears\nthe moment the tab closes. For production error visibility, see\n",[22,7078,7079],{"href":399},"Part 6",[4325,7081,7083],{"id":7082},"lifecycle-belongs-to-the-store-not-the-component",[22,7084,182],{"title":625,"href":181},[594,7086,7087],{},"If the store owns polling, the store owns starting and stopping it:",[644,7089,7091],{"className":831,"code":7090,"language":833,"meta":653,"style":653},"withHooks({\n  onInit(store) {\n    \u002F\u002F effect() is acceptable here: store-owned lifecycle, not state derivation\n    effect((onCleanup) => {\n      const subscription = timer(0, POLL_INTERVAL_MS).subscribe(() => store.loadTopCarriers());\n      onCleanup(() => subscription.unsubscribe());\n    });\n  },\n}),\n",[651,7092,7093,7102,7115,7120,7138,7181,7203,7211,7215],{"__ignoreMap":653},[837,7094,7095,7098,7100],{"class":839,"line":840},[837,7096,7097],{"class":854},"withHooks",[837,7099,859],{"class":937},[837,7101,1095],{"class":862},[837,7103,7104,7107,7109,7111,7113],{"class":839,"line":847},[837,7105,7106],{"class":854},"  onInit",[837,7108,859],{"class":862},[837,7110,4552],{"class":1002},[837,7112,1011],{"class":862},[837,7114,914],{"class":862},[837,7116,7117],{"class":839,"line":894},[837,7118,7119],{"class":843},"    \u002F\u002F effect() is acceptable here: store-owned lifecycle, not state derivation\n",[837,7121,7122,7125,7127,7129,7132,7134,7136],{"class":839,"line":917},[837,7123,7124],{"class":854},"    effect",[837,7126,859],{"class":937},[837,7128,859],{"class":862},[837,7130,7131],{"class":1002},"onCleanup",[837,7133,1011],{"class":862},[837,7135,1114],{"class":924},[837,7137,914],{"class":862},[837,7139,7140,7142,7145,7147,7150,7152,7154,7156,7159,7161,7163,7165,7167,7169,7171,7173,7175,7177,7179],{"class":839,"line":944},[837,7141,2710],{"class":897},[837,7143,7144],{"class":1348}," subscription",[837,7146,950],{"class":924},[837,7148,7149],{"class":854}," timer",[837,7151,859],{"class":937},[837,7153,5492],{"class":953},[837,7155,877],{"class":862},[837,7157,7158],{"class":1348}," POLL_INTERVAL_MS",[837,7160,1011],{"class":937},[837,7162,1040],{"class":869},[837,7164,1090],{"class":854},[837,7166,859],{"class":937},[837,7168,1028],{"class":862},[837,7170,1114],{"class":924},[837,7172,4896],{"class":937},[837,7174,1040],{"class":869},[837,7176,4101],{"class":854},[837,7178,1384],{"class":937},[837,7180,941],{"class":862},[837,7182,7183,7186,7188,7190,7192,7194,7196,7199,7201],{"class":839,"line":959},[837,7184,7185],{"class":854},"      onCleanup",[837,7187,859],{"class":937},[837,7189,1028],{"class":862},[837,7191,1114],{"class":924},[837,7193,7144],{"class":937},[837,7195,1040],{"class":869},[837,7197,7198],{"class":854},"unsubscribe",[837,7200,1384],{"class":937},[837,7202,941],{"class":862},[837,7204,7205,7207,7209],{"class":839,"line":984},[837,7206,1206],{"class":862},[837,7208,1011],{"class":937},[837,7210,941],{"class":862},[837,7212,7213],{"class":839,"line":991},[837,7214,2018],{"class":862},[837,7216,7217,7219,7221],{"class":839,"line":1017},[837,7218,3505],{"class":862},[837,7220,1011],{"class":937},[837,7222,1387],{"class":862},[594,7224,7225,7226,7228],{},"Note the ",[651,7227,7131],{}," callback — without it, a component-scoped store that gets destroyed and\nrecreated (route navigation, tab switching) accumulates timers. This is the most common leak\nin signal-store code.",[594,7230,7231,7239,7240,7242],{},[597,7232,7233,7234,7236,7237,1040],{},"Never call ",[651,7235,692],{}," inside ",[651,7238,2785],{}," Methods run outside the injection context;\n",[651,7241,692],{}," requires one. It will throw.",[4325,7244,7246],{"id":7245},"scoping-singleton-or-component-scoped",[22,7247,188],{"title":625,"href":187},[1645,7249,7250,7260],{},[1648,7251,7252],{},[1651,7253,7254,7257],{},[1654,7255,7256],{},"Need",[1654,7258,7259],{},"How",[1664,7261,7262,7275,7289],{},[1651,7263,7264,7267],{},[1669,7265,7266],{},"Shared across the app (session, tenant, feature flags)",[1669,7268,7269,7272,7273],{},[651,7270,7271],{},"signalStore({ providedIn: 'root' }, ...)",", in ",[651,7274,1678],{},[1651,7276,7277,7280],{},[1669,7278,7279],{},"Shared across one feature's routes",[1669,7281,4076,7282,7285,7286],{},[651,7283,7284],{},"providedIn","; add to the feature route's ",[651,7287,7288],{},"providers: []",[1651,7290,7291,7294],{},[1669,7292,7293],{},"Owned by one component, dies with it",[1669,7295,4076,7296,7298,7299],{},[651,7297,7284],{},"; add to the component's ",[651,7300,7288],{},[594,7302,7303,7304,7306,7307,7309],{},"Component-scoped is the right default. A store in ",[651,7305,7288],{}," gets a fresh instance per\ncomponent instance and is destroyed with it — no cross-instance state bleed, no manual reset\nlogic. Reach for ",[651,7308,4120],{}," only when two unrelated parts of the tree genuinely need\nto observe the same state.",[4325,7311,7313],{"id":7312},"when-a-store-gets-big",[22,7314,194],{"title":625,"href":193},[594,7316,7317],{},"At roughly 200 lines, extract pure functions into sibling files and let the store become a\ncomposition of thin wrappers:",[644,7319,7322],{"className":7320,"code":7321,"language":649},[647],"carriers\u002F\n  carriers.store.ts       ← composition; each method 1–5 lines\n  carriers.state.ts       ← interface + initialState\n  carriers.computed.ts    ← pure derivation functions\n  carriers.methods.ts     ← pure state-transition functions\n  carriers.store.spec.ts\n",[651,7323,7321],{"__ignoreMap":653},[644,7325,7327],{"className":831,"code":7326,"language":833,"meta":653,"style":653},"export const CarriersStore = signalStore(\n  withState(initialState),\n  withCallState(),\n  withComputed((store) => ({\n    summary: computed(() => buildSummary(store.carriers(), store.selectedId())),\n    sorted: computed(() => sortCarriers(store.carriers())),\n  })),\n  withMethods((store) => ({\n    selectCarrier: (id: string) =>\n      actionPatchState('selectCarrier()', store, applySelection(store, id)),\n  })),\n  withLogger('CarriersStore')\n);\n",[651,7328,7329,7343,7351,7359,7377,7415,7443,7451,7469,7488,7516,7524,7534],{"__ignoreMap":653},[837,7330,7331,7333,7335,7337,7339,7341],{"class":839,"line":840},[837,7332,898],{"class":897},[837,7334,1345],{"class":897},[837,7336,4476],{"class":1348},[837,7338,950],{"class":924},[837,7340,4481],{"class":854},[837,7342,4484],{"class":937},[837,7344,7345,7347,7349],{"class":839,"line":847},[837,7346,4489],{"class":854},[837,7348,4499],{"class":937},[837,7350,1387],{"class":862},[837,7352,7353,7355,7357],{"class":839,"line":894},[837,7354,4510],{"class":854},[837,7356,1028],{"class":937},[837,7358,1387],{"class":862},[837,7360,7361,7363,7365,7367,7369,7371,7373,7375],{"class":839,"line":917},[837,7362,4545],{"class":854},[837,7364,859],{"class":937},[837,7366,859],{"class":862},[837,7368,4552],{"class":1002},[837,7370,1011],{"class":862},[837,7372,1114],{"class":924},[837,7374,1106],{"class":937},[837,7376,1095],{"class":862},[837,7378,7379,7382,7384,7386,7388,7390,7392,7395,7397,7399,7401,7403,7405,7407,7409,7411,7413],{"class":839,"line":944},[837,7380,7381],{"class":937},"    summary",[837,7383,870],{"class":869},[837,7385,4887],{"class":854},[837,7387,859],{"class":937},[837,7389,1028],{"class":862},[837,7391,1114],{"class":924},[837,7393,7394],{"class":854}," buildSummary",[837,7396,5518],{"class":937},[837,7398,1040],{"class":869},[837,7400,1109],{"class":854},[837,7402,1028],{"class":937},[837,7404,877],{"class":862},[837,7406,4896],{"class":937},[837,7408,1040],{"class":869},[837,7410,4977],{"class":854},[837,7412,2755],{"class":937},[837,7414,1387],{"class":862},[837,7416,7417,7420,7422,7424,7426,7428,7430,7433,7435,7437,7439,7441],{"class":839,"line":959},[837,7418,7419],{"class":937},"    sorted",[837,7421,870],{"class":869},[837,7423,4887],{"class":854},[837,7425,859],{"class":937},[837,7427,1028],{"class":862},[837,7429,1114],{"class":924},[837,7431,7432],{"class":854}," sortCarriers",[837,7434,5518],{"class":937},[837,7436,1040],{"class":869},[837,7438,1109],{"class":854},[837,7440,2755],{"class":937},[837,7442,1387],{"class":862},[837,7444,7445,7447,7449],{"class":839,"line":984},[837,7446,5172],{"class":862},[837,7448,4567],{"class":937},[837,7450,1387],{"class":862},[837,7452,7453,7455,7457,7459,7461,7463,7465,7467],{"class":839,"line":991},[837,7454,4578],{"class":854},[837,7456,859],{"class":937},[837,7458,859],{"class":862},[837,7460,4552],{"class":1002},[837,7462,1011],{"class":862},[837,7464,1114],{"class":924},[837,7466,1106],{"class":937},[837,7468,1095],{"class":862},[837,7470,7471,7474,7476,7478,7480,7482,7484,7486],{"class":839,"line":1017},[837,7472,7473],{"class":854},"    selectCarrier",[837,7475,870],{"class":869},[837,7477,1106],{"class":862},[837,7479,3992],{"class":1002},[837,7481,870],{"class":924},[837,7483,968],{"class":967},[837,7485,1011],{"class":862},[837,7487,4932],{"class":924},[837,7489,7490,7493,7495,7498,7500,7502,7504,7507,7509,7511,7514],{"class":839,"line":1022},[837,7491,7492],{"class":854},"      actionPatchState",[837,7494,859],{"class":937},[837,7496,7497],{"class":873},"'selectCarrier()'",[837,7499,877],{"class":862},[837,7501,4896],{"class":937},[837,7503,877],{"class":862},[837,7505,7506],{"class":854}," applySelection",[837,7508,5518],{"class":937},[837,7510,877],{"class":862},[837,7512,7513],{"class":937}," id))",[837,7515,1387],{"class":862},[837,7517,7518,7520,7522],{"class":839,"line":1033},[837,7519,5172],{"class":862},[837,7521,4567],{"class":937},[837,7523,1387],{"class":862},[837,7525,7526,7528,7530,7532],{"class":839,"line":1053},[837,7527,4630],{"class":854},[837,7529,859],{"class":937},[837,7531,4635],{"class":873},[837,7533,891],{"class":937},[837,7535,7536,7538],{"class":839,"line":1098},[837,7537,1011],{"class":937},[837,7539,941],{"class":862},[594,7541,7542,7543,7545],{},"Pure functions are trivially unit-testable without a ",[651,7544,2799],{},", and the store file stays a\none-screen table of contents for the feature.",[1266,7547,7549],{"id":7548},"_23-components-are-a-projection-of-state",[22,7550,200],{"title":625,"href":199},[594,7552,7553],{},"A component's job is to render signals and forward user intent. That's the whole job.",[644,7555,7557],{"className":831,"code":7556,"language":833,"meta":653,"style":653},"@Component({\n  selector: 'app-carrier-list',\n  templateUrl: '.\u002Fcarrier-list.component.html',\n  styleUrl: '.\u002Fcarrier-list.component.scss',\n  changeDetection: ChangeDetectionStrategy.OnPush, \u002F\u002F redundant on v22+, required on v20\u002Fv21\n  imports: [MatButton, BarMeterComponent],\n  providers: [CarriersStore],\n})\nexport class CarrierListComponent {\n  protected readonly store = inject(CarriersStore);\n\n  readonly range = input.required\u003CDateRange>();\n\n  \u002F** Bars scale against the busiest carrier in the current result set. *\u002F\n  protected readonly carriers = computed(() => {\n    const carriers = this.store.carriers();\n    const max = carriers.reduce((acc, carrier) => Math.max(acc, carrier.shipmentCount), 0);\n    return carriers.map((carrier) => ({\n      ...carrier,\n      percent: max > 0 ? Math.round((carrier.shipmentCount \u002F max) * 100) : 0,\n    }));\n  });\n\n  constructor() {\n    this.store.loadTopCarriers(this.range);\n  }\n\n  protected select(id: string): void {\n    this.store.selectCarrier(id);\n  }\n}\n",[651,7558,7559,7569,7580,7591,7603,7622,7643,7657,7663,7673,7691,7695,7719,7723,7728,7748,7771,7830,7854,7863,7913,7921,7929,7933,7941,7963,7967,7972,7996,8015,8020],{"__ignoreMap":653},[837,7560,7561,7563,7565,7567],{"class":839,"line":840},[837,7562,851],{"class":850},[837,7564,855],{"class":854},[837,7566,859],{"class":858},[837,7568,1095],{"class":862},[837,7570,7571,7574,7576,7578],{"class":839,"line":847},[837,7572,7573],{"class":858},"  selector",[837,7575,870],{"class":869},[837,7577,874],{"class":873},[837,7579,1387],{"class":862},[837,7581,7582,7585,7587,7589],{"class":839,"line":894},[837,7583,7584],{"class":858},"  templateUrl",[837,7586,870],{"class":869},[837,7588,885],{"class":873},[837,7590,1387],{"class":862},[837,7592,7593,7596,7598,7601],{"class":839,"line":917},[837,7594,7595],{"class":858},"  styleUrl",[837,7597,870],{"class":869},[837,7599,7600],{"class":873}," '.\u002Fcarrier-list.component.scss'",[837,7602,1387],{"class":862},[837,7604,7605,7608,7610,7613,7615,7617,7619],{"class":839,"line":944},[837,7606,7607],{"class":858},"  changeDetection",[837,7609,870],{"class":869},[837,7611,7612],{"class":858}," ChangeDetectionStrategy",[837,7614,1040],{"class":869},[837,7616,3361],{"class":937},[837,7618,877],{"class":862},[837,7620,7621],{"class":843}," \u002F\u002F redundant on v22+, required on v20\u002Fv21\n",[837,7623,7624,7627,7629,7631,7634,7636,7639,7641],{"class":839,"line":959},[837,7625,7626],{"class":858},"  imports",[837,7628,870],{"class":869},[837,7630,2000],{"class":858},[837,7632,7633],{"class":937},"MatButton",[837,7635,877],{"class":862},[837,7637,7638],{"class":937}," BarMeterComponent",[837,7640,2121],{"class":858},[837,7642,1387],{"class":862},[837,7644,7645,7647,7649,7651,7653,7655],{"class":839,"line":984},[837,7646,1363],{"class":858},[837,7648,870],{"class":869},[837,7650,2000],{"class":858},[837,7652,5017],{"class":937},[837,7654,2121],{"class":858},[837,7656,1387],{"class":862},[837,7658,7659,7661],{"class":839,"line":991},[837,7660,3505],{"class":862},[837,7662,891],{"class":858},[837,7664,7665,7667,7669,7671],{"class":839,"line":1017},[837,7666,898],{"class":897},[837,7668,901],{"class":897},[837,7670,905],{"class":904},[837,7672,914],{"class":862},[837,7674,7675,7678,7680,7682,7684,7686,7689],{"class":839,"line":1022},[837,7676,7677],{"class":897},"  protected",[837,7679,2526],{"class":897},[837,7681,4896],{"class":920},[837,7683,950],{"class":924},[837,7685,2718],{"class":854},[837,7687,7688],{"class":937},"(CarriersStore)",[837,7690,941],{"class":862},[837,7692,7693],{"class":839,"line":1033},[837,7694,988],{"emptyLinePlaceholder":987},[837,7696,7697,7699,7701,7703,7705,7707,7709,7711,7713,7715,7717],{"class":839,"line":1053},[837,7698,2363],{"class":897},[837,7700,3774],{"class":920},[837,7702,950],{"class":924},[837,7704,2371],{"class":937},[837,7706,1040],{"class":869},[837,7708,2376],{"class":854},[837,7710,1069],{"class":1068},[837,7712,6166],{"class":904},[837,7714,1078],{"class":1068},[837,7716,1028],{"class":937},[837,7718,941],{"class":862},[837,7720,7721],{"class":839,"line":1098},[837,7722,988],{"emptyLinePlaceholder":987},[837,7724,7725],{"class":839,"line":1151},[837,7726,7727],{"class":843},"  \u002F** Bars scale against the busiest carrier in the current result set. *\u002F\n",[837,7729,7730,7732,7734,7736,7738,7740,7742,7744,7746],{"class":839,"line":1203},[837,7731,7677],{"class":897},[837,7733,2526],{"class":897},[837,7735,1130],{"class":920},[837,7737,950],{"class":924},[837,7739,4887],{"class":854},[837,7741,859],{"class":937},[837,7743,1028],{"class":862},[837,7745,1114],{"class":924},[837,7747,914],{"class":862},[837,7749,7750,7753,7755,7757,7759,7761,7763,7765,7767,7769],{"class":839,"line":1213},[837,7751,7752],{"class":897},"    const",[837,7754,1130],{"class":1348},[837,7756,950],{"class":924},[837,7758,1120],{"class":1036},[837,7760,1040],{"class":869},[837,7762,4552],{"class":937},[837,7764,1040],{"class":869},[837,7766,1109],{"class":854},[837,7768,1028],{"class":937},[837,7770,941],{"class":862},[837,7772,7773,7775,7778,7780,7782,7784,7787,7789,7791,7794,7796,7798,7800,7802,7805,7807,7810,7813,7815,7817,7819,7822,7824,7826,7828],{"class":839,"line":1219},[837,7774,7752],{"class":897},[837,7776,7777],{"class":1348}," max",[837,7779,950],{"class":924},[837,7781,1130],{"class":937},[837,7783,1040],{"class":869},[837,7785,7786],{"class":854},"reduce",[837,7788,859],{"class":937},[837,7790,859],{"class":862},[837,7792,7793],{"class":1002},"acc",[837,7795,877],{"class":862},[837,7797,4962],{"class":1002},[837,7799,1011],{"class":862},[837,7801,1114],{"class":924},[837,7803,7804],{"class":937}," Math",[837,7806,1040],{"class":869},[837,7808,7809],{"class":854},"max",[837,7811,7812],{"class":937},"(acc",[837,7814,877],{"class":862},[837,7816,4962],{"class":937},[837,7818,1040],{"class":869},[837,7820,7821],{"class":937},"shipmentCount)",[837,7823,877],{"class":862},[837,7825,4913],{"class":953},[837,7827,1011],{"class":937},[837,7829,941],{"class":862},[837,7831,7832,7834,7836,7838,7840,7842,7844,7846,7848,7850,7852],{"class":839,"line":3975},[837,7833,3801],{"class":897},[837,7835,1130],{"class":937},[837,7837,1040],{"class":869},[837,7839,3903],{"class":854},[837,7841,859],{"class":937},[837,7843,859],{"class":862},[837,7845,4955],{"class":1002},[837,7847,1011],{"class":862},[837,7849,1114],{"class":924},[837,7851,1106],{"class":937},[837,7853,1095],{"class":862},[837,7855,7856,7859,7861],{"class":839,"line":4039},[837,7857,7858],{"class":924},"      ...",[837,7860,4955],{"class":937},[837,7862,1387],{"class":862},[837,7864,7865,7868,7870,7873,7875,7877,7879,7881,7883,7886,7889,7891,7894,7896,7899,7902,7905,7907,7909,7911],{"class":839,"line":4507},[837,7866,7867],{"class":937},"      percent",[837,7869,870],{"class":869},[837,7871,7872],{"class":937}," max ",[837,7874,1078],{"class":924},[837,7876,4913],{"class":953},[837,7878,5927],{"class":924},[837,7880,7804],{"class":937},[837,7882,1040],{"class":869},[837,7884,7885],{"class":854},"round",[837,7887,7888],{"class":937},"((carrier",[837,7890,1040],{"class":869},[837,7892,7893],{"class":937},"shipmentCount ",[837,7895,3026],{"class":924},[837,7897,7898],{"class":937}," max) ",[837,7900,7901],{"class":924},"*",[837,7903,7904],{"class":953}," 100",[837,7906,2114],{"class":937},[837,7908,870],{"class":924},[837,7910,4913],{"class":953},[837,7912,1387],{"class":862},[837,7914,7915,7917,7919],{"class":839,"line":4520},[837,7916,1206],{"class":862},[837,7918,4567],{"class":937},[837,7920,941],{"class":862},[837,7922,7923,7925,7927],{"class":839,"line":4542},[837,7924,5172],{"class":862},[837,7926,1011],{"class":937},[837,7928,941],{"class":862},[837,7930,7931],{"class":839,"line":4575},[837,7932,988],{"emptyLinePlaceholder":987},[837,7934,7935,7937,7939],{"class":839,"line":4606},[837,7936,994],{"class":897},[837,7938,1028],{"class":862},[837,7940,914],{"class":862},[837,7942,7943,7945,7947,7949,7951,7953,7955,7957,7959,7961],{"class":839,"line":4627},[837,7944,1037],{"class":1036},[837,7946,1040],{"class":869},[837,7948,4552],{"class":937},[837,7950,1040],{"class":869},[837,7952,4101],{"class":854},[837,7954,859],{"class":937},[837,7956,2317],{"class":1036},[837,7958,1040],{"class":869},[837,7960,6605],{"class":937},[837,7962,941],{"class":862},[837,7964,7965],{"class":839,"line":4645},[837,7966,1216],{"class":862},[837,7968,7970],{"class":839,"line":7969},27,[837,7971,988],{"emptyLinePlaceholder":987},[837,7973,7975,7977,7980,7982,7984,7986,7988,7990,7992,7994],{"class":839,"line":7974},28,[837,7976,7677],{"class":897},[837,7978,7979],{"class":854}," select",[837,7981,859],{"class":862},[837,7983,3992],{"class":1002},[837,7985,870],{"class":924},[837,7987,968],{"class":967},[837,7989,1011],{"class":862},[837,7991,870],{"class":924},[837,7993,2636],{"class":967},[837,7995,914],{"class":862},[837,7997,7999,8001,8003,8005,8007,8010,8013],{"class":839,"line":7998},29,[837,8000,1037],{"class":1036},[837,8002,1040],{"class":869},[837,8004,4552],{"class":937},[837,8006,1040],{"class":869},[837,8008,8009],{"class":854},"selectCarrier",[837,8011,8012],{"class":937},"(id)",[837,8014,941],{"class":862},[837,8016,8018],{"class":839,"line":8017},30,[837,8019,1216],{"class":862},[837,8021,8023],{"class":839,"line":8022},31,[837,8024,1222],{"class":862},[594,8026,8027,8028,8030,8032,8033,8036,8037,8039],{},"That constructor line is the piece agents most reliably reinvent badly. Passing the ",[651,8029,6189],{},[603,8031,678],{}," — not ",[651,8034,8035],{},"range()"," — is what makes the fetch reactive, and it replaces the ",[651,8038,692],{},"\nan agent writes unprompted.",[594,8041,8042],{},[597,8043,8044],{},"Component rules",[658,8046,8047,8067,8076,8092,8103,8115,8124,8139,8153,8168],{},[18,8048,3358,8049,8051,8052,8055,8056,8059,8060,8063,8064,3229],{},[651,8050,3361],{},". It's the ",[597,8053,8054],{},"default in v22+"," — don't write it there. On\nv20\u002Fv21 set ",[651,8057,8058],{},"changeDetection: ChangeDetectionStrategy.OnPush"," explicitly. Never opt out with\n",[651,8061,8062],{},"ChangeDetectionStrategy.Eager"," (or the deprecated ",[651,8065,8066],{},"Default",[18,8068,8069,8075],{},[597,8070,8071,8072,1040],{},"Do not write ",[651,8073,8074],{},"standalone: true"," It's the default since v19 and an error to add in v20+.",[18,8077,8078,8083,8084,8087,8088,8091],{},[597,8079,4076,8080,1040],{},[651,8081,8082],{},".subscribe()"," The one pragmatic exception is dialog results — ",[651,8085,8086],{},"afterClosed()"," is a\nsingle-emission Observable tied to a user action, and routing it through a store is more\nceremony than clarity. Take the exception knowingly; don't let it become a precedent. If you\ngenuinely must subscribe, ",[651,8089,8090],{},"takeUntilDestroyed()"," is mandatory.",[18,8093,8094,2304,8099,8102],{},[597,8095,8096,8097,1040],{},"No pass-through ",[651,8098,682],{},[651,8100,8101],{},"readonly items = computed(() => this.store.items())"," adds a\ngraph node and zero information. Use the store signal directly in the template.",[18,8104,8105,8108,8109,8111,8112,8114],{},[597,8106,8107],{},"Keep components small and single-responsibility."," A component doing layout ",[603,8110,4135],{},"\nfiltering ",[603,8113,4135],{}," export is three components.",[18,8116,8117,8120,8121,1040],{},[597,8118,8119],{},"Prefer inline templates for small components","; external files once the template earns it.\nWhen external, use paths relative to the component's ",[651,8122,8123],{},".ts",[18,8125,8126,8032,8133,2973,8136,1040],{},[597,8127,8128,8129,8132],{},"Host bindings in the ",[651,8130,8131],{},"host"," metadata object",[651,8134,8135],{},"@HostBinding",[651,8137,8138],{},"@HostListener",[18,8140,8141,675,8144,4102,8146,8149,8150,1040],{},[597,8142,8143],{},"Import standalone components, not their modules",[651,8145,7633],{},[651,8147,8148],{},"MatButtonModule",".\nModules pull in the whole barrel. No unused entries in ",[651,8151,8152],{},"imports",[18,8154,8155,679,8161,8163,8164,8167],{},[597,8156,8157,8160],{},[651,8158,8159],{},"protected"," for template-only members",[651,8162,999],{}," for internals, ",[651,8165,8166],{},"readonly"," on injected\ndependencies and signals. This makes the template's actual API obvious.",[18,8169,8170,8180,8181,8184],{},[597,8171,4076,8172,8175,8176,8179],{},[651,8173,8174],{},"any",", no ",[651,8177,8178],{},"as"," casts to silence a mismatch."," A cast that makes an error go away has\nmoved it to runtime. Prefer ",[651,8182,8183],{},"unknown"," and narrow.",[594,8186,8187,8188,8190,8191,8194,8195,8198,8199,8202],{},"On naming: the current Angular style guide drops the ",[651,8189,855],{}," suffix (",[651,8192,8193],{},"CarrierList",", not\n",[651,8196,8197],{},"CarrierListComponent","), while a great many existing codebases keep it. Either is fine —\n",[597,8200,8201],{},"pick one and enforce it in lint."," The cost is inconsistency, not the choice.",[1266,8204,8206],{"id":8205},"_24-the-small-component-escape-hatch-rxresource",[22,8207,8208,8209],{"title":625,"href":205},"2.4 The small-component escape hatch: ",[651,8210,668],{},[594,8212,8213,8214,8216,8217,8219],{},"Not every component needs a store. If a component's ",[603,8215,3664],{}," data story is \"fetch one thing by\nid, show a spinner while it loads, show an error if it fails,\" a store is real ceremony for no\nreal benefit. Angular's ",[651,8218,668],{}," covers that case directly:",[644,8221,8223],{"className":831,"code":8222,"language":833,"meta":653,"style":653},"@Component({\n  selector: 'app-carrier-detail',\n  templateUrl: '.\u002Fcarrier-detail.component.html',\n  changeDetection: ChangeDetectionStrategy.OnPush, \u002F\u002F redundant on v22+, required on v20\u002Fv21\n})\nexport class CarrierDetailComponent {\n  private readonly carriersApi = inject(CarriersApiService);\n\n  readonly carrierId = input.required\u003Cstring>();   \u002F\u002F bound from the route\n\n  \u002F\u002F Re-fetches when carrierId changes; cancels the superseded request.\n  protected readonly carrier = rxResource({\n    params: () => this.carrierId(),\n    stream: ({ params: id }) => this.carriersApi.loadCarrier(id),\n  });\n}\n",[651,8224,8225,8235,8246,8257,8273,8279,8290,8306,8310,8337,8341,8346,8363,8385,8421,8429],{"__ignoreMap":653},[837,8226,8227,8229,8231,8233],{"class":839,"line":840},[837,8228,851],{"class":850},[837,8230,855],{"class":854},[837,8232,859],{"class":858},[837,8234,1095],{"class":862},[837,8236,8237,8239,8241,8244],{"class":839,"line":847},[837,8238,7573],{"class":858},[837,8240,870],{"class":869},[837,8242,8243],{"class":873}," 'app-carrier-detail'",[837,8245,1387],{"class":862},[837,8247,8248,8250,8252,8255],{"class":839,"line":894},[837,8249,7584],{"class":858},[837,8251,870],{"class":869},[837,8253,8254],{"class":873}," '.\u002Fcarrier-detail.component.html'",[837,8256,1387],{"class":862},[837,8258,8259,8261,8263,8265,8267,8269,8271],{"class":839,"line":917},[837,8260,7607],{"class":858},[837,8262,870],{"class":869},[837,8264,7612],{"class":858},[837,8266,1040],{"class":869},[837,8268,3361],{"class":937},[837,8270,877],{"class":862},[837,8272,7621],{"class":843},[837,8274,8275,8277],{"class":839,"line":944},[837,8276,3505],{"class":862},[837,8278,891],{"class":858},[837,8280,8281,8283,8285,8288],{"class":839,"line":959},[837,8282,898],{"class":897},[837,8284,901],{"class":897},[837,8286,8287],{"class":904}," CarrierDetailComponent",[837,8289,914],{"class":862},[837,8291,8292,8294,8296,8298,8300,8302,8304],{"class":839,"line":984},[837,8293,2523],{"class":897},[837,8295,2526],{"class":897},[837,8297,6139],{"class":920},[837,8299,950],{"class":924},[837,8301,2718],{"class":854},[837,8303,5108],{"class":937},[837,8305,941],{"class":862},[837,8307,8308],{"class":839,"line":991},[837,8309,988],{"emptyLinePlaceholder":987},[837,8311,8312,8314,8316,8318,8320,8322,8324,8326,8328,8330,8332,8334],{"class":839,"line":1017},[837,8313,2363],{"class":897},[837,8315,2366],{"class":920},[837,8317,950],{"class":924},[837,8319,2371],{"class":937},[837,8321,1040],{"class":869},[837,8323,2376],{"class":854},[837,8325,1069],{"class":1068},[837,8327,2381],{"class":967},[837,8329,1078],{"class":1068},[837,8331,1028],{"class":937},[837,8333,1133],{"class":862},[837,8335,8336],{"class":843},"   \u002F\u002F bound from the route\n",[837,8338,8339],{"class":839,"line":1022},[837,8340,988],{"emptyLinePlaceholder":987},[837,8342,8343],{"class":839,"line":1033},[837,8344,8345],{"class":843},"  \u002F\u002F Re-fetches when carrierId changes; cancels the superseded request.\n",[837,8347,8348,8350,8352,8354,8356,8359,8361],{"class":839,"line":1053},[837,8349,7677],{"class":897},[837,8351,2526],{"class":897},[837,8353,4962],{"class":920},[837,8355,950],{"class":924},[837,8357,8358],{"class":854}," rxResource",[837,8360,859],{"class":937},[837,8362,1095],{"class":862},[837,8364,8365,8368,8370,8372,8374,8376,8378,8381,8383],{"class":839,"line":1098},[837,8366,8367],{"class":854},"    params",[837,8369,870],{"class":869},[837,8371,1976],{"class":862},[837,8373,1114],{"class":924},[837,8375,1120],{"class":1036},[837,8377,1040],{"class":869},[837,8379,8380],{"class":854},"carrierId",[837,8382,1028],{"class":937},[837,8384,1387],{"class":862},[837,8386,8387,8390,8392,8395,8398,8400,8402,8404,8406,8408,8410,8413,8415,8417,8419],{"class":839,"line":1151},[837,8388,8389],{"class":854},"    stream",[837,8391,870],{"class":869},[837,8393,8394],{"class":862}," ({",[837,8396,8397],{"class":920}," params",[837,8399,870],{"class":862},[837,8401,3982],{"class":1002},[837,8403,5813],{"class":862},[837,8405,1114],{"class":924},[837,8407,1120],{"class":1036},[837,8409,1040],{"class":869},[837,8411,8412],{"class":937},"carriersApi",[837,8414,1040],{"class":869},[837,8416,5595],{"class":854},[837,8418,8012],{"class":937},[837,8420,1387],{"class":862},[837,8422,8423,8425,8427],{"class":839,"line":1203},[837,8424,5172],{"class":862},[837,8426,1011],{"class":937},[837,8428,941],{"class":862},[837,8430,8431],{"class":839,"line":1213},[837,8432,1222],{"class":862},[644,8434,8436],{"className":2828,"code":8435,"language":2830,"meta":653,"style":653},"@if (carrier.isLoading()) {\n  \u003Capp-spinner \u002F>\n} @else if (carrier.error(); as error) {\n  \u003Capp-error-banner [message]=\"error.message\" \u002F>\n} @else if (carrier.hasValue()) {\n  \u003Capp-carrier-card [carrier]=\"carrier.value()\" \u002F>\n}\n",[651,8437,8438,8443,8451,8456,8472,8477,8494],{"__ignoreMap":653},[837,8439,8440],{"class":839,"line":840},[837,8441,8442],{"class":937},"@if (carrier.isLoading()) {\n",[837,8444,8445,8447,8449],{"class":839,"line":847},[837,8446,2842],{"class":869},[837,8448,2897],{"class":2845},[837,8450,2858],{"class":869},[837,8452,8453],{"class":839,"line":894},[837,8454,8455],{"class":937},"} @else if (carrier.error(); as error) {\n",[837,8457,8458,8460,8462,8465,8467,8470],{"class":839,"line":917},[837,8459,2842],{"class":869},[837,8461,2911],{"class":2845},[837,8463,8464],{"class":2849}," [message]",[837,8466,934],{"class":869},[837,8468,8469],{"class":873},"\"error.message\"",[837,8471,2858],{"class":869},[837,8473,8474],{"class":839,"line":944},[837,8475,8476],{"class":937},"} @else if (carrier.hasValue()) {\n",[837,8478,8479,8481,8484,8487,8489,8492],{"class":839,"line":959},[837,8480,2842],{"class":869},[837,8482,8483],{"class":2845},"app-carrier-card",[837,8485,8486],{"class":2849}," [carrier]",[837,8488,934],{"class":869},[837,8490,8491],{"class":873},"\"carrier.value()\"",[837,8493,2858],{"class":869},[837,8495,8496],{"class":839,"line":984},[837,8497,1222],{"class":937},[594,8499,8500,8502,8503,679,8506,679,8509,679,8512,679,8515,8518,8519,8522,8523,8526,8527,2304,8530,8532,8533,8535,8536,8538],{},[651,8501,668],{}," gives you ",[651,8504,8505],{},"value()",[651,8507,8508],{},"status()",[651,8510,8511],{},"isLoading()",[651,8513,8514],{},"error()",[651,8516,8517],{},"hasValue()",", and\n",[651,8520,8521],{},"reload()"," — the same shape a hand-rolled ",[651,8524,8525],{},"withCallState"," would, without the store. It takes a\n",[651,8528,8529],{},"params",[603,8531,3952],{},", so it is reactive by construction: the same \"pass a signal, get automatic\nre-fetch and cancellation\" idea as ",[651,8534,702],{},", packaged for the single-fetch case. There is\nstill no ",[651,8537,692],{}," anywhere.",[594,8540,8541,8542,8544,8545,8547,8548,8551,8552,8555,8556,8559,8560,8563,8564,8567,8568,8570,8571,3026,8573,8575,8576,3026,8579,8581,8582,8584,8585,8588,8589,8592],{},"Two footnotes. ",[651,8543,668],{}," lives in ",[651,8546,3524],{}," and is for services returning\nObservables; plain ",[651,8549,8550],{},"resource"," from ",[651,8553,8554],{},"@angular\u002Fcore"," is the Promise-based equivalent, taking a\n",[651,8557,8558],{},"loader"," instead of a ",[651,8561,8562],{},"stream"," and receiving an ",[651,8565,8566],{},"abortSignal"," you should forward to ",[651,8569,1613],{},".\nOn versions: the option names are ",[651,8572,8529],{},[651,8574,8562],{}," (the older ",[651,8577,8578],{},"request",[651,8580,8558],{}," pair is gone),\nand ",[651,8583,668],{}," became ",[597,8586,8587],{},"stable in v22.0"," after being experimental through v21. Stability\nremoves the churn risk but not the architectural argument — keep it confined to small cases and\nlet the store carry anything load-bearing. v22 also adds ",[651,8590,8591],{},"chain()"," for wiring one resource's\noutput into another's params.",[4325,8594,8596],{"id":8595},"when-to-graduate-to-a-store",[22,8597,214],{"title":625,"href":213},[594,8599,8600,8602,8603,8606],{},[651,8601,668],{}," is a legitimate tool with a narrow window. Use it only when ",[603,8604,8605],{},"all"," of these hold:",[658,8608,8609,8612,8615,8621],{},[18,8610,8611],{},"one read, no writes or mutations;",[18,8613,8614],{},"no state beyond the fetched value and its status;",[18,8616,8617,8618,8620],{},"the fetch is keyed by one signal (or one trivial ",[651,8619,682],{},");",[18,8622,8623],{},"nothing else in the app needs to observe this data.",[594,8625,8626],{},"The moment any of those stops being true, move to a signal store:",[1645,8628,8629,8642],{},[1648,8630,8631],{},[1651,8632,8633,8636],{},[1654,8634,8635],{},"Signal",[1654,8637,8638,8639,8641],{},"Why ",[651,8640,668],{}," stops paying off",[1664,8643,8644,8652,8663,8674,8682,8695],{},[1651,8645,8646,8649],{},[1669,8647,8648],{},"A second fetch appears",[1669,8650,8651],{},"Two resources with no coordination; nowhere for cross-cutting derived state",[1651,8653,8654,8657],{},[1669,8655,8656],{},"The component writes anything back",[1669,8658,8659,8660,8662],{},"Mutations need an orchestrated pipeline (",[651,8661,6499],{},", optimistic update, refetch)",[1651,8664,8665,8668],{},[1669,8666,8667],{},"Selection, filters, sorting, or pagination arrive",[1669,8669,8670,8671],{},"That's feature state; it belongs in ",[651,8672,8673],{},"withState",[1651,8675,8676,8679],{},[1669,8677,8678],{},"Another component needs the same data",[1669,8680,8681],{},"Nothing to inject — you'd fetch twice",[1651,8683,8684,8687],{},[1669,8685,8686],{},"Polling, debouncing, or retry is needed",[1669,8688,8689,8691,8692,8694],{},[651,8690,702],{}," composes RxJS naturally; ",[651,8693,668],{}," doesn't",[1651,8696,8697,8700],{},[1669,8698,8699],{},"You want the devtools timeline",[1669,8701,8702],{},"Resource mutations aren't described actions",[594,8704,8705,8706,8708,8709,8711,8712,8714,8715,8717,8718,8721,8722,8724,8725,8727],{},"The failure mode to watch for is the slow slide: an ",[651,8707,668],{}," plus a ",[651,8710,678],{}," for selection,\nplus another ",[651,8713,678],{}," for a filter, plus a ",[651,8716,682],{}," stitching them together, plus a second\nresource — at which point you have a store with none of a store's structure, scattered across\na component. ",[597,8719,8720],{},"Refactor at the second concern, not the fifth."," Migrating one ",[651,8723,668],{}," to\none ",[651,8726,702],{}," is a ten-minute change; unpicking six component signals later is not.",[617,8729],{},[620,8731,8733],{"id":8732},"part-3-reactivity-with-signals",[22,8734,220],{"title":625,"href":219},[594,8736,8737],{},"Part 2 covered where reactive code lives. This part covers the primitives themselves, because\nmost bad signal code is a wrong pick from a small menu.",[1266,8739,8741],{"id":8740},"_31-the-decision-table",[22,8742,228],{"title":625,"href":227},[594,8744,8745],{},"Hand your agent this table before anything else in this section.",[1645,8747,8748,8761],{},[1648,8749,8750],{},[1651,8751,8752,8755,8758],{},[1654,8753,8754],{},"You need",[1654,8756,8757],{},"Use",[1654,8759,8760],{},"Notes",[1664,8762,8763,8786,8799,8816,8830,8842,8859,8877,8889,8908,8926,8944,8956,8972,8984,9004],{},[1651,8764,8765,8768,8773],{},[1669,8766,8767],{},"Local writable state",[1669,8769,8770],{},[651,8771,8772],{},"signal()",[1669,8774,8775,2973,8777,8779,8780,2304,8783],{},[651,8776,2649],{},[651,8778,5556],{},". ",[597,8781,8782],{},"Never",[651,8784,8785],{},"mutate",[1651,8787,8788,8791,8796],{},[1669,8789,8790],{},"A value derived from other signals",[1669,8792,8793],{},[651,8794,8795],{},"computed()",[1669,8797,8798],{},"Pure, lazy, memoized",[1651,8800,8801,8808,8813],{},[1669,8802,8803,8804,8807],{},"Writable state that ",[603,8805,8806],{},"resets"," when a source changes",[1669,8809,8810],{},[651,8811,8812],{},"linkedSignal()",[1669,8814,8815],{},"The most underused API in Angular",[1651,8817,8818,8821,8827],{},[1669,8819,8820],{},"Async data fetched in reaction to signals",[1669,8822,8823,8824,8826],{},"store ",[651,8825,702],{},", passed the signal",[1669,8828,8829],{},"Cancellation + call state for free",[1651,8831,8832,8835,8839],{},[1669,8833,8834],{},"A single read-only fetch keyed by one signal",[1669,8836,8837],{},[651,8838,668],{},[1669,8840,8841],{},"Trivial cases only (§2.4). Stable as of v22.0",[1651,8843,8844,8847,8852],{},[1669,8845,8846],{},"To trigger async work when a signal changes",[1669,8848,8849,8850],{},"pass the signal to ",[651,8851,702],{},[1669,8853,8854,8856,8857],{},[597,8855,8782],{}," an ",[651,8858,692],{},[1651,8860,8861,8864,8871],{},[1669,8862,8863],{},"Component inputs",[1669,8865,8866,2973,8868],{},[651,8867,1594],{},[651,8869,8870],{},"input.required()",[1669,8872,8873,8874],{},"Not ",[651,8875,8876],{},"@Input()",[1651,8878,8879,8882,8887],{},[1669,8880,8881],{},"Two-way bound input",[1669,8883,8884],{},[651,8885,8886],{},"model()",[1669,8888],{},[1651,8890,8891,8894,8899],{},[1669,8892,8893],{},"Component outputs",[1669,8895,8896],{},[651,8897,8898],{},"output()",[1669,8900,8873,8901,8904,8905],{},[651,8902,8903],{},"@Output()"," + ",[651,8906,8907],{},"EventEmitter",[1651,8909,8910,8913,8921],{},[1669,8911,8912],{},"A reference to a child element\u002Fcomponent",[1669,8914,8915,2973,8918],{},[651,8916,8917],{},"viewChild()",[651,8919,8920],{},"viewChildren()",[1669,8922,8873,8923],{},[651,8924,8925],{},"@ViewChild",[1651,8927,8928,8931,8939],{},[1669,8929,8930],{},"A reference to projected content",[1669,8932,8933,2973,8936],{},[651,8934,8935],{},"contentChild()",[651,8937,8938],{},"contentChildren()",[1669,8940,8873,8941],{},[651,8942,8943],{},"@ContentChild",[1651,8945,8946,8949,8954],{},[1669,8947,8948],{},"To read a signal without depending on it",[1669,8950,8951],{},[651,8952,8953],{},"untracked()",[1669,8955],{},[1651,8957,8958,8961,8966],{},[1669,8959,8960],{},"To consume an Observable as a signal",[1669,8962,8963],{},[651,8964,8965],{},"toSignal()",[1669,8967,8968,8969],{},"Needs ",[651,8970,8971],{},"initialValue",[1651,8973,8974,8977,8982],{},[1669,8975,8976],{},"To expose a signal as an Observable",[1669,8978,8979],{},[651,8980,8981],{},"toObservable()",[1669,8983],{},[1651,8985,8986,8989,8997],{},[1669,8987,8988],{},"To touch the DOM after render",[1669,8990,8991,2973,8994],{},[651,8992,8993],{},"afterNextRender()",[651,8995,8996],{},"afterRenderEffect()",[1669,8998,8999,2304,9002],{},[597,9000,9001],{},"Not",[651,9003,692],{},[1651,9005,9006,9009,9013],{},[1669,9007,9008],{},"To sync to a non-signal API (analytics, localStorage, canvas)",[1669,9010,9011],{},[651,9012,692],{},[1669,9014,9015],{},"Last resort. Justify in a comment",[1266,9017,9019],{"id":9018},"_32-signal-writable-state",[22,9020,9021,9022,9024],{"title":625,"href":233},"3.2 ",[651,9023,678],{}," — writable state",[644,9026,9028],{"className":831,"code":9027,"language":833,"meta":653,"style":653},"const count = signal(0);\n\ncount();                       \u002F\u002F read — always call the getter\ncount.set(3);                  \u002F\u002F replace\ncount.update((value) => value + 1);   \u002F\u002F derive from previous\n",[651,9029,9030,9049,9053,9065,9085],{"__ignoreMap":653},[837,9031,9032,9034,9037,9039,9041,9043,9045,9047],{"class":839,"line":840},[837,9033,3442],{"class":897},[837,9035,9036],{"class":1348}," count",[837,9038,950],{"class":924},[837,9040,2534],{"class":854},[837,9042,859],{"class":937},[837,9044,5492],{"class":953},[837,9046,1011],{"class":937},[837,9048,941],{"class":862},[837,9050,9051],{"class":839,"line":847},[837,9052,988],{"emptyLinePlaceholder":987},[837,9054,9055,9058,9060,9062],{"class":839,"line":894},[837,9056,9057],{"class":854},"count",[837,9059,1028],{"class":937},[837,9061,1133],{"class":862},[837,9063,9064],{"class":843},"                       \u002F\u002F read — always call the getter\n",[837,9066,9067,9069,9071,9073,9075,9078,9080,9082],{"class":839,"line":917},[837,9068,9057],{"class":937},[837,9070,1040],{"class":869},[837,9072,2649],{"class":854},[837,9074,859],{"class":937},[837,9076,9077],{"class":953},"3",[837,9079,1011],{"class":937},[837,9081,1133],{"class":862},[837,9083,9084],{"class":843},"                  \u002F\u002F replace\n",[837,9086,9087,9089,9091,9093,9095,9097,9100,9102,9104,9107,9109,9111,9113,9115],{"class":839,"line":944},[837,9088,9057],{"class":937},[837,9090,1040],{"class":869},[837,9092,5556],{"class":854},[837,9094,859],{"class":937},[837,9096,859],{"class":862},[837,9098,9099],{"class":1002},"value",[837,9101,1011],{"class":862},[837,9103,1114],{"class":924},[837,9105,9106],{"class":937}," value ",[837,9108,5573],{"class":924},[837,9110,5576],{"class":953},[837,9112,1011],{"class":937},[837,9114,1133],{"class":862},[837,9116,9117],{"class":843},"   \u002F\u002F derive from previous\n",[594,9119,9120],{},"Three things to internalise:",[594,9122,9123,9128,9129,2304,9132,9134],{},[597,9124,9125,9126,1040],{},"Never ",[651,9127,8785],{}," It doesn't exist any more, and mutating an object ",[603,9130,9131],{},"inside",[651,9133,5556],{}," without\nreturning a new reference defeats change detection, because signal equality is reference-based\nby default:",[644,9136,9138],{"className":831,"code":9137,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ same reference — dependents may not be notified\nitems.update((list) => { list.push(newItem); return list; });\n\n\u002F\u002F ✅ new reference\nitems.update((list) => [...list, newItem]);\n",[651,9139,9140,9145,9192,9196,9201],{"__ignoreMap":653},[837,9141,9142],{"class":839,"line":840},[837,9143,9144],{"class":843},"\u002F\u002F ❌ same reference — dependents may not be notified\n",[837,9146,9147,9150,9152,9154,9156,9158,9161,9163,9165,9167,9170,9172,9175,9178,9180,9182,9184,9186,9188,9190],{"class":839,"line":847},[837,9148,9149],{"class":937},"items",[837,9151,1040],{"class":869},[837,9153,5556],{"class":854},[837,9155,859],{"class":937},[837,9157,859],{"class":862},[837,9159,9160],{"class":1002},"list",[837,9162,1011],{"class":862},[837,9164,1114],{"class":924},[837,9166,1117],{"class":862},[837,9168,9169],{"class":937}," list",[837,9171,1040],{"class":869},[837,9173,9174],{"class":854},"push",[837,9176,9177],{"class":937},"(newItem)",[837,9179,1133],{"class":862},[837,9181,2585],{"class":897},[837,9183,9169],{"class":937},[837,9185,1133],{"class":862},[837,9187,888],{"class":862},[837,9189,1011],{"class":937},[837,9191,941],{"class":862},[837,9193,9194],{"class":839,"line":894},[837,9195,988],{"emptyLinePlaceholder":987},[837,9197,9198],{"class":839,"line":917},[837,9199,9200],{"class":843},"\u002F\u002F ✅ new reference\n",[837,9202,9203,9205,9207,9209,9211,9213,9215,9217,9219,9221,9224,9226,9228,9231],{"class":839,"line":944},[837,9204,9149],{"class":937},[837,9206,1040],{"class":869},[837,9208,5556],{"class":854},[837,9210,859],{"class":937},[837,9212,859],{"class":862},[837,9214,9160],{"class":1002},[837,9216,1011],{"class":862},[837,9218,1114],{"class":924},[837,9220,2000],{"class":937},[837,9222,9223],{"class":924},"...",[837,9225,9160],{"class":937},[837,9227,877],{"class":862},[837,9229,9230],{"class":937}," newItem])",[837,9232,941],{"class":862},[594,9234,9235,9238],{},[597,9236,9237],{},"Expose readonly state from services and stores."," A public writable signal is a public\nsetter for your internal state:",[644,9240,9242],{"className":831,"code":9241,"language":833,"meta":653,"style":653},"private readonly _count = signal(0);\nreadonly count = this._count.asReadonly();\n",[651,9243,9244,9261],{"__ignoreMap":653},[837,9245,9246,9249,9251,9253,9255,9257,9259],{"class":839,"line":840},[837,9247,9248],{"class":937},"private readonly _count ",[837,9250,934],{"class":924},[837,9252,2534],{"class":854},[837,9254,859],{"class":937},[837,9256,5492],{"class":953},[837,9258,1011],{"class":937},[837,9260,941],{"class":862},[837,9262,9263,9266,9268,9270,9272,9275,9277,9280,9282],{"class":839,"line":847},[837,9264,9265],{"class":937},"readonly count ",[837,9267,934],{"class":924},[837,9269,1120],{"class":1036},[837,9271,1040],{"class":869},[837,9273,9274],{"class":937},"_count",[837,9276,1040],{"class":869},[837,9278,9279],{"class":854},"asReadonly",[837,9281,1028],{"class":937},[837,9283,941],{"class":862},[594,9285,9286,9293],{},[597,9287,9288,9289,9292],{},"Use a custom ",[651,9290,9291],{},"equal"," for expensive derivations."," If a signal holds a structure that is\noften recomputed to an equivalent value, a comparator prevents downstream churn:",[644,9295,9297],{"className":831,"code":9296,"language":833,"meta":653,"style":653},"readonly filters = signal(defaultFilters, { equal: (a, b) => a.key === b.key });\n",[651,9298,9299],{"__ignoreMap":653},[837,9300,9301,9304,9306,9308,9311,9313,9315,9318,9320,9322,9324,9326,9329,9331,9333,9336,9338,9341,9343,9345,9347,9349,9351,9353],{"class":839,"line":840},[837,9302,9303],{"class":937},"readonly filters ",[837,9305,934],{"class":924},[837,9307,2534],{"class":854},[837,9309,9310],{"class":937},"(defaultFilters",[837,9312,877],{"class":862},[837,9314,1117],{"class":862},[837,9316,9317],{"class":854}," equal",[837,9319,870],{"class":869},[837,9321,1106],{"class":862},[837,9323,22],{"class":1002},[837,9325,877],{"class":862},[837,9327,9328],{"class":1002}," b",[837,9330,1011],{"class":862},[837,9332,1114],{"class":924},[837,9334,9335],{"class":937}," a",[837,9337,1040],{"class":869},[837,9339,9340],{"class":937},"key ",[837,9342,4970],{"class":924},[837,9344,9328],{"class":937},[837,9346,1040],{"class":869},[837,9348,9340],{"class":937},[837,9350,3505],{"class":862},[837,9352,1011],{"class":937},[837,9354,941],{"class":862},[1266,9356,9358],{"id":9357},"_33-computed-derived-state",[22,9359,9360,9361,9363],{"title":625,"href":239},"3.3 ",[651,9362,682],{}," — derived state",[594,9365,9366,9368,9369,9372,9373,9376,9377,9380],{},[651,9367,682],{}," is ",[597,9370,9371],{},"lazy"," (the body doesn't run until something reads it), ",[597,9374,9375],{},"memoized"," (it only\nrecomputes when a tracked dependency actually changes), and has ",[597,9378,9379],{},"dynamic dependencies"," —\nonly the signals genuinely read on the last run are tracked, so a short-circuiting branch\ndoesn't subscribe to signals it skipped.",[644,9382,9384],{"className":831,"code":9383,"language":833,"meta":653,"style":653},"readonly visible = computed(() =>\n  this.carriers().filter((carrier) => carrier.risk >= this.threshold())\n);\n",[651,9385,9386,9401,9444],{"__ignoreMap":653},[837,9387,9388,9391,9393,9395,9397,9399],{"class":839,"line":840},[837,9389,9390],{"class":937},"readonly visible ",[837,9392,934],{"class":924},[837,9394,4887],{"class":854},[837,9396,859],{"class":937},[837,9398,1028],{"class":862},[837,9400,4932],{"class":924},[837,9402,9403,9405,9407,9409,9411,9413,9415,9417,9419,9421,9423,9425,9427,9429,9432,9435,9437,9439,9442],{"class":839,"line":847},[837,9404,3480],{"class":1036},[837,9406,1040],{"class":869},[837,9408,1109],{"class":854},[837,9410,1028],{"class":937},[837,9412,1040],{"class":869},[837,9414,5360],{"class":854},[837,9416,859],{"class":937},[837,9418,859],{"class":862},[837,9420,4955],{"class":1002},[837,9422,1011],{"class":862},[837,9424,1114],{"class":924},[837,9426,4962],{"class":937},[837,9428,1040],{"class":869},[837,9430,9431],{"class":937},"risk ",[837,9433,9434],{"class":924},">=",[837,9436,1120],{"class":1036},[837,9438,1040],{"class":869},[837,9440,9441],{"class":854},"threshold",[837,9443,4980],{"class":937},[837,9445,9446,9448],{"class":839,"line":894},[837,9447,1011],{"class":937},[837,9449,941],{"class":862},[594,9451,9452,9453,9456,9457,9459,9460,9463,9464,9467],{},"Rules: keep it ",[597,9454,9455],{},"pure"," — no writes, no I\u002FO, no ",[651,9458,3234],{},", no side effects. Keep it\n",[597,9461,9462],{},"cheap"," — see ",[22,9465,9466],{"href":507},"Part 9",". And don't write pass-through computeds.",[1266,9469,9471],{"id":9470},"_34-reactive-contexts-untracked-and-the-await-trap",[22,9472,9473,9474,9477,9478,9480],{"title":625,"href":245},"3.4 Reactive contexts, ",[651,9475,9476],{},"untracked",", and the ",[651,9479,2741],{}," trap",[594,9482,9483,9484,9487,9488,9490,9491,9493,9494,9496],{},"A ",[597,9485,9486],{},"reactive context"," is a runtime state where Angular records which signals you read, to\nbuild the dependency graph. Angular enters one when evaluating ",[651,9489,682],{}," bodies, ",[651,9492,688],{},"\ncallbacks, ",[651,9495,685],{}," computations, and component templates.",[594,9498,9499],{},"Two consequences agents routinely miss.",[594,9501,9502,9507],{},[597,9503,9504,9506],{},[651,9505,8953],{}," reads without depending."," Use it when a reactive context needs a value but\nshouldn't re-run when it changes:",[644,9509,9511],{"className":831,"code":9510,"language":833,"meta":653,"style":653},"effect(() => {\n  \u002F\u002F Re-runs when `user` changes — but not when `sessionId` does.\n  analytics.track('user_changed', user(), untracked(this.sessionId));\n});\n",[651,9512,9513,9525,9530,9568],{"__ignoreMap":653},[837,9514,9515,9517,9519,9521,9523],{"class":839,"line":840},[837,9516,688],{"class":854},[837,9518,859],{"class":937},[837,9520,1028],{"class":862},[837,9522,1114],{"class":924},[837,9524,914],{"class":862},[837,9526,9527],{"class":839,"line":847},[837,9528,9529],{"class":843},"  \u002F\u002F Re-runs when `user` changes — but not when `sessionId` does.\n",[837,9531,9532,9535,9537,9540,9542,9545,9547,9550,9552,9554,9557,9559,9561,9563,9566],{"class":839,"line":894},[837,9533,9534],{"class":937},"  analytics",[837,9536,1040],{"class":869},[837,9538,9539],{"class":854},"track",[837,9541,859],{"class":937},[837,9543,9544],{"class":873},"'user_changed'",[837,9546,877],{"class":862},[837,9548,9549],{"class":854}," user",[837,9551,1028],{"class":937},[837,9553,877],{"class":862},[837,9555,9556],{"class":854}," untracked",[837,9558,859],{"class":937},[837,9560,2317],{"class":1036},[837,9562,1040],{"class":869},[837,9564,9565],{"class":937},"sessionId))",[837,9567,941],{"class":862},[837,9569,9570,9572,9574],{"class":839,"line":917},[837,9571,3505],{"class":862},[837,9573,1011],{"class":937},[837,9575,941],{"class":862},[594,9577,9578,9583,9584,9586],{},[597,9579,9580,9581,1040],{},"Tracking stops at the first ",[651,9582,2741],{}," The reactive context is only active for synchronous\ncode, so a signal read after an ",[651,9585,2741],{}," is invisible to the graph:",[644,9588,9590],{"className":831,"code":9589,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ theme() is never tracked — read after the await\neffect(async () => {\n  const data = await fetchData();\n  applyTheme(theme(), data);\n});\n\n\u002F\u002F ✅ read the signal first, then await\neffect(async () => {\n  const currentTheme = theme();\n  const data = await fetchData();\n  applyTheme(currentTheme, data);\n});\n",[651,9591,9592,9597,9611,9630,9649,9657,9661,9666,9680,9696,9712,9725],{"__ignoreMap":653},[837,9593,9594],{"class":839,"line":840},[837,9595,9596],{"class":843},"\u002F\u002F ❌ theme() is never tracked — read after the await\n",[837,9598,9599,9601,9603,9605,9607,9609],{"class":839,"line":847},[837,9600,688],{"class":854},[837,9602,859],{"class":937},[837,9604,2699],{"class":897},[837,9606,1976],{"class":862},[837,9608,1114],{"class":924},[837,9610,914],{"class":862},[837,9612,9613,9615,9618,9620,9623,9626,9628],{"class":839,"line":894},[837,9614,4219],{"class":897},[837,9616,9617],{"class":1348}," data",[837,9619,950],{"class":924},[837,9621,9622],{"class":897}," await",[837,9624,9625],{"class":854}," fetchData",[837,9627,1028],{"class":937},[837,9629,941],{"class":862},[837,9631,9632,9635,9637,9640,9642,9644,9647],{"class":839,"line":917},[837,9633,9634],{"class":854},"  applyTheme",[837,9636,859],{"class":937},[837,9638,9639],{"class":854},"theme",[837,9641,1028],{"class":937},[837,9643,877],{"class":862},[837,9645,9646],{"class":937}," data)",[837,9648,941],{"class":862},[837,9650,9651,9653,9655],{"class":839,"line":944},[837,9652,3505],{"class":862},[837,9654,1011],{"class":937},[837,9656,941],{"class":862},[837,9658,9659],{"class":839,"line":959},[837,9660,988],{"emptyLinePlaceholder":987},[837,9662,9663],{"class":839,"line":984},[837,9664,9665],{"class":843},"\u002F\u002F ✅ read the signal first, then await\n",[837,9667,9668,9670,9672,9674,9676,9678],{"class":839,"line":991},[837,9669,688],{"class":854},[837,9671,859],{"class":937},[837,9673,2699],{"class":897},[837,9675,1976],{"class":862},[837,9677,1114],{"class":924},[837,9679,914],{"class":862},[837,9681,9682,9684,9687,9689,9692,9694],{"class":839,"line":1017},[837,9683,4219],{"class":897},[837,9685,9686],{"class":1348}," currentTheme",[837,9688,950],{"class":924},[837,9690,9691],{"class":854}," theme",[837,9693,1028],{"class":937},[837,9695,941],{"class":862},[837,9697,9698,9700,9702,9704,9706,9708,9710],{"class":839,"line":1022},[837,9699,4219],{"class":897},[837,9701,9617],{"class":1348},[837,9703,950],{"class":924},[837,9705,9622],{"class":897},[837,9707,9625],{"class":854},[837,9709,1028],{"class":937},[837,9711,941],{"class":862},[837,9713,9714,9716,9719,9721,9723],{"class":839,"line":1033},[837,9715,9634],{"class":854},[837,9717,9718],{"class":937},"(currentTheme",[837,9720,877],{"class":862},[837,9722,9646],{"class":937},[837,9724,941],{"class":862},[837,9726,9727,9729,9731],{"class":839,"line":1053},[837,9728,3505],{"class":862},[837,9730,1011],{"class":937},[837,9732,941],{"class":862},[594,9734,9735,9736,9738,9739,9741],{},"The same rule applies inside ",[651,9737,682],{}," — which is one of several reasons ",[651,9740,682],{}," bodies\nmust be synchronous and pure.",[1266,9743,9745],{"id":9744},"_35-linkedsignal-writable-state-with-a-source-of-truth",[22,9746,9747,9748,9750],{"title":625,"href":251},"3.5 ",[651,9749,685],{}," — writable state with a source of truth",[594,9752,9753,9754,9757,9758,9760,9761,9763],{},"This is the API agents know least and need most often. Whenever the requirement is ",[603,9755,9756],{},"\"the user\ncan change X, but X should reset when Y changes\"",", that's ",[651,9759,685],{}," — not an effect, and\nnot a ",[651,9762,682],{}," you then fight against.",[644,9765,9767],{"className":831,"code":9766,"language":833,"meta":653,"style":653},"\u002F\u002F Selection is user-writable, but a new filter clears it.\nreadonly selectedId = linkedSignal\u003Cstring, string | null>({\n  source: this.filter,\n  computation: () => null,\n});\n\n\u002F\u002F Page returns to 1 whenever the search term changes.\nreadonly page = linkedSignal({ source: this.searchTerm, computation: () => 1 });\n\n\u002F\u002F Keep the selection if it still exists in the new list; otherwise pick the first.\nreadonly selected = linkedSignal\u003CCarrier[], Carrier | undefined>({\n  source: this.carriers,\n  computation: (carriers, previous) =>\n    carriers.find((carrier) => carrier.id === previous?.value?.id) ?? carriers[0],\n});\n",[651,9768,9769,9774,9802,9817,9832,9840,9844,9849,9893,9897,9902,9932,9946,9965,10015],{"__ignoreMap":653},[837,9770,9771],{"class":839,"line":840},[837,9772,9773],{"class":843},"\u002F\u002F Selection is user-writable, but a new filter clears it.\n",[837,9775,9776,9779,9781,9784,9786,9788,9790,9792,9794,9796,9798,9800],{"class":839,"line":847},[837,9777,9778],{"class":937},"readonly selectedId ",[837,9780,934],{"class":924},[837,9782,9783],{"class":854}," linkedSignal",[837,9785,1069],{"class":1068},[837,9787,2381],{"class":967},[837,9789,877],{"class":862},[837,9791,968],{"class":967},[837,9793,971],{"class":924},[837,9795,975],{"class":974},[837,9797,1078],{"class":1068},[837,9799,859],{"class":937},[837,9801,1095],{"class":862},[837,9803,9804,9807,9809,9811,9813,9815],{"class":839,"line":894},[837,9805,9806],{"class":937},"  source",[837,9808,870],{"class":869},[837,9810,1120],{"class":1036},[837,9812,1040],{"class":869},[837,9814,5360],{"class":937},[837,9816,1387],{"class":862},[837,9818,9819,9822,9824,9826,9828,9830],{"class":839,"line":917},[837,9820,9821],{"class":854},"  computation",[837,9823,870],{"class":869},[837,9825,1976],{"class":862},[837,9827,1114],{"class":924},[837,9829,975],{"class":967},[837,9831,1387],{"class":862},[837,9833,9834,9836,9838],{"class":839,"line":944},[837,9835,3505],{"class":862},[837,9837,1011],{"class":937},[837,9839,941],{"class":862},[837,9841,9842],{"class":839,"line":959},[837,9843,988],{"emptyLinePlaceholder":987},[837,9845,9846],{"class":839,"line":984},[837,9847,9848],{"class":843},"\u002F\u002F Page returns to 1 whenever the search term changes.\n",[837,9850,9851,9854,9856,9858,9860,9862,9865,9867,9869,9871,9874,9876,9879,9881,9883,9885,9887,9889,9891],{"class":839,"line":991},[837,9852,9853],{"class":937},"readonly page ",[837,9855,934],{"class":924},[837,9857,9783],{"class":854},[837,9859,859],{"class":937},[837,9861,863],{"class":862},[837,9863,9864],{"class":937}," source",[837,9866,870],{"class":869},[837,9868,1120],{"class":1036},[837,9870,1040],{"class":869},[837,9872,9873],{"class":937},"searchTerm",[837,9875,877],{"class":862},[837,9877,9878],{"class":854}," computation",[837,9880,870],{"class":869},[837,9882,1976],{"class":862},[837,9884,1114],{"class":924},[837,9886,5576],{"class":953},[837,9888,888],{"class":862},[837,9890,1011],{"class":937},[837,9892,941],{"class":862},[837,9894,9895],{"class":839,"line":1017},[837,9896,988],{"emptyLinePlaceholder":987},[837,9898,9899],{"class":839,"line":1022},[837,9900,9901],{"class":843},"\u002F\u002F Keep the selection if it still exists in the new list; otherwise pick the first.\n",[837,9903,9904,9907,9909,9911,9913,9915,9917,9919,9921,9923,9926,9928,9930],{"class":839,"line":1033},[837,9905,9906],{"class":937},"readonly selected ",[837,9908,934],{"class":924},[837,9910,9783],{"class":854},[837,9912,1069],{"class":1068},[837,9914,1072],{"class":904},[837,9916,1075],{"class":930},[837,9918,877],{"class":862},[837,9920,927],{"class":904},[837,9922,971],{"class":924},[837,9924,9925],{"class":974}," undefined",[837,9927,1078],{"class":1068},[837,9929,859],{"class":937},[837,9931,1095],{"class":862},[837,9933,9934,9936,9938,9940,9942,9944],{"class":839,"line":1053},[837,9935,9806],{"class":937},[837,9937,870],{"class":869},[837,9939,1120],{"class":1036},[837,9941,1040],{"class":869},[837,9943,1109],{"class":937},[837,9945,1387],{"class":862},[837,9947,9948,9950,9952,9954,9956,9958,9961,9963],{"class":839,"line":1098},[837,9949,9821],{"class":854},[837,9951,870],{"class":869},[837,9953,1106],{"class":862},[837,9955,1109],{"class":1002},[837,9957,877],{"class":862},[837,9959,9960],{"class":1002}," previous",[837,9962,1011],{"class":862},[837,9964,4932],{"class":924},[837,9966,9967,9970,9972,9974,9976,9978,9980,9982,9984,9986,9988,9990,9992,9994,9997,9999,10001,10004,10006,10009,10011,10013],{"class":839,"line":1151},[837,9968,9969],{"class":937},"    carriers",[837,9971,1040],{"class":869},[837,9973,4948],{"class":854},[837,9975,859],{"class":937},[837,9977,859],{"class":862},[837,9979,4955],{"class":1002},[837,9981,1011],{"class":862},[837,9983,1114],{"class":924},[837,9985,4962],{"class":937},[837,9987,1040],{"class":869},[837,9989,4967],{"class":937},[837,9991,4970],{"class":924},[837,9993,9960],{"class":937},[837,9995,9996],{"class":869},"?.",[837,9998,9099],{"class":937},[837,10000,9996],{"class":869},[837,10002,10003],{"class":937},"id) ",[837,10005,2598],{"class":924},[837,10007,10008],{"class":937}," carriers[",[837,10010,5492],{"class":953},[837,10012,2121],{"class":937},[837,10014,1387],{"class":862},[837,10016,10017,10019,10021],{"class":839,"line":1203},[837,10018,3505],{"class":862},[837,10020,1011],{"class":937},[837,10022,941],{"class":862},[594,10024,10025,10026,10029],{},"That third form — where ",[651,10027,10028],{},"computation"," receives the previous value — is the shape that\notherwise gets written as a buggy effect. It's a shortlist of recurring cases: a selected row\nthat survives a refresh if still present, a draft value that reloads when the entity changes,\na tab that falls back when the active tab disappears.",[1266,10031,10033],{"id":10032},"_36-effect-the-last-resort",[22,10034,10035,10036,10038],{"title":625,"href":257},"3.6 ",[651,10037,688],{}," — the last resort",[594,10040,10041,10043],{},[651,10042,692],{}," is the single biggest source of unmaintainable signal code, so it gets two rules,\nstated as strongly as they deserve:",[3289,10045,10046,10055],{},[594,10047,10048,10054],{},[597,10049,10050,10051,10053],{},"1. Avoid ",[651,10052,692],{}," wherever an alternative exists."," In practice an alternative almost\nalways exists.",[594,10056,10057,10062,10063,8175,10065,8175,10067,10069],{},[597,10058,10059,10060,1040],{},"2. Never write state from inside an ",[651,10061,692],{}," No ",[651,10064,2649],{},[651,10066,5556],{},[651,10068,5397],{},".\nNo exceptions.",[594,10071,10072,10073,10076,10077,10080,10081,10083],{},"An effect that writes signals creates a feedback edge in the reactive graph. The symptoms are\nglitchy intermediate renders, ",[651,10074,10075],{},"ExpressionChangedAfterItHasBeenChecked"," errors, infinite loops\nthat only trigger for certain data, and — most insidiously — code that works until someone\nadds a second effect and the two start racing. Signals are a ",[603,10078,10079],{},"pull"," system; ",[651,10082,692],{}," is the\none place you can smuggle push semantics in, and every time you do you give up the guarantee\nthat makes the rest of the system predictable.",[594,10085,10086],{},"Nearly every effect an agent writes is one of three things in disguise. In order of how often\nthey apply:",[594,10088,10089],{},[597,10090,10091,10092,10094],{},"Triggering async work when a signal changes → ",[651,10093,702],{},", given the signal.",[644,10096,10098],{"className":831,"code":10097,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ effect as a fetch trigger\nconstructor() {\n  effect(() => { this.store.loadCarrier(this.carrierId()); });\n}\n\n\u002F\u002F ✅ pass the signal; rxMethod re-runs on change and switchMap cancels the stale request\nconstructor() {\n  this.store.loadCarrier(this.carrierId);\n}\n",[651,10099,10100,10105,10113,10154,10158,10162,10167,10175,10198],{"__ignoreMap":653},[837,10101,10102],{"class":839,"line":840},[837,10103,10104],{"class":843},"\u002F\u002F ❌ effect as a fetch trigger\n",[837,10106,10107,10109,10111],{"class":839,"line":847},[837,10108,6784],{"class":854},[837,10110,5839],{"class":937},[837,10112,1095],{"class":862},[837,10114,10115,10118,10120,10122,10124,10126,10128,10130,10132,10134,10136,10138,10140,10142,10144,10146,10148,10150,10152],{"class":839,"line":894},[837,10116,10117],{"class":854},"  effect",[837,10119,859],{"class":937},[837,10121,1028],{"class":862},[837,10123,1114],{"class":924},[837,10125,1117],{"class":862},[837,10127,1120],{"class":1036},[837,10129,1040],{"class":869},[837,10131,4552],{"class":937},[837,10133,1040],{"class":869},[837,10135,5595],{"class":854},[837,10137,859],{"class":937},[837,10139,2317],{"class":1036},[837,10141,1040],{"class":869},[837,10143,8380],{"class":854},[837,10145,1384],{"class":937},[837,10147,1133],{"class":862},[837,10149,888],{"class":862},[837,10151,1011],{"class":937},[837,10153,941],{"class":862},[837,10155,10156],{"class":839,"line":917},[837,10157,1222],{"class":862},[837,10159,10160],{"class":839,"line":944},[837,10161,988],{"emptyLinePlaceholder":987},[837,10163,10164],{"class":839,"line":959},[837,10165,10166],{"class":843},"\u002F\u002F ✅ pass the signal; rxMethod re-runs on change and switchMap cancels the stale request\n",[837,10168,10169,10171,10173],{"class":839,"line":984},[837,10170,6784],{"class":854},[837,10172,5839],{"class":937},[837,10174,1095],{"class":862},[837,10176,10177,10179,10181,10183,10185,10187,10189,10191,10193,10196],{"class":839,"line":991},[837,10178,3480],{"class":1036},[837,10180,1040],{"class":869},[837,10182,4552],{"class":937},[837,10184,1040],{"class":869},[837,10186,5595],{"class":854},[837,10188,859],{"class":937},[837,10190,2317],{"class":1036},[837,10192,1040],{"class":869},[837,10194,10195],{"class":937},"carrierId)",[837,10197,941],{"class":862},[837,10199,10200],{"class":839,"line":1017},[837,10201,1222],{"class":862},[594,10203,10204],{},[597,10205,10206,10207,1040],{},"Deriving a value → ",[651,10208,682],{},[644,10210,10212],{"className":831,"code":10211,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ effect writing state\nreadonly filtered = signal\u003CCarrier[]>([]);\nconstructor() {\n  effect(() => {\n    this.filtered.set(this.all().filter((c) => c.name.includes(this.filter())));\n  });\n}\n\n\u002F\u002F ✅ computed\nreadonly filtered = computed(() =>\n  this.all().filter((carrier) => carrier.name.includes(this.filter()))\n);\n",[651,10213,10214,10219,10241,10249,10261,10324,10332,10336,10340,10345,10359,10404],{"__ignoreMap":653},[837,10215,10216],{"class":839,"line":840},[837,10217,10218],{"class":843},"\u002F\u002F ❌ effect writing state\n",[837,10220,10221,10224,10226,10228,10230,10232,10234,10236,10239],{"class":839,"line":847},[837,10222,10223],{"class":937},"readonly filtered ",[837,10225,934],{"class":924},[837,10227,2534],{"class":854},[837,10229,1069],{"class":1068},[837,10231,1072],{"class":904},[837,10233,1075],{"class":930},[837,10235,1078],{"class":1068},[837,10237,10238],{"class":937},"([])",[837,10240,941],{"class":862},[837,10242,10243,10245,10247],{"class":839,"line":894},[837,10244,6784],{"class":854},[837,10246,5839],{"class":937},[837,10248,1095],{"class":862},[837,10250,10251,10253,10255,10257,10259],{"class":839,"line":917},[837,10252,10117],{"class":854},[837,10254,859],{"class":937},[837,10256,1028],{"class":862},[837,10258,1114],{"class":924},[837,10260,914],{"class":862},[837,10262,10263,10265,10267,10270,10272,10274,10276,10278,10280,10282,10284,10286,10288,10290,10292,10295,10297,10299,10302,10304,10306,10308,10311,10313,10315,10317,10319,10322],{"class":839,"line":944},[837,10264,1037],{"class":1036},[837,10266,1040],{"class":869},[837,10268,10269],{"class":937},"filtered",[837,10271,1040],{"class":869},[837,10273,2649],{"class":854},[837,10275,859],{"class":937},[837,10277,2317],{"class":1036},[837,10279,1040],{"class":869},[837,10281,8605],{"class":854},[837,10283,1028],{"class":937},[837,10285,1040],{"class":869},[837,10287,5360],{"class":854},[837,10289,859],{"class":937},[837,10291,859],{"class":862},[837,10293,10294],{"class":1002},"c",[837,10296,1011],{"class":862},[837,10298,1114],{"class":924},[837,10300,10301],{"class":937}," c",[837,10303,1040],{"class":869},[837,10305,4006],{"class":937},[837,10307,1040],{"class":869},[837,10309,10310],{"class":854},"includes",[837,10312,859],{"class":937},[837,10314,2317],{"class":1036},[837,10316,1040],{"class":869},[837,10318,5360],{"class":854},[837,10320,10321],{"class":937},"())))",[837,10323,941],{"class":862},[837,10325,10326,10328,10330],{"class":839,"line":959},[837,10327,5172],{"class":862},[837,10329,1011],{"class":937},[837,10331,941],{"class":862},[837,10333,10334],{"class":839,"line":984},[837,10335,1222],{"class":862},[837,10337,10338],{"class":839,"line":991},[837,10339,988],{"emptyLinePlaceholder":987},[837,10341,10342],{"class":839,"line":1017},[837,10343,10344],{"class":843},"\u002F\u002F ✅ computed\n",[837,10346,10347,10349,10351,10353,10355,10357],{"class":839,"line":1022},[837,10348,10223],{"class":937},[837,10350,934],{"class":924},[837,10352,4887],{"class":854},[837,10354,859],{"class":937},[837,10356,1028],{"class":862},[837,10358,4932],{"class":924},[837,10360,10361,10363,10365,10367,10369,10371,10373,10375,10377,10379,10381,10383,10385,10387,10389,10391,10393,10395,10397,10399,10401],{"class":839,"line":1033},[837,10362,3480],{"class":1036},[837,10364,1040],{"class":869},[837,10366,8605],{"class":854},[837,10368,1028],{"class":937},[837,10370,1040],{"class":869},[837,10372,5360],{"class":854},[837,10374,859],{"class":937},[837,10376,859],{"class":862},[837,10378,4955],{"class":1002},[837,10380,1011],{"class":862},[837,10382,1114],{"class":924},[837,10384,4962],{"class":937},[837,10386,1040],{"class":869},[837,10388,4006],{"class":937},[837,10390,1040],{"class":869},[837,10392,10310],{"class":854},[837,10394,859],{"class":937},[837,10396,2317],{"class":1036},[837,10398,1040],{"class":869},[837,10400,5360],{"class":854},[837,10402,10403],{"class":937},"()))\n",[837,10405,10406,10408],{"class":839,"line":1053},[837,10407,1011],{"class":937},[837,10409,941],{"class":862},[594,10411,10412],{},[597,10413,10414,10415,1040],{},"Resetting writable state when a source changes → ",[651,10416,685],{},[644,10418,10420],{"className":831,"code":10419,"language":833,"meta":653,"style":653},"\u002F\u002F ❌ effect resetting state\nconstructor() {\n  effect(() => { this.filter(); this.selectedId.set(null); });\n}\n\n\u002F\u002F ✅ linkedSignal\nreadonly selectedId = linkedSignal({ source: this.filter, computation: () => null });\n",[651,10421,10422,10427,10435,10482,10486,10490,10495],{"__ignoreMap":653},[837,10423,10424],{"class":839,"line":840},[837,10425,10426],{"class":843},"\u002F\u002F ❌ effect resetting state\n",[837,10428,10429,10431,10433],{"class":839,"line":847},[837,10430,6784],{"class":854},[837,10432,5839],{"class":937},[837,10434,1095],{"class":862},[837,10436,10437,10439,10441,10443,10445,10447,10449,10451,10453,10455,10457,10459,10461,10463,10465,10467,10469,10472,10474,10476,10478,10480],{"class":839,"line":894},[837,10438,10117],{"class":854},[837,10440,859],{"class":937},[837,10442,1028],{"class":862},[837,10444,1114],{"class":924},[837,10446,1117],{"class":862},[837,10448,1120],{"class":1036},[837,10450,1040],{"class":869},[837,10452,5360],{"class":854},[837,10454,1028],{"class":937},[837,10456,1133],{"class":862},[837,10458,1120],{"class":1036},[837,10460,1040],{"class":869},[837,10462,4977],{"class":937},[837,10464,1040],{"class":869},[837,10466,2649],{"class":854},[837,10468,859],{"class":937},[837,10470,10471],{"class":967},"null",[837,10473,1011],{"class":937},[837,10475,1133],{"class":862},[837,10477,888],{"class":862},[837,10479,1011],{"class":937},[837,10481,941],{"class":862},[837,10483,10484],{"class":839,"line":917},[837,10485,1222],{"class":862},[837,10487,10488],{"class":839,"line":944},[837,10489,988],{"emptyLinePlaceholder":987},[837,10491,10492],{"class":839,"line":959},[837,10493,10494],{"class":843},"\u002F\u002F ✅ linkedSignal\n",[837,10496,10497,10499,10501,10503,10505,10507,10509,10511,10513,10515,10517,10519,10521,10523,10525,10527,10529,10531,10533],{"class":839,"line":984},[837,10498,9778],{"class":937},[837,10500,934],{"class":924},[837,10502,9783],{"class":854},[837,10504,859],{"class":937},[837,10506,863],{"class":862},[837,10508,9864],{"class":937},[837,10510,870],{"class":869},[837,10512,1120],{"class":1036},[837,10514,1040],{"class":869},[837,10516,5360],{"class":937},[837,10518,877],{"class":862},[837,10520,9878],{"class":854},[837,10522,870],{"class":869},[837,10524,1976],{"class":862},[837,10526,1114],{"class":924},[837,10528,975],{"class":967},[837,10530,888],{"class":862},[837,10532,1011],{"class":937},[837,10534,941],{"class":862},[594,10536,10537,10538,10540,10541,10543,10544,10546,10547,10550,10551,10553],{},"What's actually left for ",[651,10539,692],{},"? A short list: logging and analytics, syncing to\n",[651,10542,3225],{},", driving a non-signal third-party library (a charting or grid instance), and\nstore-owned lifecycle like polling in ",[651,10545,5649],{},". All of them share a shape —\n",[603,10548,10549],{},"pushing signal state out to something that isn't reactive",". None of them read state back in.\nAny surviving ",[651,10552,692],{}," should carry a comment naming which alternative was considered and\nwhy it didn't fit.",[594,10555,10556,10557,10559,10560,10563,10564,10566],{},"Also: ",[651,10558,692],{}," runs ",[597,10561,10562],{},"before"," the DOM is updated, and it must be created in an injection\ncontext. Use ",[651,10565,7131],{}," for anything that needs tearing down.",[1266,10568,10570],{"id":10569},"_37-dom-work-afternextrender-and-afterrendereffect",[22,10571,10572,10573,3104,10575],{"title":625,"href":263},"3.7 DOM work: ",[651,10574,3389],{},[651,10576,10577],{},"afterRenderEffect",[594,10579,10580,10582,10583,10585,10586,10588],{},[651,10581,692],{}," is the wrong tool for reading or writing the DOM, because it runs before Angular\nhas rendered. Use the render hooks — ",[651,10584,3389],{}," for one-shot setup, ",[651,10587,10577],{},"\nfor reactive DOM work.",[594,10590,10591,10593],{},[651,10592,10577],{}," makes you split reads from writes into phases, which is what prevents\nlayout thrashing:",[644,10595,10597],{"className":831,"code":10596,"language":833,"meta":653,"style":653},"export class TrendChart {\n  private readonly canvas = viewChild.required\u003CElementRef\u003CHTMLCanvasElement>>('canvas');\n\n  constructor() {\n    afterRenderEffect({\n      earlyRead: () => this.canvas().nativeElement.getBoundingClientRect().width,\n      write: (width) => drawChart(this.canvas().nativeElement, width()),\n    });\n  }\n}\n",[651,10598,10599,10610,10649,10653,10661,10670,10709,10750,10758,10762],{"__ignoreMap":653},[837,10600,10601,10603,10605,10608],{"class":839,"line":840},[837,10602,898],{"class":897},[837,10604,901],{"class":897},[837,10606,10607],{"class":904}," TrendChart",[837,10609,914],{"class":862},[837,10611,10612,10614,10616,10619,10621,10624,10626,10628,10630,10633,10635,10638,10640,10642,10645,10647],{"class":839,"line":847},[837,10613,2523],{"class":897},[837,10615,2526],{"class":897},[837,10617,10618],{"class":920}," canvas",[837,10620,950],{"class":924},[837,10622,10623],{"class":937}," viewChild",[837,10625,1040],{"class":869},[837,10627,2376],{"class":854},[837,10629,1069],{"class":1068},[837,10631,10632],{"class":904},"ElementRef",[837,10634,1069],{"class":1068},[837,10636,10637],{"class":904},"HTMLCanvasElement",[837,10639,2551],{"class":1068},[837,10641,859],{"class":937},[837,10643,10644],{"class":873},"'canvas'",[837,10646,1011],{"class":937},[837,10648,941],{"class":862},[837,10650,10651],{"class":839,"line":894},[837,10652,988],{"emptyLinePlaceholder":987},[837,10654,10655,10657,10659],{"class":839,"line":917},[837,10656,994],{"class":897},[837,10658,1028],{"class":862},[837,10660,914],{"class":862},[837,10662,10663,10666,10668],{"class":839,"line":944},[837,10664,10665],{"class":854},"    afterRenderEffect",[837,10667,859],{"class":937},[837,10669,1095],{"class":862},[837,10671,10672,10675,10677,10679,10681,10683,10685,10688,10690,10692,10695,10697,10700,10702,10704,10707],{"class":839,"line":959},[837,10673,10674],{"class":854},"      earlyRead",[837,10676,870],{"class":869},[837,10678,1976],{"class":862},[837,10680,1114],{"class":924},[837,10682,1120],{"class":1036},[837,10684,1040],{"class":869},[837,10686,10687],{"class":854},"canvas",[837,10689,1028],{"class":937},[837,10691,1040],{"class":869},[837,10693,10694],{"class":937},"nativeElement",[837,10696,1040],{"class":869},[837,10698,10699],{"class":854},"getBoundingClientRect",[837,10701,1028],{"class":937},[837,10703,1040],{"class":869},[837,10705,10706],{"class":937},"width",[837,10708,1387],{"class":862},[837,10710,10711,10714,10716,10718,10720,10722,10724,10727,10729,10731,10733,10735,10737,10739,10741,10743,10746,10748],{"class":839,"line":984},[837,10712,10713],{"class":854},"      write",[837,10715,870],{"class":869},[837,10717,1106],{"class":862},[837,10719,10706],{"class":1002},[837,10721,1011],{"class":862},[837,10723,1114],{"class":924},[837,10725,10726],{"class":854}," drawChart",[837,10728,859],{"class":937},[837,10730,2317],{"class":1036},[837,10732,1040],{"class":869},[837,10734,10687],{"class":854},[837,10736,1028],{"class":937},[837,10738,1040],{"class":869},[837,10740,10694],{"class":937},[837,10742,877],{"class":862},[837,10744,10745],{"class":854}," width",[837,10747,1384],{"class":937},[837,10749,1387],{"class":862},[837,10751,10752,10754,10756],{"class":839,"line":991},[837,10753,1206],{"class":862},[837,10755,1011],{"class":937},[837,10757,941],{"class":862},[837,10759,10760],{"class":839,"line":1017},[837,10761,1216],{"class":862},[837,10763,10764],{"class":839,"line":1022},[837,10765,1222],{"class":862},[594,10767,10768,10769,10772,10773,10772,10776,10779,10780,10783,10784,10786,10787,10789],{},"Phases run in order: ",[651,10770,10771],{},"earlyRead"," → ",[651,10774,10775],{},"write",[651,10777,10778],{},"mixedReadWrite"," (avoid) → ",[651,10781,10782],{},"read",". Never read the\nDOM in ",[651,10785,10775],{},"; never write in ",[651,10788,10782],{},". Neither hook runs during SSR, which makes them the\ncorrect home for browser-only work.",[1266,10791,10793],{"id":10792},"_38-inputs-outputs-and-queries",[22,10794,270],{"title":625,"href":269},[594,10796,10797,10800],{},[597,10798,10799],{},"Inputs"," are signals:",[644,10802,10804],{"className":831,"code":10803,"language":833,"meta":653,"style":653},"readonly name = input('Guest');                             \u002F\u002F optional, with default\nreadonly carrierId = input.required\u003Cstring>();              \u002F\u002F required — compile-time checked\nreadonly label = input('', { alias: 'btnLabel' });          \u002F\u002F aliased\nreadonly disabled = input(false, { transform: booleanAttribute });   \u002F\u002F coerced\n",[651,10805,10806,10827,10853,10888],{"__ignoreMap":653},[837,10807,10808,10811,10813,10815,10817,10820,10822,10824],{"class":839,"line":840},[837,10809,10810],{"class":937},"readonly name ",[837,10812,934],{"class":924},[837,10814,2371],{"class":854},[837,10816,859],{"class":937},[837,10818,10819],{"class":873},"'Guest'",[837,10821,1011],{"class":937},[837,10823,1133],{"class":862},[837,10825,10826],{"class":843},"                             \u002F\u002F optional, with default\n",[837,10828,10829,10832,10834,10836,10838,10840,10842,10844,10846,10848,10850],{"class":839,"line":847},[837,10830,10831],{"class":937},"readonly carrierId ",[837,10833,934],{"class":924},[837,10835,2371],{"class":937},[837,10837,1040],{"class":869},[837,10839,2376],{"class":854},[837,10841,1069],{"class":1068},[837,10843,2381],{"class":967},[837,10845,1078],{"class":1068},[837,10847,1028],{"class":937},[837,10849,1133],{"class":862},[837,10851,10852],{"class":843},"              \u002F\u002F required — compile-time checked\n",[837,10854,10855,10858,10860,10862,10864,10867,10869,10871,10874,10876,10879,10881,10883,10885],{"class":839,"line":894},[837,10856,10857],{"class":937},"readonly label ",[837,10859,934],{"class":924},[837,10861,2371],{"class":854},[837,10863,859],{"class":937},[837,10865,10866],{"class":873},"''",[837,10868,877],{"class":862},[837,10870,1117],{"class":862},[837,10872,10873],{"class":937}," alias",[837,10875,870],{"class":869},[837,10877,10878],{"class":873}," 'btnLabel'",[837,10880,888],{"class":862},[837,10882,1011],{"class":937},[837,10884,1133],{"class":862},[837,10886,10887],{"class":843},"          \u002F\u002F aliased\n",[837,10889,10890,10893,10895,10897,10899,10902,10904,10906,10909,10911,10914,10916,10918,10920],{"class":839,"line":917},[837,10891,10892],{"class":937},"readonly disabled ",[837,10894,934],{"class":924},[837,10896,2371],{"class":854},[837,10898,859],{"class":937},[837,10900,10901],{"class":953},"false",[837,10903,877],{"class":862},[837,10905,1117],{"class":862},[837,10907,10908],{"class":937}," transform",[837,10910,870],{"class":869},[837,10912,10913],{"class":937}," booleanAttribute ",[837,10915,3505],{"class":862},[837,10917,1011],{"class":937},[837,10919,1133],{"class":862},[837,10921,10922],{"class":843},"   \u002F\u002F coerced\n",[594,10924,10925,10926,679,10928,10931,10932,10934],{},"Transforms must be pure and statically analysable. Don't name an input after a DOM property\n(",[651,10927,3992],{},[651,10929,10930],{},"title","). Derive from inputs with ",[651,10933,682],{},", never by copying into a local signal in\nan effect.",[594,10936,10937,10941,10942,10945],{},[597,10938,10939],{},[651,10940,8886],{}," for two-way binding — ",[651,10943,10944],{},"[(value)]=\"mySignal\""," in the consumer.",[594,10947,10948,10951,10952,870],{},[597,10949,10950],{},"Outputs"," are ",[651,10953,8898],{},[644,10955,10957],{"className":831,"code":10956,"language":833,"meta":653,"style":653},"readonly carrierSelected = output\u003Cstring>();\nreadonly panelClosed = output\u003Cvoid>();\n",[651,10958,10959,10979],{"__ignoreMap":653},[837,10960,10961,10964,10966,10969,10971,10973,10975,10977],{"class":839,"line":840},[837,10962,10963],{"class":937},"readonly carrierSelected ",[837,10965,934],{"class":924},[837,10967,10968],{"class":854}," output",[837,10970,1069],{"class":1068},[837,10972,2381],{"class":967},[837,10974,1078],{"class":1068},[837,10976,1028],{"class":937},[837,10978,941],{"class":862},[837,10980,10981,10984,10986,10988,10990,10993,10995,10997],{"class":839,"line":847},[837,10982,10983],{"class":937},"readonly panelClosed ",[837,10985,934],{"class":924},[837,10987,10968],{"class":854},[837,10989,1069],{"class":1068},[837,10991,10992],{"class":967},"void",[837,10994,1078],{"class":1068},[837,10996,1028],{"class":937},[837,10998,941],{"class":862},[594,11000,11001,11002,11005,11006,11009],{},"Name them for the event that happened (",[651,11003,11004],{},"carrierSelected","), not the handler (",[651,11007,11008],{},"onSelect","). They\ndon't bubble, and they must not collide with native DOM event names.",[594,11011,11012,11015],{},[597,11013,11014],{},"Queries"," are signals too, and production-ready since v19:",[644,11017,11019],{"className":831,"code":11018,"language":833,"meta":653,"style":653},"readonly canvas = viewChild.required\u003CElementRef>('canvas');\nreadonly rows = viewChildren(RowComponent);\nreadonly projected = contentChildren(ItemComponent);\n",[651,11020,11021,11048,11063],{"__ignoreMap":653},[837,11022,11023,11026,11028,11030,11032,11034,11036,11038,11040,11042,11044,11046],{"class":839,"line":840},[837,11024,11025],{"class":937},"readonly canvas ",[837,11027,934],{"class":924},[837,11029,10623],{"class":937},[837,11031,1040],{"class":869},[837,11033,2376],{"class":854},[837,11035,1069],{"class":1068},[837,11037,10632],{"class":904},[837,11039,1078],{"class":1068},[837,11041,859],{"class":937},[837,11043,10644],{"class":873},[837,11045,1011],{"class":937},[837,11047,941],{"class":862},[837,11049,11050,11053,11055,11058,11061],{"class":839,"line":847},[837,11051,11052],{"class":937},"readonly rows ",[837,11054,934],{"class":924},[837,11056,11057],{"class":854}," viewChildren",[837,11059,11060],{"class":937},"(RowComponent)",[837,11062,941],{"class":862},[837,11064,11065,11068,11070,11073,11076],{"class":839,"line":894},[837,11066,11067],{"class":937},"readonly projected ",[837,11069,934],{"class":924},[837,11071,11072],{"class":854}," contentChildren",[837,11074,11075],{"class":937},"(ItemComponent)",[837,11077,941],{"class":862},[594,11079,11080,11081,11083,11084,11087],{},"Signal queries compose with ",[651,11082,682],{}," and need no lifecycle hook to be safe to read — a\nstrict improvement over the decorator versions. ",[651,11085,11086],{},"ng generate @angular\u002Fcore:signal-queries-migration","\nconverts existing code.",[1266,11089,11091],{"id":11090},"_39-rxjs-interop-and-when-to-stay-in-rxjs",[22,11092,276],{"title":625,"href":275},[594,11094,11095,11096,11099],{},"Signals are for state; RxJS is for ",[597,11097,11098],{},"events over time",". Neither replaces the other.",[594,11101,11102,11103,679,11106,1387,11109,3026,11112,679,11115,11118,11119,11121,11122,11124],{},"Reach for RxJS when you need time-based operators — ",[651,11104,11105],{},"debounceTime",[651,11107,11108],{},"throttleTime",[651,11110,11111],{},"retry",[651,11113,11114],{},"retryWhen",[651,11116,11117],{},"combineLatest"," over async streams, ",[651,11120,5617],{}," cancellation. In this\narchitecture that lands almost entirely inside store ",[651,11123,702],{},"s, which is exactly where you\nwant it.",[594,11126,11127],{},"At the boundary:",[644,11129,11131],{"className":831,"code":11130,"language":833,"meta":653,"style":653},"\u002F\u002F Observable → signal. `initialValue` is required unless the source emits synchronously.\nreadonly connection = toSignal(this.socket.status$, { initialValue: 'connecting' as const });\n\n\u002F\u002F Signal → Observable, for feeding an RxJS pipeline.\nreadonly term$ = toObservable(this.searchTerm);\n",[651,11132,11133,11138,11184,11188,11193],{"__ignoreMap":653},[837,11134,11135],{"class":839,"line":840},[837,11136,11137],{"class":843},"\u002F\u002F Observable → signal. `initialValue` is required unless the source emits synchronously.\n",[837,11139,11140,11143,11145,11148,11150,11152,11154,11157,11159,11162,11164,11166,11169,11171,11174,11176,11178,11180,11182],{"class":839,"line":847},[837,11141,11142],{"class":937},"readonly connection ",[837,11144,934],{"class":924},[837,11146,11147],{"class":854}," toSignal",[837,11149,859],{"class":937},[837,11151,2317],{"class":1036},[837,11153,1040],{"class":869},[837,11155,11156],{"class":937},"socket",[837,11158,1040],{"class":869},[837,11160,11161],{"class":937},"status$",[837,11163,877],{"class":862},[837,11165,1117],{"class":862},[837,11167,11168],{"class":937}," initialValue",[837,11170,870],{"class":869},[837,11172,11173],{"class":873}," 'connecting'",[837,11175,5996],{"class":897},[837,11177,1345],{"class":897},[837,11179,888],{"class":862},[837,11181,1011],{"class":937},[837,11183,941],{"class":862},[837,11185,11186],{"class":839,"line":894},[837,11187,988],{"emptyLinePlaceholder":987},[837,11189,11190],{"class":839,"line":917},[837,11191,11192],{"class":843},"\u002F\u002F Signal → Observable, for feeding an RxJS pipeline.\n",[837,11194,11195,11198,11200,11203,11205,11207,11209,11212],{"class":839,"line":944},[837,11196,11197],{"class":937},"readonly term$ ",[837,11199,934],{"class":924},[837,11201,11202],{"class":854}," toObservable",[837,11204,859],{"class":937},[837,11206,2317],{"class":1036},[837,11208,1040],{"class":869},[837,11210,11211],{"class":937},"searchTerm)",[837,11213,941],{"class":862},[594,11215,11216,11217,11220,11221,11223,11224,11226],{},"Prefer a store-derived signal over ",[651,11218,11219],{},"toSignal"," when the data already flows through a store —\n",[651,11222,11219],{}," on top of a store is usually a sign the pipeline is in the wrong place. And note\n",[651,11225,11219],{}," subscribes immediately and unsubscribes on destroy, so it needs an injection context.",[594,11228,11229,11230,11232],{},"If you truly must subscribe manually, ",[651,11231,8090],{}," is not optional:",[644,11234,11236],{"className":831,"code":11235,"language":833,"meta":653,"style":653},"someEvents$.pipe(takeUntilDestroyed()).subscribe(handler);\n",[651,11237,11238],{"__ignoreMap":653},[837,11239,11240,11243,11245,11247,11249,11252,11254,11256,11258,11261],{"class":839,"line":840},[837,11241,11242],{"class":937},"someEvents$",[837,11244,1040],{"class":869},[837,11246,3898],{"class":854},[837,11248,859],{"class":937},[837,11250,11251],{"class":854},"takeUntilDestroyed",[837,11253,1384],{"class":937},[837,11255,1040],{"class":869},[837,11257,1090],{"class":854},[837,11259,11260],{"class":937},"(handler)",[837,11262,941],{"class":862},[1266,11264,11266],{"id":11265},"part-3-rules",[22,11267,282],{"title":625,"href":281},[658,11269,11270,11273,11283,11290,11295,11303,11308,11313,11321,11333],{},[18,11271,11272],{},"Pick from the decision table; don't improvise.",[18,11274,11275,3026,11277,11279,11280,11282],{},[651,11276,2649],{},[651,11278,5556],{}," only, never ",[651,11281,8785],{},"; always produce new references for objects and arrays.",[18,11284,11285,11286,11289],{},"Expose ",[651,11287,11288],{},"asReadonly()"," from services and stores.",[18,11291,11292,11294],{},[651,11293,682],{}," is pure, cheap, and never a pass-through.",[18,11296,11297,11298,11300,11301,1040],{},"Read signals ",[603,11299,10562],{}," any ",[651,11302,2741],{},[18,11304,11305,11307],{},[651,11306,685],{}," for writable-state-with-a-source; it replaces most effects that reset state.",[18,11309,11310,11312],{},[651,11311,692],{}," is a last resort, never writes state, and carries a justifying comment.",[18,11314,11315,11316,2973,11318,11320],{},"DOM work goes in ",[651,11317,3389],{},[651,11319,10577],{},", split into phases.",[18,11322,11323,11324,3026,11326,3026,11328,3026,11330,11332],{},"Signal ",[651,11325,1594],{},[651,11327,8898],{},[651,11329,8917],{},[651,11331,8935],{}," — never the decorator equivalents.",[18,11334,11335,11336,11338,11339,11341],{},"Keep RxJS inside store ",[651,11337,702],{},"s; ",[651,11340,8090],{}," on any manual subscription.",[617,11343],{},[620,11345,11347],{"id":11346},"part-4-dependency-injection",[22,11348,288],{"title":625,"href":287},[594,11350,11351,11352,11354],{},"DI is how this architecture is assembled and how it stays testable. It's also the part agents\nunderstand most shallowly — they know ",[651,11353,4120],{}," and stop there.",[1266,11356,11358],{"id":11357},"_41-always-inject-always-in-an-injection-context",[22,11359,11360,11361,11363],{"title":625,"href":295},"4.1 Always ",[651,11362,2291],{},", always in an injection context",[644,11365,11367],{"className":831,"code":11366,"language":833,"meta":653,"style":653},"export class CarrierList {\n  private readonly store = inject(CarriersStore);      \u002F\u002F ✅ field initializer — preferred\n  private readonly router = inject(Router);\n}\n",[651,11368,11369,11380,11399,11417],{"__ignoreMap":653},[837,11370,11371,11373,11375,11378],{"class":839,"line":840},[837,11372,898],{"class":897},[837,11374,901],{"class":897},[837,11376,11377],{"class":904}," CarrierList",[837,11379,914],{"class":862},[837,11381,11382,11384,11386,11388,11390,11392,11394,11396],{"class":839,"line":847},[837,11383,2523],{"class":897},[837,11385,2526],{"class":897},[837,11387,4896],{"class":920},[837,11389,950],{"class":924},[837,11391,2718],{"class":854},[837,11393,7688],{"class":937},[837,11395,1133],{"class":862},[837,11397,11398],{"class":843},"      \u002F\u002F ✅ field initializer — preferred\n",[837,11400,11401,11403,11405,11408,11410,11412,11415],{"class":839,"line":894},[837,11402,2523],{"class":897},[837,11404,2526],{"class":897},[837,11406,11407],{"class":920}," router",[837,11409,950],{"class":924},[837,11411,2718],{"class":854},[837,11413,11414],{"class":937},"(Router)",[837,11416,941],{"class":862},[837,11418,11419],{"class":839,"line":917},[837,11420,1222],{"class":862},[594,11422,11423,11424,11426,11427,11429],{},"Prefer ",[651,11425,2291],{}," over constructor parameters: it works in functions (guards, interceptors,\nresolvers, ",[651,11428,702],{}," defaults), composes into reusable helper functions, and doesn't force\ninheritance boilerplate through constructor signatures in subclasses.",[594,11431,11432,11434,11435,11438],{},[651,11433,2291],{}," only works inside an ",[597,11436,11437],{},"injection context",", which exists in exactly four places:",[14,11440,11441,11459,11465,11480],{},[18,11442,11443,11446,11447,679,11450,679,11453,679,11456,1011],{},[597,11444,11445],{},"field initialisers"," of DI-instantiated classes (",[651,11448,11449],{},"@Component",[651,11451,11452],{},"@Directive",[651,11454,11455],{},"@Injectable",[651,11457,11458],{},"@Pipe",[18,11460,11461,11464],{},[597,11462,11463],{},"constructor bodies"," of those classes",[18,11466,11467,675,11470,11473,11474,5402,11477],{},[597,11468,11469],{},"factory functions",[651,11471,11472],{},"useFactory",", and an ",[651,11475,11476],{},"InjectionToken",[651,11478,11479],{},"factory",[18,11481,11482,11485],{},[597,11483,11484],{},"functional APIs Angular invokes"," — route guards, resolvers, HTTP interceptors",[644,11487,11489],{"className":831,"code":11488,"language":833,"meta":653,"style":653},"export class Example {\n  private readonly a = inject(ServiceA);      \u002F\u002F ✅\n  constructor() { const b = inject(ServiceB); }  \u002F\u002F ✅\n  onClick() { const c = inject(ServiceC); }    \u002F\u002F ❌ throws — not an injection context\n}\n",[651,11490,11491,11502,11522,11548,11575],{"__ignoreMap":653},[837,11492,11493,11495,11497,11500],{"class":839,"line":840},[837,11494,898],{"class":897},[837,11496,901],{"class":897},[837,11498,11499],{"class":904}," Example",[837,11501,914],{"class":862},[837,11503,11504,11506,11508,11510,11512,11514,11517,11519],{"class":839,"line":847},[837,11505,2523],{"class":897},[837,11507,2526],{"class":897},[837,11509,9335],{"class":920},[837,11511,950],{"class":924},[837,11513,2718],{"class":854},[837,11515,11516],{"class":937},"(ServiceA)",[837,11518,1133],{"class":862},[837,11520,11521],{"class":843},"      \u002F\u002F ✅\n",[837,11523,11524,11526,11528,11530,11532,11534,11536,11538,11541,11543,11545],{"class":839,"line":894},[837,11525,994],{"class":897},[837,11527,1028],{"class":862},[837,11529,1117],{"class":862},[837,11531,1345],{"class":897},[837,11533,9328],{"class":1348},[837,11535,950],{"class":924},[837,11537,2718],{"class":854},[837,11539,11540],{"class":937},"(ServiceB)",[837,11542,1133],{"class":862},[837,11544,888],{"class":862},[837,11546,11547],{"class":843},"  \u002F\u002F ✅\n",[837,11549,11550,11553,11555,11557,11559,11561,11563,11565,11568,11570,11572],{"class":839,"line":917},[837,11551,11552],{"class":854},"  onClick",[837,11554,1028],{"class":862},[837,11556,1117],{"class":862},[837,11558,1345],{"class":897},[837,11560,10301],{"class":1348},[837,11562,950],{"class":924},[837,11564,2718],{"class":854},[837,11566,11567],{"class":937},"(ServiceC)",[837,11569,1133],{"class":862},[837,11571,888],{"class":862},[837,11573,11574],{"class":843},"    \u002F\u002F ❌ throws — not an injection context\n",[837,11576,11577],{"class":839,"line":944},[837,11578,1222],{"class":862},[594,11580,11581,11582,870],{},"When you need a context outside those places — dynamic component creation, deferred work —\ncapture an injector and use ",[651,11583,11584],{},"runInInjectionContext",[644,11586,11588],{"className":831,"code":11587,"language":833,"meta":653,"style":653},"private readonly injector = inject(Injector);\n\ncreateLater() {\n  runInInjectionContext(this.injector, () => {\n    const analytics = inject(AnalyticsService);   \u002F\u002F ✅ now valid\n  });\n}\n",[651,11589,11590,11604,11608,11617,11638,11657,11665],{"__ignoreMap":653},[837,11591,11592,11595,11597,11599,11602],{"class":839,"line":840},[837,11593,11594],{"class":937},"private readonly injector ",[837,11596,934],{"class":924},[837,11598,2718],{"class":854},[837,11600,11601],{"class":937},"(Injector)",[837,11603,941],{"class":862},[837,11605,11606],{"class":839,"line":847},[837,11607,988],{"emptyLinePlaceholder":987},[837,11609,11610,11613,11615],{"class":839,"line":894},[837,11611,11612],{"class":854},"createLater",[837,11614,5839],{"class":937},[837,11616,1095],{"class":862},[837,11618,11619,11622,11624,11626,11628,11630,11632,11634,11636],{"class":839,"line":917},[837,11620,11621],{"class":854},"  runInInjectionContext",[837,11623,859],{"class":937},[837,11625,2317],{"class":1036},[837,11627,1040],{"class":869},[837,11629,6824],{"class":937},[837,11631,877],{"class":862},[837,11633,1976],{"class":862},[837,11635,1114],{"class":924},[837,11637,914],{"class":862},[837,11639,11640,11642,11645,11647,11649,11652,11654],{"class":839,"line":944},[837,11641,7752],{"class":897},[837,11643,11644],{"class":1348}," analytics",[837,11646,950],{"class":924},[837,11648,2718],{"class":854},[837,11650,11651],{"class":937},"(AnalyticsService)",[837,11653,1133],{"class":862},[837,11655,11656],{"class":843},"   \u002F\u002F ✅ now valid\n",[837,11658,11659,11661,11663],{"class":839,"line":959},[837,11660,5172],{"class":862},[837,11662,1011],{"class":937},[837,11664,941],{"class":862},[837,11666,11667],{"class":839,"line":984},[837,11668,1222],{"class":862},[594,11670,11671,11672,11674],{},"And if you write a helper that itself calls ",[651,11673,2291],{},", guard it so callers get a clear error\nrather than a confusing one:",[644,11676,11678],{"className":831,"code":11677,"language":833,"meta":653,"style":653},"export function injectHostElement\u003CT extends Element>(): T {\n  assertInInjectionContext(injectHostElement);\n  return inject(ElementRef).nativeElement;\n}\n",[651,11679,11680,11709,11719,11734],{"__ignoreMap":653},[837,11681,11682,11684,11686,11689,11691,11693,11695,11698,11700,11702,11704,11707],{"class":839,"line":840},[837,11683,898],{"class":897},[837,11685,1505],{"class":897},[837,11687,11688],{"class":854}," injectHostElement",[837,11690,1069],{"class":1068},[837,11692,6559],{"class":904},[837,11694,6869],{"class":897},[837,11696,11697],{"class":904}," Element",[837,11699,1078],{"class":1068},[837,11701,1028],{"class":862},[837,11703,870],{"class":924},[837,11705,11706],{"class":904}," T",[837,11708,914],{"class":862},[837,11710,11711,11714,11717],{"class":839,"line":847},[837,11712,11713],{"class":854},"  assertInInjectionContext",[837,11715,11716],{"class":937},"(injectHostElement)",[837,11718,941],{"class":862},[837,11720,11721,11723,11725,11728,11730,11732],{"class":839,"line":894},[837,11722,1532],{"class":897},[837,11724,2718],{"class":854},[837,11726,11727],{"class":937},"(ElementRef)",[837,11729,1040],{"class":869},[837,11731,10694],{"class":937},[837,11733,941],{"class":862},[837,11735,11736],{"class":839,"line":917},[837,11737,1222],{"class":862},[594,11739,11740,11741,11750],{},"This also explains a rule from Part 2: NgRx signal store services are injected as ",[597,11742,11743,11744,2973,11746,2973,11748],{},"default\nparameters of ",[651,11745,2785],{},[651,11747,4657],{},[651,11749,7097],{},", because those factory functions\nrun in an injection context while individual store methods do not.",[644,11752,11754],{"className":831,"code":11753,"language":833,"meta":653,"style":653},"withMethods((store, api = inject(CarriersApiService), router = inject(Router)) => ({ ... }))\n",[651,11755,11756],{"__ignoreMap":653},[837,11757,11758,11760,11762,11764,11766,11768,11770,11772,11774,11776,11778,11780,11782,11784,11786,11788,11790,11792,11794,11796,11798],{"class":839,"line":840},[837,11759,2785],{"class":854},[837,11761,859],{"class":937},[837,11763,859],{"class":862},[837,11765,4552],{"class":1002},[837,11767,877],{"class":862},[837,11769,2713],{"class":1002},[837,11771,950],{"class":924},[837,11773,2718],{"class":854},[837,11775,5108],{"class":937},[837,11777,877],{"class":862},[837,11779,11407],{"class":1002},[837,11781,950],{"class":924},[837,11783,2718],{"class":854},[837,11785,11414],{"class":937},[837,11787,1011],{"class":862},[837,11789,1114],{"class":924},[837,11791,1106],{"class":937},[837,11793,863],{"class":862},[837,11795,4530],{"class":924},[837,11797,888],{"class":862},[837,11799,5954],{"class":937},[1266,11801,11803],{"id":11802},"_42-the-two-injector-hierarchies",[22,11804,302],{"title":625,"href":301},[594,11806,11807,11808,11811],{},"Angular has two trees, searched in a specific order. Knowing this is the difference between\npredicting a ",[651,11809,11810],{},"NullInjectorError"," and guessing at one.",[1645,11813,11814,11827],{},[1648,11815,11816],{},[1651,11817,11818,11821,11824],{},[1654,11819,11820],{},"Hierarchy",[1654,11822,11823],{},"Configured by",[1654,11825,11826],{},"Lifetime",[1664,11828,11829,11855],{},[1651,11830,11831,11838,11852],{},[1669,11832,11833],{},[597,11834,11835],{},[651,11836,11837],{},"EnvironmentInjector",[1669,11839,11840,679,11842,11845,11846,5402,11849],{},[651,11841,4120],{},[651,11843,11844],{},"ApplicationConfig.providers",", a ",[651,11847,11848],{},"Route",[651,11850,11851],{},"providers",[1669,11853,11854],{},"App, or the route subtree",[1651,11856,11857,11864,11875],{},[1669,11858,11859],{},[597,11860,11861],{},[651,11862,11863],{},"ElementInjector",[1669,11865,11866,2973,11868,1607,11871,2973,11873],{},[651,11867,11851],{},[651,11869,11870],{},"viewProviders",[651,11872,11449],{},[651,11874,11452],{},[1669,11876,11877],{},"The component instance",[594,11879,11880],{},"Resolution order when something requests a dependency:",[14,11882,11883,11893,11901],{},[18,11884,11885,11886,11892],{},"Search ",[597,11887,11888,11889,11891],{},"up the ",[651,11890,11863],{}," tree",", from the requesting component to the root element.",[18,11894,11895,11896,11900],{},"If not found, search ",[597,11897,11888,11898,11891],{},[651,11899,11837],{},", from the nearest environment\ninjector to the root.",[18,11902,11903,11904,1040],{},"If still not found, throw — unless marked ",[651,11905,11906],{},"optional",[594,11908,11909,11910,11913,11914,11917],{},"The practical implication: a component-level provider ",[597,11911,11912],{},"shadows"," a root one for that\ncomponent and its whole subtree. That's the mechanism behind component-scoped stores, and it's\nalso how a well-meaning ",[651,11915,11916],{},"providers: [SomeService]"," accidentally gives a subtree its own copy\nof what should be a singleton.",[1266,11919,11921],{"id":11920},"_43-choosing-a-scope",[22,11922,308],{"title":625,"href":307},[1645,11924,11925,11936],{},[1648,11926,11927],{},[1651,11928,11929,11932,11934],{},[1654,11930,11931],{},"Scope",[1654,11933,7259],{},[1654,11935,6478],{},[1664,11937,11938,11951,11966,11980],{},[1651,11939,11940,11943,11948],{},[1669,11941,11942],{},"Application singleton",[1669,11944,11945],{},[651,11946,11947],{},"@Injectable({ providedIn: 'root' })",[1669,11949,11950],{},"API services, auth, config, logging, shared stores",[1651,11952,11953,11956,11963],{},[1669,11954,11955],{},"Route subtree",[1669,11957,11958,11960,11961],{},[651,11959,7288],{}," on a ",[651,11962,11848],{},[1669,11964,11965],{},"Feature state that should die when the user leaves the feature",[1651,11967,11968,11971,11977],{},[1669,11969,11970],{},"Component instance",[1669,11972,11973,1607,11975],{},[651,11974,7288],{},[651,11976,11449],{},[1669,11978,11979],{},"Component-scoped stores, per-instance form state",[1651,11981,11982,11985,11992],{},[1669,11983,11984],{},"Component view only",[1669,11986,11987,1607,11990],{},[651,11988,11989],{},"viewProviders: []",[651,11991,11449],{},[1669,11993,11994],{},"Same, but hidden from projected content",[594,11996,11997,11999,12000,12003],{},[651,11998,4120],{}," is the right default for stateless services: tree-shakable, zero\nboilerplate, one instance. But ",[597,12001,12002],{},"be deliberate about stateful singletons"," — a root-provided\nstore that holds a selected row is shared by every consumer, whether you meant that or not.",[594,12005,12006,2300,12008,12010,12011,12013,12014,12016,12017,12020,12021,12023,12024,12026],{},[651,12007,11851],{},[651,12009,11870],{}," is a real distinction, not trivia: ",[651,12012,11851],{}," is visible to the\ncomponent, its template, ",[603,12015,4135],{}," anything projected in via ",[651,12018,12019],{},"\u003Cng-content>","; ",[651,12022,11870],{}," hides\nthe service from projected content. Use ",[651,12025,11870],{}," when a service is an internal\nimplementation detail that consumer-supplied content must not reach.",[1266,12028,12030],{"id":12029},"_44-providers-beyond-the-shorthand",[22,12031,314],{"title":625,"href":313},[644,12033,12035],{"className":831,"code":12034,"language":833,"meta":653,"style":653},"providers: [\n  LocalService,                                            \u002F\u002F shorthand for useClass: itself\n  { provide: Logger, useClass: RemoteLogger },              \u002F\u002F swap implementation\n  { provide: API_URL, useValue: '\u002Fapi\u002Fv2' },                \u002F\u002F static value\n  { provide: ApiClient, useFactory: () => new ApiClient(inject(HttpClient)) },  \u002F\u002F computed\n  { provide: LegacyLogger, useExisting: Logger },           \u002F\u002F alias — same instance\n  { provide: VALIDATORS, useClass: LengthValidator, multi: true },  \u002F\u002F contribute to an array\n]\n",[651,12036,12037,12045,12055,12081,12107,12145,12171,12205],{"__ignoreMap":653},[837,12038,12039,12041,12043],{"class":839,"line":840},[837,12040,11851],{"class":5594},[837,12042,870],{"class":862},[837,12044,1368],{"class":937},[837,12046,12047,12050,12052],{"class":839,"line":847},[837,12048,12049],{"class":937},"  LocalService",[837,12051,877],{"class":862},[837,12053,12054],{"class":843},"                                            \u002F\u002F shorthand for useClass: itself\n",[837,12056,12057,12059,12061,12063,12066,12068,12071,12073,12076,12078],{"class":839,"line":894},[837,12058,1920],{"class":862},[837,12060,1546],{"class":937},[837,12062,870],{"class":869},[837,12064,12065],{"class":937}," Logger",[837,12067,877],{"class":862},[837,12069,12070],{"class":937}," useClass",[837,12072,870],{"class":869},[837,12074,12075],{"class":937}," RemoteLogger ",[837,12077,5532],{"class":862},[837,12079,12080],{"class":843},"              \u002F\u002F swap implementation\n",[837,12082,12083,12085,12087,12089,12092,12094,12096,12098,12101,12104],{"class":839,"line":917},[837,12084,1920],{"class":862},[837,12086,1546],{"class":937},[837,12088,870],{"class":869},[837,12090,12091],{"class":1348}," API_URL",[837,12093,877],{"class":862},[837,12095,1556],{"class":937},[837,12097,870],{"class":869},[837,12099,12100],{"class":873}," '\u002Fapi\u002Fv2'",[837,12102,12103],{"class":862}," },",[837,12105,12106],{"class":843},"                \u002F\u002F static value\n",[837,12108,12109,12111,12113,12115,12118,12120,12123,12125,12127,12129,12131,12133,12135,12137,12140,12142],{"class":839,"line":944},[837,12110,1920],{"class":862},[837,12112,1546],{"class":937},[837,12114,870],{"class":869},[837,12116,12117],{"class":937}," ApiClient",[837,12119,877],{"class":862},[837,12121,12122],{"class":854}," useFactory",[837,12124,870],{"class":869},[837,12126,1976],{"class":862},[837,12128,1114],{"class":924},[837,12130,5440],{"class":5439},[837,12132,12117],{"class":854},[837,12134,859],{"class":937},[837,12136,5544],{"class":854},[837,12138,12139],{"class":937},"(HttpClient)) ",[837,12141,5532],{"class":862},[837,12143,12144],{"class":843},"  \u002F\u002F computed\n",[837,12146,12147,12149,12151,12153,12156,12158,12161,12163,12166,12168],{"class":839,"line":959},[837,12148,1920],{"class":862},[837,12150,1546],{"class":937},[837,12152,870],{"class":869},[837,12154,12155],{"class":937}," LegacyLogger",[837,12157,877],{"class":862},[837,12159,12160],{"class":937}," useExisting",[837,12162,870],{"class":869},[837,12164,12165],{"class":937}," Logger ",[837,12167,5532],{"class":862},[837,12169,12170],{"class":843},"           \u002F\u002F alias — same instance\n",[837,12172,12173,12175,12177,12179,12182,12184,12186,12188,12191,12193,12196,12198,12200,12202],{"class":839,"line":984},[837,12174,1920],{"class":862},[837,12176,1546],{"class":937},[837,12178,870],{"class":869},[837,12180,12181],{"class":1348}," VALIDATORS",[837,12183,877],{"class":862},[837,12185,12070],{"class":937},[837,12187,870],{"class":869},[837,12189,12190],{"class":937}," LengthValidator",[837,12192,877],{"class":862},[837,12194,12195],{"class":937}," multi",[837,12197,870],{"class":869},[837,12199,1048],{"class":953},[837,12201,12103],{"class":862},[837,12203,12204],{"class":843},"  \u002F\u002F contribute to an array\n",[837,12206,12207],{"class":839,"line":991},[837,12208,12209],{"class":937},"]\n",[594,12211,12212,12215,12216,12219,12220,12223],{},[651,12213,12214],{},"useExisting"," aliases to the ",[603,12217,12218],{},"same"," instance; ",[651,12221,12222],{},"useClass"," creates a second one. Getting these\nconfused produces two stores where you meant one.",[1266,12225,12227],{"id":12226},"_45-injectiontoken-and-when-not-to-make-one",[22,12228,12229,12230,12232],{"title":625,"href":319},"4.5 ",[651,12231,11476],{}," — and when not to make one",[594,12234,12235,12236,12238],{},"Use an ",[651,12237,11476],{}," for anything that isn't a class: config objects, primitives, functions.\nGive it a default factory so it works without explicit registration:",[644,12240,12242],{"className":831,"code":12241,"language":833,"meta":653,"style":653},"export interface AppConfig {\n  apiUrl: string;\n  pageSize: number;\n}\n\nexport const APP_CONFIG = new InjectionToken\u003CAppConfig>('app.config', {\n  providedIn: 'root',\n  factory: () => ({ apiUrl: '\u002Fapi', pageSize: 50 }),\n});\n",[651,12243,12244,12255,12266,12277,12281,12285,12315,12326,12361],{"__ignoreMap":653},[837,12245,12246,12248,12251,12253],{"class":839,"line":840},[837,12247,898],{"class":897},[837,12249,12250],{"class":897}," interface",[837,12252,1518],{"class":904},[837,12254,914],{"class":862},[837,12256,12257,12260,12262,12264],{"class":839,"line":847},[837,12258,12259],{"class":920},"  apiUrl",[837,12261,870],{"class":924},[837,12263,968],{"class":967},[837,12265,941],{"class":862},[837,12267,12268,12271,12273,12275],{"class":839,"line":894},[837,12269,12270],{"class":920},"  pageSize",[837,12272,870],{"class":924},[837,12274,3769],{"class":967},[837,12276,941],{"class":862},[837,12278,12279],{"class":839,"line":917},[837,12280,1222],{"class":862},[837,12282,12283],{"class":839,"line":944},[837,12284,988],{"emptyLinePlaceholder":987},[837,12286,12287,12289,12291,12293,12295,12297,12299,12301,12304,12306,12308,12311,12313],{"class":839,"line":959},[837,12288,898],{"class":897},[837,12290,1345],{"class":897},[837,12292,1551],{"class":1348},[837,12294,950],{"class":924},[837,12296,5440],{"class":5439},[837,12298,5443],{"class":854},[837,12300,1069],{"class":1068},[837,12302,12303],{"class":904},"AppConfig",[837,12305,1078],{"class":1068},[837,12307,859],{"class":937},[837,12309,12310],{"class":873},"'app.config'",[837,12312,877],{"class":862},[837,12314,914],{"class":862},[837,12316,12317,12320,12322,12324],{"class":839,"line":984},[837,12318,12319],{"class":937},"  providedIn",[837,12321,870],{"class":869},[837,12323,2503],{"class":873},[837,12325,1387],{"class":862},[837,12327,12328,12331,12333,12335,12337,12339,12341,12343,12345,12347,12349,12351,12353,12355,12357,12359],{"class":839,"line":991},[837,12329,12330],{"class":854},"  factory",[837,12332,870],{"class":869},[837,12334,1976],{"class":862},[837,12336,1114],{"class":924},[837,12338,1106],{"class":937},[837,12340,863],{"class":862},[837,12342,1440],{"class":937},[837,12344,870],{"class":869},[837,12346,1445],{"class":873},[837,12348,877],{"class":862},[837,12350,1450],{"class":937},[837,12352,870],{"class":869},[837,12354,1455],{"class":953},[837,12356,888],{"class":862},[837,12358,1011],{"class":937},[837,12360,1387],{"class":862},[837,12362,12363,12365,12367],{"class":839,"line":1017},[837,12364,3505],{"class":862},[837,12366,1011],{"class":937},[837,12368,941],{"class":862},[594,12370,12371,12374],{},[597,12372,12373],{},"Now the counter-rule, because this is where agents over-engineer."," Do not invent a token\nwhen a plain service or a function parameter would do. A token is justified when:",[658,12376,12377,12380,12383,12390],{},[18,12378,12379],{},"the dependency isn't a class (config, primitive, function);",[18,12381,12382],{},"you need multiple competing implementations chosen at runtime;",[18,12384,12385,12386,12389],{},"you're contributing to a ",[651,12387,12388],{},"multi"," collection;",[18,12391,12392,12395,12396,3026,12399,12402,12403,12405],{},[597,12393,12394],{},"you need a test seam"," — a token with a production default factory is far better than\n",[651,12397,12398],{},"vi.mock",[651,12400,12401],{},"jest.mock"," on a shared module, because the override is scoped to ",[651,12404,2799],{}," instead\nof mutating the test runner's module cache and leaking into unrelated suites.",[594,12407,12408,12409,12411],{},"A token is ",[603,12410,2789],{}," justified because indirection feels architectural. The tell-tale smell is a\ntoken with exactly one provider, one consumer, and no test that overrides it — delete it and\ninject the class.",[1266,12413,12415],{"id":12414},"_46-resolution-modifiers",[22,12416,326],{"title":625,"href":325},[644,12418,12420],{"className":831,"code":12419,"language":833,"meta":653,"style":653},"\u002F\u002F null instead of throwing when absent\nreadonly telemetry = inject(TelemetryService, { optional: true });\n\n\u002F\u002F skip this element's providers; start at the parent\nreadonly parentGrid = inject(GridComponent, { skipSelf: true });\n\n\u002F\u002F only this element's injector; don't walk up\nreadonly ownConfig = inject(PANEL_CONFIG, { self: true });\n\n\u002F\u002F stop at the host component's view boundary\nreadonly hostForm = inject(FormContainer, { host: true });\n",[651,12421,12422,12427,12456,12460,12465,12494,12498,12503,12534,12538,12543],{"__ignoreMap":653},[837,12423,12424],{"class":839,"line":840},[837,12425,12426],{"class":843},"\u002F\u002F null instead of throwing when absent\n",[837,12428,12429,12432,12434,12436,12439,12441,12443,12446,12448,12450,12452,12454],{"class":839,"line":847},[837,12430,12431],{"class":937},"readonly telemetry ",[837,12433,934],{"class":924},[837,12435,2718],{"class":854},[837,12437,12438],{"class":937},"(TelemetryService",[837,12440,877],{"class":862},[837,12442,1117],{"class":862},[837,12444,12445],{"class":937}," optional",[837,12447,870],{"class":869},[837,12449,1048],{"class":953},[837,12451,888],{"class":862},[837,12453,1011],{"class":937},[837,12455,941],{"class":862},[837,12457,12458],{"class":839,"line":894},[837,12459,988],{"emptyLinePlaceholder":987},[837,12461,12462],{"class":839,"line":917},[837,12463,12464],{"class":843},"\u002F\u002F skip this element's providers; start at the parent\n",[837,12466,12467,12470,12472,12474,12477,12479,12481,12484,12486,12488,12490,12492],{"class":839,"line":944},[837,12468,12469],{"class":937},"readonly parentGrid ",[837,12471,934],{"class":924},[837,12473,2718],{"class":854},[837,12475,12476],{"class":937},"(GridComponent",[837,12478,877],{"class":862},[837,12480,1117],{"class":862},[837,12482,12483],{"class":937}," skipSelf",[837,12485,870],{"class":869},[837,12487,1048],{"class":953},[837,12489,888],{"class":862},[837,12491,1011],{"class":937},[837,12493,941],{"class":862},[837,12495,12496],{"class":839,"line":959},[837,12497,988],{"emptyLinePlaceholder":987},[837,12499,12500],{"class":839,"line":984},[837,12501,12502],{"class":843},"\u002F\u002F only this element's injector; don't walk up\n",[837,12504,12505,12508,12510,12512,12514,12517,12519,12521,12524,12526,12528,12530,12532],{"class":839,"line":991},[837,12506,12507],{"class":937},"readonly ownConfig ",[837,12509,934],{"class":924},[837,12511,2718],{"class":854},[837,12513,859],{"class":937},[837,12515,12516],{"class":1348},"PANEL_CONFIG",[837,12518,877],{"class":862},[837,12520,1117],{"class":862},[837,12522,12523],{"class":937}," self",[837,12525,870],{"class":869},[837,12527,1048],{"class":953},[837,12529,888],{"class":862},[837,12531,1011],{"class":937},[837,12533,941],{"class":862},[837,12535,12536],{"class":839,"line":1017},[837,12537,988],{"emptyLinePlaceholder":987},[837,12539,12540],{"class":839,"line":1022},[837,12541,12542],{"class":843},"\u002F\u002F stop at the host component's view boundary\n",[837,12544,12545,12548,12550,12552,12555,12557,12559,12562,12564,12566,12568,12570],{"class":839,"line":1033},[837,12546,12547],{"class":937},"readonly hostForm ",[837,12549,934],{"class":924},[837,12551,2718],{"class":854},[837,12553,12554],{"class":937},"(FormContainer",[837,12556,877],{"class":862},[837,12558,1117],{"class":862},[837,12560,12561],{"class":937}," host",[837,12563,870],{"class":869},[837,12565,1048],{"class":953},[837,12567,888],{"class":862},[837,12569,1011],{"class":937},[837,12571,941],{"class":862},[594,12573,12574,12576,12577,12580,12581,12583],{},[651,12575,11906],{}," is the one you'll use most — genuinely useful for optional integrations. The others\nare for directive authors coordinating with ancestors. If application code is reaching for\n",[651,12578,12579],{},"skipSelf"," or ",[651,12582,8131],{},", that's usually a sign the relationship should be an explicit input.",[1266,12585,12587],{"id":12586},"_47-di-is-your-testing-seam",[22,12588,332],{"title":625,"href":331},[594,12590,12591],{},"The reason to care about all of this: DI is what makes the architecture testable without\nmocking frameworks.",[644,12593,12595],{"className":831,"code":12594,"language":833,"meta":653,"style":653},"TestBed.configureTestingModule({\n  providers: [\n    CarriersStore,\n    { provide: CarriersApiService, useValue: { loadTopCarriers: () => of(FIXTURE) } },\n  ],\n});\nconst store = TestBed.inject(CarriersStore);\n",[651,12596,12597,12610,12618,12625,12666,12672,12680],{"__ignoreMap":653},[837,12598,12599,12601,12603,12606,12608],{"class":839,"line":840},[837,12600,2799],{"class":937},[837,12602,1040],{"class":869},[837,12604,12605],{"class":854},"configureTestingModule",[837,12607,859],{"class":937},[837,12609,1095],{"class":862},[837,12611,12612,12614,12616],{"class":839,"line":847},[837,12613,1363],{"class":937},[837,12615,870],{"class":869},[837,12617,1368],{"class":937},[837,12619,12620,12623],{"class":839,"line":894},[837,12621,12622],{"class":937},"    CarriersStore",[837,12624,1387],{"class":862},[837,12626,12627,12629,12631,12633,12635,12637,12639,12641,12643,12646,12648,12650,12652,12655,12657,12660,12662,12664],{"class":839,"line":917},[837,12628,1543],{"class":862},[837,12630,1546],{"class":937},[837,12632,870],{"class":869},[837,12634,3726],{"class":937},[837,12636,877],{"class":862},[837,12638,1556],{"class":937},[837,12640,870],{"class":869},[837,12642,1117],{"class":862},[837,12644,12645],{"class":854}," loadTopCarriers",[837,12647,870],{"class":869},[837,12649,1976],{"class":862},[837,12651,1114],{"class":924},[837,12653,12654],{"class":854}," of",[837,12656,859],{"class":937},[837,12658,12659],{"class":1348},"FIXTURE",[837,12661,2114],{"class":937},[837,12663,3505],{"class":862},[837,12665,1148],{"class":862},[837,12667,12668,12670],{"class":839,"line":944},[837,12669,1466],{"class":937},[837,12671,1387],{"class":862},[837,12673,12674,12676,12678],{"class":839,"line":959},[837,12675,3505],{"class":862},[837,12677,1011],{"class":937},[837,12679,941],{"class":862},[837,12681,12682,12684,12686,12688,12691,12693,12695,12697],{"class":839,"line":984},[837,12683,3442],{"class":897},[837,12685,4896],{"class":1348},[837,12687,950],{"class":924},[837,12689,12690],{"class":937}," TestBed",[837,12692,1040],{"class":869},[837,12694,5544],{"class":854},[837,12696,7688],{"class":937},[837,12698,941],{"class":862},[594,12700,12701,12702,12705,12706,12708],{},"No HTTP mocking, no module mocking, no component mounting. This works ",[603,12703,12704],{},"because"," the store\ndepends on an injected abstraction rather than reaching for ",[651,12707,3516],{}," itself — which is the\nlayering rule from Part 2, paying for itself.",[1266,12710,12712],{"id":12711},"part-4-rules",[22,12713,338],{"title":625,"href":337},[658,12715,12716,12721,12733,12742,12750,12753,12758,12763],{},[18,12717,12718,12720],{},[651,12719,2291],{}," in field initialisers; never constructor params in new code.",[18,12722,12723,12724,12726,12727,12730,12731,1040],{},"Know the four injection contexts; use ",[651,12725,11584],{}," when outside them and\n",[651,12728,12729],{},"assertInInjectionContext"," in helpers that call ",[651,12732,2291],{},[18,12734,12735,12736,3026,12738,3026,12740,1040],{},"Inject store dependencies as default params of ",[651,12737,2785],{},[651,12739,4657],{},[651,12741,7097],{},[18,12743,12744,12746,12747,12749],{},[651,12745,4120],{}," for stateless services; component or route ",[651,12748,11851],{}," for stateful\nscoped things.",[18,12751,12752],{},"Understand that a component provider shadows a root one for its subtree.",[18,12754,12755,12757],{},[651,12756,11870],{}," when projected content must not see the service.",[18,12759,12760,12762],{},[651,12761,11476],{}," for non-class dependencies and test seams — not for decoration. Delete\nsingle-provider, single-consumer, never-overridden tokens.",[18,12764,12765,12766,12768],{},"Prefer DI overrides in ",[651,12767,2799],{}," over module mocks.",[617,12770],{},[620,12772,12774],{"id":12773},"part-5-auth-security-architecture",[22,12775,344],{"title":625,"href":343},[594,12777,12778],{},"Component\u002Fstore\u002Fservice layering makes this tractable: auth is state (who is signed in, what\nthey can do), so it lives in a store like everything else. The parts that are genuinely\ndifferent from a feature store are token handling, request-level enforcement, and the fact\nthat a mistake here is a security incident, not a bug ticket.",[1266,12780,12782],{"id":12781},"_51-where-auth-state-lives",[22,12783,352],{"title":625,"href":351},[594,12785,12786,12787,12790],{},"A root-provided ",[651,12788,12789],{},"AuthStore"," — the one legitimate case for a singleton stateful store, because\nthe whole app needs to observe it:",[644,12792,12794],{"className":831,"code":12793,"language":833,"meta":653,"style":653},"\u002F\u002F auth.store.ts\ninterface AuthState {\n  user: User | null;\n  accessToken: string | null;   \u002F\u002F in memory only — see below\n}\n\nexport const AuthStore = signalStore(\n  { providedIn: 'root' },\n  withState\u003CAuthState>({ user: null, accessToken: null }),\n  withComputed((store) => ({\n    isAuthenticated: computed(() => store.user() !== null),\n    permissions: computed(() => store.user()?.permissions ?? []),\n  })),\n  withMethods((store, auth = inject(AuthApiService)) => ({\n    login: rxMethod\u003CCredentials>(\n      pipe(\n        exhaustMap((credentials) =>\n          auth.login(credentials).pipe(\n            tapResponse({\n              next: ({ user, accessToken }) => patchState(store, { user, accessToken }),\n              error: () => patchState(store, { user: null, accessToken: null }),\n            })\n          )\n        )\n      )\n    ),\n    logout: () => patchState(store, { user: null, accessToken: null }),\n  })),\n  withLogger('AuthStore'),\n);\n",[651,12795,12796,12801,12810,12826,12844,12848,12852,12867,12879,12915,12933,12966,13001,13009,13039,13057,13064,13080,13099,13108,13148,13187,13194,13199,13204,13209,13215,13254,13262,13275],{"__ignoreMap":653},[837,12797,12798],{"class":839,"line":840},[837,12799,12800],{"class":843},"\u002F\u002F auth.store.ts\n",[837,12802,12803,12805,12808],{"class":839,"line":847},[837,12804,4379],{"class":897},[837,12806,12807],{"class":904}," AuthState",[837,12809,914],{"class":862},[837,12811,12812,12815,12817,12820,12822,12824],{"class":839,"line":894},[837,12813,12814],{"class":920},"  user",[837,12816,870],{"class":924},[837,12818,12819],{"class":904}," User",[837,12821,971],{"class":924},[837,12823,975],{"class":974},[837,12825,941],{"class":862},[837,12827,12828,12831,12833,12835,12837,12839,12841],{"class":839,"line":917},[837,12829,12830],{"class":920},"  accessToken",[837,12832,870],{"class":924},[837,12834,968],{"class":967},[837,12836,971],{"class":924},[837,12838,975],{"class":974},[837,12840,1133],{"class":862},[837,12842,12843],{"class":843},"   \u002F\u002F in memory only — see below\n",[837,12845,12846],{"class":839,"line":944},[837,12847,1222],{"class":862},[837,12849,12850],{"class":839,"line":959},[837,12851,988],{"emptyLinePlaceholder":987},[837,12853,12854,12856,12858,12861,12863,12865],{"class":839,"line":984},[837,12855,898],{"class":897},[837,12857,1345],{"class":897},[837,12859,12860],{"class":1348}," AuthStore",[837,12862,950],{"class":924},[837,12864,4481],{"class":854},[837,12866,4484],{"class":937},[837,12868,12869,12871,12873,12875,12877],{"class":839,"line":991},[837,12870,1920],{"class":862},[837,12872,2498],{"class":937},[837,12874,870],{"class":869},[837,12876,2503],{"class":873},[837,12878,1148],{"class":862},[837,12880,12881,12883,12885,12888,12890,12892,12894,12896,12898,12900,12902,12905,12907,12909,12911,12913],{"class":839,"line":1017},[837,12882,4489],{"class":854},[837,12884,1069],{"class":1068},[837,12886,12887],{"class":904},"AuthState",[837,12889,1078],{"class":1068},[837,12891,859],{"class":937},[837,12893,863],{"class":862},[837,12895,9549],{"class":937},[837,12897,870],{"class":869},[837,12899,975],{"class":967},[837,12901,877],{"class":862},[837,12903,12904],{"class":937}," accessToken",[837,12906,870],{"class":869},[837,12908,975],{"class":967},[837,12910,888],{"class":862},[837,12912,1011],{"class":937},[837,12914,1387],{"class":862},[837,12916,12917,12919,12921,12923,12925,12927,12929,12931],{"class":839,"line":1022},[837,12918,4545],{"class":854},[837,12920,859],{"class":937},[837,12922,859],{"class":862},[837,12924,4552],{"class":1002},[837,12926,1011],{"class":862},[837,12928,1114],{"class":924},[837,12930,1106],{"class":937},[837,12932,1095],{"class":862},[837,12934,12935,12938,12940,12942,12944,12946,12948,12950,12952,12955,12957,12960,12962,12964],{"class":839,"line":1033},[837,12936,12937],{"class":937},"    isAuthenticated",[837,12939,870],{"class":869},[837,12941,4887],{"class":854},[837,12943,859],{"class":937},[837,12945,1028],{"class":862},[837,12947,1114],{"class":924},[837,12949,4896],{"class":937},[837,12951,1040],{"class":869},[837,12953,12954],{"class":854},"user",[837,12956,5839],{"class":937},[837,12958,12959],{"class":924},"!==",[837,12961,975],{"class":967},[837,12963,1011],{"class":937},[837,12965,1387],{"class":862},[837,12967,12968,12971,12973,12975,12977,12979,12981,12983,12985,12987,12989,12991,12994,12996,12999],{"class":839,"line":1053},[837,12969,12970],{"class":937},"    permissions",[837,12972,870],{"class":869},[837,12974,4887],{"class":854},[837,12976,859],{"class":937},[837,12978,1028],{"class":862},[837,12980,1114],{"class":924},[837,12982,4896],{"class":937},[837,12984,1040],{"class":869},[837,12986,12954],{"class":854},[837,12988,1028],{"class":937},[837,12990,9996],{"class":869},[837,12992,12993],{"class":937},"permissions ",[837,12995,2598],{"class":924},[837,12997,12998],{"class":937}," [])",[837,13000,1387],{"class":862},[837,13002,13003,13005,13007],{"class":839,"line":1098},[837,13004,5172],{"class":862},[837,13006,4567],{"class":937},[837,13008,1387],{"class":862},[837,13010,13011,13013,13015,13017,13019,13021,13024,13026,13028,13031,13033,13035,13037],{"class":839,"line":1151},[837,13012,4578],{"class":854},[837,13014,859],{"class":937},[837,13016,859],{"class":862},[837,13018,4552],{"class":1002},[837,13020,877],{"class":862},[837,13022,13023],{"class":1002}," auth",[837,13025,950],{"class":924},[837,13027,2718],{"class":854},[837,13029,13030],{"class":937},"(AuthApiService)",[837,13032,1011],{"class":862},[837,13034,1114],{"class":924},[837,13036,1106],{"class":937},[837,13038,1095],{"class":862},[837,13040,13041,13044,13046,13048,13050,13053,13055],{"class":839,"line":1203},[837,13042,13043],{"class":937},"    login",[837,13045,870],{"class":869},[837,13047,5126],{"class":854},[837,13049,1069],{"class":1068},[837,13051,13052],{"class":904},"Credentials",[837,13054,1078],{"class":1068},[837,13056,4484],{"class":937},[837,13058,13059,13062],{"class":839,"line":1213},[837,13060,13061],{"class":854},"      pipe",[837,13063,4484],{"class":937},[837,13065,13066,13069,13071,13073,13076,13078],{"class":839,"line":1219},[837,13067,13068],{"class":854},"        exhaustMap",[837,13070,859],{"class":937},[837,13072,859],{"class":862},[837,13074,13075],{"class":1002},"credentials",[837,13077,1011],{"class":862},[837,13079,4932],{"class":924},[837,13081,13082,13085,13087,13090,13093,13095,13097],{"class":839,"line":3975},[837,13083,13084],{"class":937},"          auth",[837,13086,1040],{"class":869},[837,13088,13089],{"class":854},"login",[837,13091,13092],{"class":937},"(credentials)",[837,13094,1040],{"class":869},[837,13096,3898],{"class":854},[837,13098,4484],{"class":937},[837,13100,13101,13104,13106],{"class":839,"line":4039},[837,13102,13103],{"class":854},"            tapResponse",[837,13105,859],{"class":937},[837,13107,1095],{"class":862},[837,13109,13110,13113,13115,13117,13119,13121,13123,13125,13127,13129,13131,13133,13135,13137,13139,13142,13144,13146],{"class":839,"line":4507},[837,13111,13112],{"class":854},"              next",[837,13114,870],{"class":869},[837,13116,8394],{"class":862},[837,13118,9549],{"class":1002},[837,13120,877],{"class":862},[837,13122,12904],{"class":1002},[837,13124,5813],{"class":862},[837,13126,1114],{"class":924},[837,13128,6278],{"class":854},[837,13130,5518],{"class":937},[837,13132,877],{"class":862},[837,13134,1117],{"class":862},[837,13136,9549],{"class":937},[837,13138,877],{"class":862},[837,13140,13141],{"class":937}," accessToken ",[837,13143,3505],{"class":862},[837,13145,1011],{"class":937},[837,13147,1387],{"class":862},[837,13149,13150,13153,13155,13157,13159,13161,13163,13165,13167,13169,13171,13173,13175,13177,13179,13181,13183,13185],{"class":839,"line":4520},[837,13151,13152],{"class":854},"              error",[837,13154,870],{"class":869},[837,13156,1976],{"class":862},[837,13158,1114],{"class":924},[837,13160,6278],{"class":854},[837,13162,5518],{"class":937},[837,13164,877],{"class":862},[837,13166,1117],{"class":862},[837,13168,9549],{"class":937},[837,13170,870],{"class":869},[837,13172,975],{"class":967},[837,13174,877],{"class":862},[837,13176,12904],{"class":937},[837,13178,870],{"class":869},[837,13180,975],{"class":967},[837,13182,888],{"class":862},[837,13184,1011],{"class":937},[837,13186,1387],{"class":862},[837,13188,13189,13192],{"class":839,"line":4542},[837,13190,13191],{"class":862},"            }",[837,13193,891],{"class":937},[837,13195,13196],{"class":839,"line":4575},[837,13197,13198],{"class":937},"          )\n",[837,13200,13201],{"class":839,"line":4606},[837,13202,13203],{"class":937},"        )\n",[837,13205,13206],{"class":839,"line":4627},[837,13207,13208],{"class":937},"      )\n",[837,13210,13211,13213],{"class":839,"line":4645},[837,13212,5368],{"class":937},[837,13214,1387],{"class":862},[837,13216,13217,13220,13222,13224,13226,13228,13230,13232,13234,13236,13238,13240,13242,13244,13246,13248,13250,13252],{"class":839,"line":7969},[837,13218,13219],{"class":854},"    logout",[837,13221,870],{"class":869},[837,13223,1976],{"class":862},[837,13225,1114],{"class":924},[837,13227,6278],{"class":854},[837,13229,5518],{"class":937},[837,13231,877],{"class":862},[837,13233,1117],{"class":862},[837,13235,9549],{"class":937},[837,13237,870],{"class":869},[837,13239,975],{"class":967},[837,13241,877],{"class":862},[837,13243,12904],{"class":937},[837,13245,870],{"class":869},[837,13247,975],{"class":967},[837,13249,888],{"class":862},[837,13251,1011],{"class":937},[837,13253,1387],{"class":862},[837,13255,13256,13258,13260],{"class":839,"line":7974},[837,13257,5172],{"class":862},[837,13259,4567],{"class":937},[837,13261,1387],{"class":862},[837,13263,13264,13266,13268,13271,13273],{"class":839,"line":7998},[837,13265,4630],{"class":854},[837,13267,859],{"class":937},[837,13269,13270],{"class":873},"'AuthStore'",[837,13272,1011],{"class":937},[837,13274,1387],{"class":862},[837,13276,13277,13279],{"class":839,"line":8017},[837,13278,1011],{"class":937},[837,13280,941],{"class":862},[594,13282,13283,13284,13286],{},"Role\u002Fpermission checks are a ",[651,13285,682],{}," off this store, never a scattered string comparison\nin a template or component:",[644,13288,13290],{"className":831,"code":13289,"language":833,"meta":653,"style":653},"readonly canEditCarriers = computed(() => store.permissions().includes('carriers:write'));\n",[651,13291,13292],{"__ignoreMap":653},[837,13293,13294,13297,13299,13301,13303,13305,13307,13309,13311,13314,13316,13318,13320,13322,13325,13327],{"class":839,"line":840},[837,13295,13296],{"class":937},"readonly canEditCarriers ",[837,13298,934],{"class":924},[837,13300,4887],{"class":854},[837,13302,859],{"class":937},[837,13304,1028],{"class":862},[837,13306,1114],{"class":924},[837,13308,4896],{"class":937},[837,13310,1040],{"class":869},[837,13312,13313],{"class":854},"permissions",[837,13315,1028],{"class":937},[837,13317,1040],{"class":869},[837,13319,10310],{"class":854},[837,13321,859],{"class":937},[837,13323,13324],{"class":873},"'carriers:write'",[837,13326,4567],{"class":937},[837,13328,941],{"class":862},[1266,13330,13332],{"id":13331},"_52-token-storage-the-single-highest-stakes-decision-here",[22,13333,358],{"title":625,"href":357},[3289,13335,13336],{},[594,13337,13338,13346,13347,13350],{},[597,13339,13340,13341,12580,13343,1040],{},"Never put an access or refresh token in ",[651,13342,3225],{},[651,13344,13345],{},"sessionStorage"," Both are\nplain synchronous JS APIs — any XSS on the page, including a compromised third-party\nscript, can read them and exfiltrate the session. This is the most common real-world\nAngular security defect, and it ships because ",[651,13348,13349],{},"localStorage.setItem"," is the path of least\nresistance, not because anyone decided it was safe.",[594,13352,13353],{},"Two acceptable patterns, in order of preference:",[14,13355,13356,13372],{},[18,13357,13358,13364,13365,12580,13368,13371],{},[597,13359,13360,13363],{},[651,13361,13362],{},"httpOnly"," cookie, set by the server on login."," JavaScript never touches the token at\nall; the browser attaches it automatically. This is the correct default for a first-party\napp talking to its own backend. Pair it with ",[651,13366,13367],{},"SameSite=Strict",[651,13369,13370],{},"Lax"," and a CSRF\ndefense (§5.4), since cookies are sent automatically on cross-site requests too.",[18,13373,13374,13379,13380,13382],{},[597,13375,13376,13377,1040],{},"In-memory only, as a signal in ",[651,13378,12789],{}," If the backend can't set cookies (a\nseparate API domain, a mobile-shared auth server), hold the access token in a signal and\nnowhere else. It's lost on full page reload, which is the point — pair it with a\n",[651,13381,13362],{}," refresh cookie so a reload can silently re-establish the session without ever\npersisting the access token to disk.",[594,13384,13385,13386,13389,13390,13392],{},"Never persist a token by hand-rolling ",[651,13387,13388],{},"JSON.stringify"," into ",[651,13391,3225],{}," \"just for the\nrefresh token, that one's fine\" — a leaked refresh token is a full session compromise with a\nlonger blast radius than a leaked access token.",[1266,13394,13396],{"id":13395},"_53-the-auth-interceptor-and-the-refresh-race-every-implementation-gets-wrong",[22,13397,364],{"title":625,"href":363},[594,13399,13400],{},"Attaching the token is the easy part:",[644,13402,13403],{"className":831,"code":4180,"language":833,"meta":653,"style":653},[651,13404,13405,13433,13453,13505],{"__ignoreMap":653},[837,13406,13407,13409,13411,13413,13415,13417,13419,13421,13423,13425,13427,13429,13431],{"class":839,"line":840},[837,13408,898],{"class":897},[837,13410,1345],{"class":897},[837,13412,4191],{"class":854},[837,13414,870],{"class":924},[837,13416,4196],{"class":904},[837,13418,950],{"class":924},[837,13420,1106],{"class":862},[837,13422,4203],{"class":1002},[837,13424,877],{"class":862},[837,13426,4208],{"class":1002},[837,13428,1011],{"class":862},[837,13430,1114],{"class":924},[837,13432,914],{"class":862},[837,13434,13435,13437,13439,13441,13443,13445,13447,13449,13451],{"class":839,"line":847},[837,13436,4219],{"class":897},[837,13438,4222],{"class":1348},[837,13440,950],{"class":924},[837,13442,2718],{"class":854},[837,13444,4229],{"class":937},[837,13446,1040],{"class":869},[837,13448,4234],{"class":854},[837,13450,1028],{"class":937},[837,13452,941],{"class":862},[837,13454,13455,13457,13459,13461,13463,13465,13467,13469,13471,13473,13475,13477,13479,13481,13483,13485,13487,13489,13491,13493,13495,13497,13499,13501,13503],{"class":839,"line":894},[837,13456,1532],{"class":897},[837,13458,4208],{"class":854},[837,13460,4247],{"class":937},[837,13462,4250],{"class":924},[837,13464,4253],{"class":937},[837,13466,1040],{"class":869},[837,13468,4258],{"class":854},[837,13470,859],{"class":937},[837,13472,863],{"class":862},[837,13474,4265],{"class":937},[837,13476,870],{"class":869},[837,13478,1117],{"class":862},[837,13480,4272],{"class":937},[837,13482,870],{"class":869},[837,13484,4277],{"class":873},[837,13486,4281],{"class":4280},[837,13488,4284],{"class":937},[837,13490,3505],{"class":4280},[837,13492,4289],{"class":873},[837,13494,888],{"class":862},[837,13496,888],{"class":862},[837,13498,2114],{"class":937},[837,13500,870],{"class":924},[837,13502,4300],{"class":937},[837,13504,941],{"class":862},[837,13506,13507],{"class":839,"line":917},[837,13508,1473],{"class":862},[594,13510,13511,13512,13515],{},"The part that's easy to get wrong is refreshing on a ",[651,13513,13514],{},"401",". A naive implementation lets every\nin-flight request independently detect the expiry and independently call the refresh endpoint\n— a \"refresh storm\" that can invalidate the very token the other requests are about to retry\nwith. Share one in-flight refresh:",[644,13517,13519],{"className":831,"code":13518,"language":833,"meta":653,"style":653},"export const refreshInterceptor: HttpInterceptorFn = (req, next) => {\n  const auth = inject(AuthStore);\n  return next(req).pipe(\n    catchError((error: HttpErrorResponse) => {\n      if (error.status !== 401 || req.url.includes('\u002Fauth\u002Frefresh')) throw error;\n      return auth.refresh$().pipe(\n        switchMap(() => next(req)), \u002F\u002F retry the original request once, with the new token\n      );\n    })\n  );\n};\n",[651,13520,13521,13550,13564,13579,13601,13648,13668,13689,13696,13702,13708],{"__ignoreMap":653},[837,13522,13523,13525,13527,13530,13532,13534,13536,13538,13540,13542,13544,13546,13548],{"class":839,"line":840},[837,13524,898],{"class":897},[837,13526,1345],{"class":897},[837,13528,13529],{"class":854}," refreshInterceptor",[837,13531,870],{"class":924},[837,13533,4196],{"class":904},[837,13535,950],{"class":924},[837,13537,1106],{"class":862},[837,13539,4203],{"class":1002},[837,13541,877],{"class":862},[837,13543,4208],{"class":1002},[837,13545,1011],{"class":862},[837,13547,1114],{"class":924},[837,13549,914],{"class":862},[837,13551,13552,13554,13556,13558,13560,13562],{"class":839,"line":847},[837,13553,4219],{"class":897},[837,13555,13023],{"class":1348},[837,13557,950],{"class":924},[837,13559,2718],{"class":854},[837,13561,4229],{"class":937},[837,13563,941],{"class":862},[837,13565,13566,13568,13570,13573,13575,13577],{"class":839,"line":894},[837,13567,1532],{"class":897},[837,13569,4208],{"class":854},[837,13571,13572],{"class":937},"(req)",[837,13574,1040],{"class":869},[837,13576,3898],{"class":854},[837,13578,4484],{"class":937},[837,13580,13581,13584,13586,13588,13590,13592,13595,13597,13599],{"class":839,"line":917},[837,13582,13583],{"class":854},"    catchError",[837,13585,859],{"class":937},[837,13587,859],{"class":862},[837,13589,1312],{"class":1002},[837,13591,870],{"class":924},[837,13593,13594],{"class":904}," HttpErrorResponse",[837,13596,1011],{"class":862},[837,13598,1114],{"class":924},[837,13600,914],{"class":862},[837,13602,13603,13606,13609,13611,13614,13616,13619,13622,13624,13626,13629,13631,13633,13635,13638,13641,13644,13646],{"class":839,"line":944},[837,13604,13605],{"class":897},"      if",[837,13607,13608],{"class":937}," (error",[837,13610,1040],{"class":869},[837,13612,13613],{"class":937},"status ",[837,13615,12959],{"class":924},[837,13617,13618],{"class":953}," 401",[837,13620,13621],{"class":924}," ||",[837,13623,4253],{"class":937},[837,13625,1040],{"class":869},[837,13627,13628],{"class":937},"url",[837,13630,1040],{"class":869},[837,13632,10310],{"class":854},[837,13634,859],{"class":937},[837,13636,13637],{"class":873},"'\u002Fauth\u002Frefresh'",[837,13639,13640],{"class":937},")) ",[837,13642,13643],{"class":897},"throw",[837,13645,5729],{"class":937},[837,13647,941],{"class":862},[837,13649,13650,13653,13655,13657,13660,13662,13664,13666],{"class":839,"line":959},[837,13651,13652],{"class":897},"      return",[837,13654,13023],{"class":937},[837,13656,1040],{"class":869},[837,13658,13659],{"class":854},"refresh$",[837,13661,1028],{"class":937},[837,13663,1040],{"class":869},[837,13665,3898],{"class":854},[837,13667,4484],{"class":937},[837,13669,13670,13673,13675,13677,13679,13681,13684,13686],{"class":839,"line":984},[837,13671,13672],{"class":854},"        switchMap",[837,13674,859],{"class":937},[837,13676,1028],{"class":862},[837,13678,1114],{"class":924},[837,13680,4208],{"class":854},[837,13682,13683],{"class":937},"(req))",[837,13685,877],{"class":862},[837,13687,13688],{"class":843}," \u002F\u002F retry the original request once, with the new token\n",[837,13690,13691,13694],{"class":839,"line":991},[837,13692,13693],{"class":937},"      )",[837,13695,941],{"class":862},[837,13697,13698,13700],{"class":839,"line":1017},[837,13699,1206],{"class":862},[837,13701,891],{"class":937},[837,13703,13704,13706],{"class":839,"line":1022},[837,13705,4985],{"class":937},[837,13707,941],{"class":862},[837,13709,13710],{"class":839,"line":1033},[837,13711,1473],{"class":862},[594,13713,13714,13717,13718,13720,13721,13724],{},[651,13715,13716],{},"refresh$()"," on the store should be the ",[603,13719,12218],{}," shared Observable handed to every caller\nduring the refresh window (an RxJS ",[651,13722,13723],{},"share({ connector: () => new ReplaySubject(1), resetOnError: true })","\nor equivalent) — not a new HTTP call per failed request. If refresh itself fails, clear the\nsession and redirect to login; don't retry a refresh failure.",[1266,13726,13728],{"id":13727},"_54-guards-enforce-ux-the-server-enforces-security",[22,13729,370],{"title":625,"href":369},[594,13731,13732,13733,13736,13737,3104,13739,13741,13742,13745,13746,13749,13750,3026,13753,13756],{},"This is worth repeating from ",[22,13734,13735],{"href":72},"Part 1"," because it\nmatters most here: ",[651,13738,2303],{},[651,13740,2299],{}," decide what the UI ",[603,13743,13744],{},"shows",", not what the user\nis ",[603,13747,13748],{},"allowed to do",". Every permission check in a guard, a ",[651,13751,13752],{},"*ngIf",[651,13754,13755],{},"@if",", or a disabled button\nmust be re-checked server-side. An agent asked to \"add an admin-only route\" will happily write\na guard and consider the feature secure — it isn't, until the API also rejects the request.",[644,13758,13760],{"className":831,"code":13759,"language":833,"meta":653,"style":653},"{\n  path: 'admin',\n  loadChildren: () => import('.\u002Ffeatures\u002Fadmin\u002Fadmin.routes'),\n  canActivate: [authGuard, permissionGuard('admin:access')],  \u002F\u002F UX gate\n  \u002F\u002F The \u002Fapi\u002Fadmin\u002F* endpoints must independently reject non-admin tokens.\n}\n",[651,13761,13762,13766,13777,13798,13824,13829],{"__ignoreMap":653},[837,13763,13764],{"class":839,"line":840},[837,13765,1095],{"class":862},[837,13767,13768,13771,13773,13775],{"class":839,"line":847},[837,13769,13770],{"class":5594},"  path",[837,13772,870],{"class":862},[837,13774,2031],{"class":873},[837,13776,1387],{"class":862},[837,13778,13779,13782,13784,13786,13788,13790,13792,13794,13796],{"class":839,"line":894},[837,13780,13781],{"class":5594},"  loadChildren",[837,13783,870],{"class":862},[837,13785,1976],{"class":862},[837,13787,1114],{"class":924},[837,13789,1981],{"class":924},[837,13791,859],{"class":937},[837,13793,2050],{"class":873},[837,13795,1011],{"class":937},[837,13797,1387],{"class":862},[837,13799,13800,13803,13805,13807,13809,13812,13814,13817,13819,13821],{"class":839,"line":917},[837,13801,13802],{"class":5594},"  canActivate",[837,13804,870],{"class":862},[837,13806,2064],{"class":937},[837,13808,877],{"class":862},[837,13810,13811],{"class":854}," permissionGuard",[837,13813,859],{"class":937},[837,13815,13816],{"class":873},"'admin:access'",[837,13818,2011],{"class":937},[837,13820,877],{"class":862},[837,13822,13823],{"class":843},"  \u002F\u002F UX gate\n",[837,13825,13826],{"class":839,"line":944},[837,13827,13828],{"class":843},"  \u002F\u002F The \u002Fapi\u002Fadmin\u002F* endpoints must independently reject non-admin tokens.\n",[837,13830,13831],{"class":839,"line":959},[837,13832,1222],{"class":862},[594,13834,13835],{},"A conditional-rendering directive follows the same rule — it hides a button, it doesn't\nsecure an action:",[644,13837,13839],{"className":831,"code":13838,"language":833,"meta":653,"style":653},"@if (store.canEditCarriers()) {\n  \u003Cbutton (click)=\"edit()\">Edit\u003C\u002Fbutton>\n}\n",[651,13840,13841,13860,13887],{"__ignoreMap":653},[837,13842,13843,13845,13848,13851,13853,13856,13858],{"class":839,"line":840},[837,13844,851],{"class":850},[837,13846,13847],{"class":854},"if",[837,13849,13850],{"class":858}," (store",[837,13852,1040],{"class":869},[837,13854,13855],{"class":854},"canEditCarriers",[837,13857,1384],{"class":858},[837,13859,914],{"class":862},[837,13861,13862,13864,13867,13870,13872,13875,13877,13880,13883,13885],{"class":839,"line":847},[837,13863,2842],{"class":924},[837,13865,13866],{"class":854},"button",[837,13868,13869],{"class":937}," (click)",[837,13871,934],{"class":924},[837,13873,13874],{"class":873},"\"edit()\"",[837,13876,1078],{"class":924},[837,13878,13879],{"class":937},"Edit",[837,13881,13882],{"class":924},"\u003C\u002F",[837,13884,13866],{"class":937},[837,13886,2885],{"class":924},[837,13888,13889],{"class":839,"line":894},[837,13890,1222],{"class":862},[594,13892,13893,13896,13897,13899],{},[597,13894,13895],{},"CSRF"," is only relevant to cookie-based auth — a pure bearer-token-in-header scheme has no\nCSRF surface, because a cross-site form or image tag can't set a custom header. If you're\nusing cookies, Angular's ",[651,13898,3516],{}," has built-in double-submit-cookie support:",[644,13901,13903],{"className":831,"code":13902,"language":833,"meta":653,"style":653},"provideHttpClient(\n  withXsrfConfiguration({ cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN' }),\n),\n",[651,13904,13905,13911,13944],{"__ignoreMap":653},[837,13906,13907,13909],{"class":839,"line":840},[837,13908,1492],{"class":854},[837,13910,4484],{"class":937},[837,13912,13913,13916,13918,13920,13923,13925,13928,13930,13933,13935,13938,13940,13942],{"class":839,"line":847},[837,13914,13915],{"class":854},"  withXsrfConfiguration",[837,13917,859],{"class":937},[837,13919,863],{"class":862},[837,13921,13922],{"class":937}," cookieName",[837,13924,870],{"class":869},[837,13926,13927],{"class":873}," 'XSRF-TOKEN'",[837,13929,877],{"class":862},[837,13931,13932],{"class":937}," headerName",[837,13934,870],{"class":869},[837,13936,13937],{"class":873}," 'X-XSRF-TOKEN'",[837,13939,888],{"class":862},[837,13941,1011],{"class":937},[837,13943,1387],{"class":862},[837,13945,13946,13948],{"class":839,"line":894},[837,13947,1011],{"class":937},[837,13949,1387],{"class":862},[594,13951,13952,13953,13956,13957,13959],{},"The server must set the readable ",[651,13954,13955],{},"XSRF-TOKEN"," cookie (not ",[651,13958,13362],{},", deliberately — the\nclient needs to read it and echo it as a header) and reject requests where the header is\nmissing or doesn't match the cookie.",[1266,13961,13963],{"id":13962},"_55-csp-and-trusted-types",[22,13964,376],{"title":625,"href":375},[594,13966,13967],{},"A strict Content-Security-Policy is the single highest-leverage defense against XSS\nconsequences, because it works even if a sanitization bug slips through review. In practice:",[658,13969,13970,13989,14001],{},[18,13971,13972,13973,13976,13977,13980,13981,13984,13985,13988],{},"Serve a ",[651,13974,13975],{},"Content-Security-Policy"," header (or ",[651,13978,13979],{},"\u003Cmeta>"," tag) that disallows ",[651,13982,13983],{},"unsafe-inline","\nand ",[651,13986,13987],{},"unsafe-eval"," for scripts.",[18,13990,13991,13992,5402,13994,13997,13998,14000],{},"Angular's production build emits no inline scripts by default; if you add one (a bootstrap\nperf snippet, a third-party embed), it needs a nonce, and the nonce must be threaded through\n",[651,13993,3307],{},[651,13995,13996],{},"ngCspNonce"," or an ",[651,13999,13996],{}," attribute on the root element — not\nhand-added per script tag.",[18,14002,14003,14004,14007,14008,14011,14012,679,14015,14018,14019,14022],{},"Consider a ",[651,14005,14006],{},"Trusted Types"," policy (",[651,14009,14010],{},"require-trusted-types-for 'script'",") in security-critical\napps. Angular's built-in sanitizer is Trusted-Types-aware; this closes off the DOM-XSS\nsinks (",[651,14013,14014],{},"innerHTML",[651,14016,14017],{},"eval",", script ",[651,14020,14021],{},"src",") at the browser level, not just in your own code.",[1266,14024,14026],{"id":14025},"_56-sanitization-and-untrusted-data-the-boundary-rule-applied",[22,14027,382],{"title":625,"href":381},[594,14029,14030,14031,14034,14035,14037,14038,14041,14042,679,14044,14047],{},"The anti-pattern catalogue already flags ",[651,14032,14033],{},"bypassSecurityTrust*"," and raw ",[651,14036,14014],{},". The rule\nunderneath it: ",[597,14039,14040],{},"treat every external input as untrusted at the point it enters the app"," —\nAPI responses, route\u002Fquery params, ",[651,14043,3225],{},[651,14045,14046],{},"postMessage",", pasted rich text — and\nnarrow or sanitize it once, at that boundary (typically the service's DTO-mapping function).\nA component or store that re-validates the same field five times downstream is a sign the\nboundary was drawn in the wrong place.",[644,14049,14051],{"className":831,"code":14050,"language":833,"meta":653,"style":653},"\u002F\u002F service — the boundary\nfunction toCarrier(dto: unknown): Carrier {\n  const parsed = carrierSchema.parse(dto); \u002F\u002F zod, or an equivalent runtime validator\n  return { id: parsed.id, name: parsed.name, riskLevel: parsed.risk_level };\n}\n",[651,14052,14053,14058,14081,14106,14149],{"__ignoreMap":653},[837,14054,14055],{"class":839,"line":840},[837,14056,14057],{"class":843},"\u002F\u002F service — the boundary\n",[837,14059,14060,14062,14064,14066,14068,14070,14073,14075,14077,14079],{"class":839,"line":847},[837,14061,3952],{"class":897},[837,14063,3955],{"class":854},[837,14065,859],{"class":862},[837,14067,3960],{"class":1002},[837,14069,870],{"class":924},[837,14071,14072],{"class":967}," unknown",[837,14074,1011],{"class":862},[837,14076,870],{"class":924},[837,14078,927],{"class":904},[837,14080,914],{"class":862},[837,14082,14083,14085,14088,14090,14093,14095,14098,14101,14103],{"class":839,"line":894},[837,14084,4219],{"class":897},[837,14086,14087],{"class":1348}," parsed",[837,14089,950],{"class":924},[837,14091,14092],{"class":937}," carrierSchema",[837,14094,1040],{"class":869},[837,14096,14097],{"class":854},"parse",[837,14099,14100],{"class":937},"(dto)",[837,14102,1133],{"class":862},[837,14104,14105],{"class":843}," \u002F\u002F zod, or an equivalent runtime validator\n",[837,14107,14108,14110,14112,14114,14116,14118,14120,14122,14124,14126,14128,14130,14132,14134,14136,14139,14141,14143,14145,14147],{"class":839,"line":917},[837,14109,1532],{"class":897},[837,14111,1117],{"class":862},[837,14113,3982],{"class":937},[837,14115,870],{"class":869},[837,14117,14087],{"class":937},[837,14119,1040],{"class":869},[837,14121,3992],{"class":937},[837,14123,877],{"class":862},[837,14125,3997],{"class":937},[837,14127,870],{"class":869},[837,14129,14087],{"class":937},[837,14131,1040],{"class":869},[837,14133,4006],{"class":937},[837,14135,877],{"class":862},[837,14137,14138],{"class":937}," riskLevel",[837,14140,870],{"class":869},[837,14142,14087],{"class":937},[837,14144,1040],{"class":869},[837,14146,4034],{"class":937},[837,14148,1473],{"class":862},[837,14150,14151],{"class":839,"line":944},[837,14152,1222],{"class":862},[594,14154,14155,14156,14158,14159,14162,14163,14166],{},"If the app must render user-supplied HTML (rich text, markdown-to-HTML), sanitize with a\nlibrary built for it (DOMPurify) ",[603,14157,10562],{}," the ",[651,14160,14161],{},"[innerHTML]"," binding, and keep that one call\nsite auditable rather than sprinkling ",[651,14164,14165],{},"bypassSecurityTrustHtml"," around the codebase.",[1266,14168,14170],{"id":14169},"_57-secrets-never-ship-in-the-client-bundle",[22,14171,388],{"title":625,"href":387},[594,14173,9483,14174,14177],{},[651,14175,14176],{},".env"," value bundled into an Angular build is public — it ships in a JS file anyone can\nread. There is no such thing as a \"secret\" Angular environment variable. Anything the\nfrontend needs (a public API key for a client-safe service, a base URL) is fine; anything\nthat would grant access if leaked (a server API key, a signing secret, a database\ncredential) must live behind the backend, called via your own API, never embedded directly.",[1266,14179,14181],{"id":14180},"part-5-rules",[22,14182,394],{"title":625,"href":393},[658,14184,14185,14195,14201,14204,14211,14221,14224],{},[18,14186,14187,14188,3026,14190,675,14192,14194],{},"No access or refresh token in ",[651,14189,3225],{},[651,14191,13345],{},[651,14193,13362],{}," cookie or\nin-memory signal only.",[18,14196,14197,14198,14200],{},"Share one in-flight refresh Observable across concurrent ",[651,14199,13514],{},"s; never let each request\ntrigger its own refresh call.",[18,14202,14203],{},"Guards and conditional rendering are UX. Every rule they enforce must also be enforced\nserver-side.",[18,14205,14206,14207,14210],{},"CSRF defense (",[651,14208,14209],{},"withXsrfConfiguration",") only when using cookie-based auth.",[18,14212,14213,14214,3026,14216,14218,14219,1040],{},"Strict CSP, no ",[651,14215,13983],{},[651,14217,13987],{},"; nonce any inline script through ",[651,14220,13996],{},[18,14222,14223],{},"Sanitize\u002Fvalidate untrusted data once, at the service boundary — not repeatedly downstream.",[18,14225,14226],{},"Never bundle a real secret into the client — only the backend holds credentials that grant\naccess.",[617,14228],{},[620,14230,14232],{"id":14231},"part-6-production-observability",[22,14233,400],{"title":625,"href":399},[594,14235,3022,14236,14239,14240,14243],{},[651,14237,14238],{},"withLogger"," feature from Part 2 gives you a devtools timeline — invaluable locally, gone\nthe moment the tab closes. Production observability is a different concern: an error that\nhappens on a customer's machine has to reach ",[603,14241,14242],{},"you",", with enough context to reproduce it,\nwithout waiting for a bug report. Most Angular apps have devtools logging and nothing else;\nthat gap is what this part covers.",[1266,14245,14247],{"id":14246},"_61-global-error-handling-the-safety-net-under-everything",[22,14248,408],{"title":625,"href":407},[594,14250,14251,14252,14255,14256,14259],{},"Angular's default ",[651,14253,14254],{},"ErrorHandler"," logs to ",[651,14257,14258],{},"console.error"," and stops. Replace it, once, at the\nroot:",[644,14261,14263],{"className":831,"code":14262,"language":833,"meta":653,"style":653},"\u002F\u002F global-error-handler.ts\n@Injectable()\nexport class GlobalErrorHandler implements ErrorHandler {\n  private readonly reporter = inject(ErrorReportingService);\n\n  handleError(error: unknown): void {\n    \u002F\u002F Never let reporting itself throw — a reporting bug must not become a second error.\n    try {\n      this.reporter.report(error, {\n        url: location.href,\n        buildVersion: environment.buildVersion,\n      });\n    } catch {\n      console.error('Error reporting failed', error);\n    }\n    console.error(error); \u002F\u002F keep the console signal for local dev\n  }\n}\n",[651,14264,14265,14270,14279,14295,14313,14317,14338,14343,14350,14371,14388,14405,14413,14422,14443,14448,14465,14469],{"__ignoreMap":653},[837,14266,14267],{"class":839,"line":840},[837,14268,14269],{"class":843},"\u002F\u002F global-error-handler.ts\n",[837,14271,14272,14274,14276],{"class":839,"line":847},[837,14273,851],{"class":850},[837,14275,2491],{"class":854},[837,14277,14278],{"class":858},"()\n",[837,14280,14281,14283,14285,14288,14290,14293],{"class":839,"line":894},[837,14282,898],{"class":897},[837,14284,901],{"class":897},[837,14286,14287],{"class":904}," GlobalErrorHandler",[837,14289,908],{"class":897},[837,14291,14292],{"class":904}," ErrorHandler",[837,14294,914],{"class":862},[837,14296,14297,14299,14301,14304,14306,14308,14311],{"class":839,"line":917},[837,14298,2523],{"class":897},[837,14300,2526],{"class":897},[837,14302,14303],{"class":920}," reporter",[837,14305,950],{"class":924},[837,14307,2718],{"class":854},[837,14309,14310],{"class":937},"(ErrorReportingService)",[837,14312,941],{"class":862},[837,14314,14315],{"class":839,"line":944},[837,14316,988],{"emptyLinePlaceholder":987},[837,14318,14319,14322,14324,14326,14328,14330,14332,14334,14336],{"class":839,"line":959},[837,14320,14321],{"class":854},"  handleError",[837,14323,859],{"class":862},[837,14325,1312],{"class":1002},[837,14327,870],{"class":924},[837,14329,14072],{"class":967},[837,14331,1011],{"class":862},[837,14333,870],{"class":924},[837,14335,2636],{"class":967},[837,14337,914],{"class":862},[837,14339,14340],{"class":839,"line":984},[837,14341,14342],{"class":843},"    \u002F\u002F Never let reporting itself throw — a reporting bug must not become a second error.\n",[837,14344,14345,14348],{"class":839,"line":991},[837,14346,14347],{"class":897},"    try",[837,14349,914],{"class":862},[837,14351,14352,14355,14357,14360,14362,14365,14367,14369],{"class":839,"line":1017},[837,14353,14354],{"class":1036},"      this",[837,14356,1040],{"class":869},[837,14358,14359],{"class":937},"reporter",[837,14361,1040],{"class":869},[837,14363,14364],{"class":854},"report",[837,14366,6336],{"class":937},[837,14368,877],{"class":862},[837,14370,914],{"class":862},[837,14372,14373,14376,14378,14381,14383,14386],{"class":839,"line":1022},[837,14374,14375],{"class":937},"        url",[837,14377,870],{"class":869},[837,14379,14380],{"class":937}," location",[837,14382,1040],{"class":869},[837,14384,14385],{"class":937},"href",[837,14387,1387],{"class":862},[837,14389,14390,14393,14395,14398,14400,14403],{"class":839,"line":1033},[837,14391,14392],{"class":937},"        buildVersion",[837,14394,870],{"class":869},[837,14396,14397],{"class":937}," environment",[837,14399,1040],{"class":869},[837,14401,14402],{"class":937},"buildVersion",[837,14404,1387],{"class":862},[837,14406,14407,14409,14411],{"class":839,"line":1053},[837,14408,3889],{"class":862},[837,14410,1011],{"class":937},[837,14412,941],{"class":862},[837,14414,14415,14417,14420],{"class":839,"line":1098},[837,14416,1206],{"class":862},[837,14418,14419],{"class":897}," catch",[837,14421,914],{"class":862},[837,14423,14424,14427,14429,14431,14433,14436,14438,14441],{"class":839,"line":1151},[837,14425,14426],{"class":937},"      console",[837,14428,1040],{"class":869},[837,14430,1312],{"class":854},[837,14432,859],{"class":937},[837,14434,14435],{"class":873},"'Error reporting failed'",[837,14437,877],{"class":862},[837,14439,14440],{"class":937}," error)",[837,14442,941],{"class":862},[837,14444,14445],{"class":839,"line":1203},[837,14446,14447],{"class":862},"    }\n",[837,14449,14450,14453,14455,14457,14460,14462],{"class":839,"line":1213},[837,14451,14452],{"class":937},"    console",[837,14454,1040],{"class":869},[837,14456,1312],{"class":854},[837,14458,14459],{"class":937},"(error)",[837,14461,1133],{"class":862},[837,14463,14464],{"class":843}," \u002F\u002F keep the console signal for local dev\n",[837,14466,14467],{"class":839,"line":1219},[837,14468,1216],{"class":862},[837,14470,14471],{"class":839,"line":3975},[837,14472,1222],{"class":862},[644,14474,14476],{"className":831,"code":14475,"language":833,"meta":653,"style":653},"\u002F\u002F app.config.ts\nproviders: [\n  { provide: ErrorHandler, useClass: GlobalErrorHandler },\n],\n",[651,14477,14478,14482,14490,14511],{"__ignoreMap":653},[837,14479,14480],{"class":839,"line":840},[837,14481,1338],{"class":843},[837,14483,14484,14486,14488],{"class":839,"line":847},[837,14485,11851],{"class":5594},[837,14487,870],{"class":862},[837,14489,1368],{"class":937},[837,14491,14492,14494,14496,14498,14500,14502,14504,14506,14509],{"class":839,"line":894},[837,14493,1920],{"class":862},[837,14495,1546],{"class":937},[837,14497,870],{"class":869},[837,14499,14292],{"class":937},[837,14501,877],{"class":862},[837,14503,12070],{"class":937},[837,14505,870],{"class":869},[837,14507,14508],{"class":937}," GlobalErrorHandler ",[837,14510,1564],{"class":862},[837,14512,14513,14515],{"class":839,"line":917},[837,14514,2121],{"class":937},[837,14516,1387],{"class":862},[594,14518,14519,14520,14523],{},"This is the backstop, not the primary error-handling path — it catches what falls through\ntemplate bindings, unhandled promise rejections Angular routes to it, and bugs nobody\nanticipated. It should never be the ",[603,14521,14522],{},"only"," place an error is reported; see below.",[1266,14525,14527],{"id":14526},"_62-normalize-http-errors-once-at-the-interceptor",[22,14528,414],{"title":625,"href":413},[594,14530,14531,14532,14534,14535,14538],{},"Every store's ",[651,14533,6436],{}," error branch currently gets a raw ",[651,14536,14537],{},"HttpErrorResponse"," or whatever\nthe service threw. Without normalization, each store re-derives \"is this a network failure, a\n4xx, a 5xx, a validation error\" independently — and inevitably inconsistently. Do it once:",[644,14540,14542],{"className":831,"code":14541,"language":833,"meta":653,"style":653},"export const errorNormalizationInterceptor: HttpInterceptorFn = (req, next) =>\n  next(req).pipe(\n    catchError((error: unknown) => {\n      throw toDomainError(error); \u002F\u002F { kind: 'network' | 'validation' | 'server' | 'auth', message, status }\n    })\n  );\n",[651,14543,14544,14571,14584,14604,14619,14625],{"__ignoreMap":653},[837,14545,14546,14548,14550,14553,14555,14557,14559,14561,14563,14565,14567,14569],{"class":839,"line":840},[837,14547,898],{"class":897},[837,14549,1345],{"class":897},[837,14551,14552],{"class":854}," errorNormalizationInterceptor",[837,14554,870],{"class":924},[837,14556,4196],{"class":904},[837,14558,950],{"class":924},[837,14560,1106],{"class":862},[837,14562,4203],{"class":1002},[837,14564,877],{"class":862},[837,14566,4208],{"class":1002},[837,14568,1011],{"class":862},[837,14570,4932],{"class":924},[837,14572,14573,14576,14578,14580,14582],{"class":839,"line":847},[837,14574,14575],{"class":854},"  next",[837,14577,13572],{"class":937},[837,14579,1040],{"class":869},[837,14581,3898],{"class":854},[837,14583,4484],{"class":937},[837,14585,14586,14588,14590,14592,14594,14596,14598,14600,14602],{"class":839,"line":894},[837,14587,13583],{"class":854},[837,14589,859],{"class":937},[837,14591,859],{"class":862},[837,14593,1312],{"class":1002},[837,14595,870],{"class":924},[837,14597,14072],{"class":967},[837,14599,1011],{"class":862},[837,14601,1114],{"class":924},[837,14603,914],{"class":862},[837,14605,14606,14609,14612,14614,14616],{"class":839,"line":917},[837,14607,14608],{"class":897},"      throw",[837,14610,14611],{"class":854}," toDomainError",[837,14613,14459],{"class":937},[837,14615,1133],{"class":862},[837,14617,14618],{"class":843}," \u002F\u002F { kind: 'network' | 'validation' | 'server' | 'auth', message, status }\n",[837,14620,14621,14623],{"class":839,"line":944},[837,14622,1206],{"class":862},[837,14624,891],{"class":937},[837,14626,14627,14629],{"class":839,"line":959},[837,14628,4985],{"class":937},[837,14630,941],{"class":862},[594,14632,14633,14634,14637,14638,14641],{},"Stores then branch on ",[651,14635,14636],{},"kind",", not on parsing ",[651,14639,14640],{},"error.status"," themselves, and the shape you\nreport to the error-reporting service is the same shape you show the user.",[1266,14643,14645],{"id":14644},"_63-report-from-the-stores-error-branch-too-not-only-the-global-handler",[22,14646,420],{"title":625,"href":419},[594,14648,9483,14649,14651,14652,14655],{},[651,14650,6436],{}," error branch that only calls ",[651,14653,14654],{},"setError()"," updates the UI but reports nothing\n— the failure is invisible outside that one user's screen. The two concerns are separate and\nboth need to happen:",[644,14657,14659],{"className":831,"code":14658,"language":833,"meta":653,"style":653},"tapResponse({\n  next: (carriers) => patchState(store, { carriers }, setSuccess()),\n  error: (error: DomainError) => {\n    patchState(store, { carriers: [] }, setError(error.message));       \u002F\u002F UI-facing\n    reporter.report(error, { action: 'loadTopCarriers', range });       \u002F\u002F observability-facing\n  },\n}),\n",[651,14660,14661,14669,14701,14722,14754,14791,14795],{"__ignoreMap":653},[837,14662,14663,14665,14667],{"class":839,"line":840},[837,14664,6436],{"class":854},[837,14666,859],{"class":937},[837,14668,1095],{"class":862},[837,14670,14671,14673,14675,14677,14679,14681,14683,14685,14687,14689,14691,14693,14695,14697,14699],{"class":839,"line":847},[837,14672,14575],{"class":854},[837,14674,870],{"class":869},[837,14676,1106],{"class":862},[837,14678,1109],{"class":1002},[837,14680,1011],{"class":862},[837,14682,1114],{"class":924},[837,14684,6278],{"class":854},[837,14686,5518],{"class":937},[837,14688,877],{"class":862},[837,14690,1117],{"class":862},[837,14692,6287],{"class":937},[837,14694,5532],{"class":862},[837,14696,5535],{"class":854},[837,14698,1384],{"class":937},[837,14700,1387],{"class":862},[837,14702,14703,14705,14707,14709,14711,14713,14716,14718,14720],{"class":839,"line":894},[837,14704,962],{"class":854},[837,14706,870],{"class":869},[837,14708,1106],{"class":862},[837,14710,1312],{"class":1002},[837,14712,870],{"class":924},[837,14714,14715],{"class":904}," DomainError",[837,14717,1011],{"class":862},[837,14719,1114],{"class":924},[837,14721,914],{"class":862},[837,14723,14724,14727,14729,14731,14733,14735,14737,14739,14741,14743,14745,14747,14749,14751],{"class":839,"line":917},[837,14725,14726],{"class":854},"    patchState",[837,14728,5518],{"class":937},[837,14730,877],{"class":862},[837,14732,1117],{"class":862},[837,14734,1130],{"class":937},[837,14736,870],{"class":869},[837,14738,6213],{"class":937},[837,14740,5532],{"class":862},[837,14742,6047],{"class":854},[837,14744,6336],{"class":937},[837,14746,1040],{"class":869},[837,14748,6341],{"class":937},[837,14750,1133],{"class":862},[837,14752,14753],{"class":843},"       \u002F\u002F UI-facing\n",[837,14755,14756,14759,14761,14763,14765,14767,14769,14772,14774,14777,14779,14782,14784,14786,14788],{"class":839,"line":944},[837,14757,14758],{"class":937},"    reporter",[837,14760,1040],{"class":869},[837,14762,14364],{"class":854},[837,14764,6336],{"class":937},[837,14766,877],{"class":862},[837,14768,1117],{"class":862},[837,14770,14771],{"class":937}," action",[837,14773,870],{"class":869},[837,14775,14776],{"class":873}," 'loadTopCarriers'",[837,14778,877],{"class":862},[837,14780,14781],{"class":937}," range ",[837,14783,3505],{"class":862},[837,14785,1011],{"class":937},[837,14787,1133],{"class":862},[837,14789,14790],{"class":843},"       \u002F\u002F observability-facing\n",[837,14792,14793],{"class":839,"line":959},[837,14794,2018],{"class":862},[837,14796,14797,14799,14801],{"class":839,"line":984},[837,14798,3505],{"class":862},[837,14800,1011],{"class":937},[837,14802,1387],{"class":862},[594,14804,14805,14806,14808,14809,14812,14813,1040],{},"Skipping the second line is the most common gap in otherwise-correct store code: the\n",[651,14807,8525],{}," pattern makes failures ",[603,14810,14811],{},"render"," correctly, which quietly removes the pressure\nto also make them ",[603,14814,14815],{},"observable",[1266,14817,14819],{"id":14818},"_64-correlation-ids-connect-a-report-back-to-what-the-user-was-doing",[22,14820,426],{"title":625,"href":425},[594,14822,14823],{},"A stack trace with no context is close to useless. Generate a correlation ID per navigation\n(or reuse a request ID per HTTP call) and thread it through:",[644,14825,14827],{"className":831,"code":14826,"language":833,"meta":653,"style":653},"export const correlationIdInterceptor: HttpInterceptorFn = (req, next) =>\n  next(req.clone({ setHeaders: { 'X-Correlation-Id': crypto.randomUUID() } }));\n",[651,14828,14829,14856],{"__ignoreMap":653},[837,14830,14831,14833,14835,14838,14840,14842,14844,14846,14848,14850,14852,14854],{"class":839,"line":840},[837,14832,898],{"class":897},[837,14834,1345],{"class":897},[837,14836,14837],{"class":854}," correlationIdInterceptor",[837,14839,870],{"class":924},[837,14841,4196],{"class":904},[837,14843,950],{"class":924},[837,14845,1106],{"class":862},[837,14847,4203],{"class":1002},[837,14849,877],{"class":862},[837,14851,4208],{"class":1002},[837,14853,1011],{"class":862},[837,14855,4932],{"class":924},[837,14857,14858,14860,14863,14865,14867,14869,14871,14873,14875,14877,14880,14882,14885,14887,14890,14892,14894,14896,14898],{"class":839,"line":847},[837,14859,14575],{"class":854},[837,14861,14862],{"class":937},"(req",[837,14864,1040],{"class":869},[837,14866,4258],{"class":854},[837,14868,859],{"class":937},[837,14870,863],{"class":862},[837,14872,4265],{"class":937},[837,14874,870],{"class":869},[837,14876,1117],{"class":862},[837,14878,14879],{"class":873}," 'X-Correlation-Id'",[837,14881,870],{"class":869},[837,14883,14884],{"class":937}," crypto",[837,14886,1040],{"class":869},[837,14888,14889],{"class":854},"randomUUID",[837,14891,5839],{"class":937},[837,14893,3505],{"class":862},[837,14895,888],{"class":862},[837,14897,4567],{"class":937},[837,14899,941],{"class":862},[594,14901,14902],{},"Attach the current correlation\u002Fsession ID to every error report and every backend log line\nthat handles the request, so a frontend error and the backend log entry that caused it can be\njoined by ID during an incident.",[1266,14904,14906],{"id":14905},"_65-breadcrumbs-without-pii",[22,14907,432],{"title":625,"href":431},[594,14909,14910,14911,14913,14914,14916,14917,14919],{},"Leave a short trail of recent actions (route changes, key store mutations, the\n",[651,14912,6978],{}," strings already flowing through ",[651,14915,14238],{},") so an error report reads as\na story, not an isolated stack trace. Pipe the same descriptive strings you're already writing\nfor ",[651,14918,14238],{}," into the production reporter's breadcrumb API — you're not inventing a second\nlogging vocabulary, just giving the existing one a second destination.",[594,14921,14922,14923,14925],{},"Do ",[597,14924,2789],{}," put user-identifying data (email, name, full request payloads) in breadcrumbs or\nerror context beyond an opaque user\u002Fsession ID. A breadcrumb trail is exactly the kind of\nplace PII accumulates by accident.",[1266,14927,14929],{"id":14928},"_66-real-user-monitoring-core-web-vitals",[22,14930,438],{"title":625,"href":437},[594,14932,14933,14934,14936,14937,14939,14940,14943],{},"Bundle-size and ",[651,14935,682],{},"-cost discipline (",[22,14938,9466],{"href":507},") is necessary but\nnot sufficient — it tells you what ",[603,14941,14942],{},"should"," be fast, not what's actually fast for real users\non real networks. Report field metrics:",[644,14945,14947],{"className":831,"code":14946,"language":833,"meta":653,"style":653},"import { onLCP, onINP, onCLS } from 'web-vitals';\n\nonLCP((metric) => reporter.metric('LCP', metric.value, { route: currentRoute() }));\nonINP((metric) => reporter.metric('INP', metric.value, { route: currentRoute() }));\nonCLS((metric) => reporter.metric('CLS', metric.value, { route: currentRoute() }));\n",[651,14948,14949,14978,14982,15038,15090],{"__ignoreMap":653},[837,14950,14951,14954,14956,14959,14961,14964,14966,14969,14971,14973,14976],{"class":839,"line":840},[837,14952,14953],{"class":897},"import",[837,14955,1117],{"class":862},[837,14957,14958],{"class":937}," onLCP",[837,14960,877],{"class":862},[837,14962,14963],{"class":937}," onINP",[837,14965,877],{"class":862},[837,14967,14968],{"class":937}," onCLS ",[837,14970,3505],{"class":862},[837,14972,3859],{"class":897},[837,14974,14975],{"class":873}," 'web-vitals'",[837,14977,941],{"class":862},[837,14979,14980],{"class":839,"line":847},[837,14981,988],{"emptyLinePlaceholder":987},[837,14983,14984,14987,14989,14991,14994,14996,14998,15000,15002,15004,15006,15009,15011,15014,15016,15018,15020,15022,15025,15027,15030,15032,15034,15036],{"class":839,"line":894},[837,14985,14986],{"class":854},"onLCP",[837,14988,859],{"class":937},[837,14990,859],{"class":862},[837,14992,14993],{"class":1002},"metric",[837,14995,1011],{"class":862},[837,14997,1114],{"class":924},[837,14999,14303],{"class":937},[837,15001,1040],{"class":869},[837,15003,14993],{"class":854},[837,15005,859],{"class":937},[837,15007,15008],{"class":873},"'LCP'",[837,15010,877],{"class":862},[837,15012,15013],{"class":937}," metric",[837,15015,1040],{"class":869},[837,15017,9099],{"class":937},[837,15019,877],{"class":862},[837,15021,1117],{"class":862},[837,15023,15024],{"class":937}," route",[837,15026,870],{"class":869},[837,15028,15029],{"class":854}," currentRoute",[837,15031,5839],{"class":937},[837,15033,3505],{"class":862},[837,15035,4567],{"class":937},[837,15037,941],{"class":862},[837,15039,15040,15043,15045,15047,15049,15051,15053,15055,15057,15059,15061,15064,15066,15068,15070,15072,15074,15076,15078,15080,15082,15084,15086,15088],{"class":839,"line":917},[837,15041,15042],{"class":854},"onINP",[837,15044,859],{"class":937},[837,15046,859],{"class":862},[837,15048,14993],{"class":1002},[837,15050,1011],{"class":862},[837,15052,1114],{"class":924},[837,15054,14303],{"class":937},[837,15056,1040],{"class":869},[837,15058,14993],{"class":854},[837,15060,859],{"class":937},[837,15062,15063],{"class":873},"'INP'",[837,15065,877],{"class":862},[837,15067,15013],{"class":937},[837,15069,1040],{"class":869},[837,15071,9099],{"class":937},[837,15073,877],{"class":862},[837,15075,1117],{"class":862},[837,15077,15024],{"class":937},[837,15079,870],{"class":869},[837,15081,15029],{"class":854},[837,15083,5839],{"class":937},[837,15085,3505],{"class":862},[837,15087,4567],{"class":937},[837,15089,941],{"class":862},[837,15091,15092,15095,15097,15099,15101,15103,15105,15107,15109,15111,15113,15116,15118,15120,15122,15124,15126,15128,15130,15132,15134,15136,15138,15140],{"class":839,"line":944},[837,15093,15094],{"class":854},"onCLS",[837,15096,859],{"class":937},[837,15098,859],{"class":862},[837,15100,14993],{"class":1002},[837,15102,1011],{"class":862},[837,15104,1114],{"class":924},[837,15106,14303],{"class":937},[837,15108,1040],{"class":869},[837,15110,14993],{"class":854},[837,15112,859],{"class":937},[837,15114,15115],{"class":873},"'CLS'",[837,15117,877],{"class":862},[837,15119,15013],{"class":937},[837,15121,1040],{"class":869},[837,15123,9099],{"class":937},[837,15125,877],{"class":862},[837,15127,1117],{"class":862},[837,15129,15024],{"class":937},[837,15131,870],{"class":869},[837,15133,15029],{"class":854},[837,15135,5839],{"class":937},[837,15137,3505],{"class":862},[837,15139,4567],{"class":937},[837,15141,941],{"class":862},[594,15143,15144,15145,15148],{},"Tag every metric with the route (and ideally build version), so a regression shows up as \"LCP\ngot worse on ",[651,15146,15147],{},"\u002Fcarriers"," after build 482,\" not as an unattributed aggregate trend.",[1266,15150,15152],{"id":15151},"_67-source-maps-ship-them-to-the-reporter-not-to-the-public",[22,15153,444],{"title":625,"href":443},[594,15155,15156,15157,15160],{},"Production builds should emit source maps (",[651,15158,15159],{},"\"sourceMap\": true"," in the production build\nconfig, or a hidden-source-map variant) and upload them directly to the error-reporting\nbackend during CI — not serve them alongside the public bundle. That gets you readable stack\ntraces in reports without handing every visitor an unminified copy of your source.",[1266,15162,15164],{"id":15163},"part-6-rules",[22,15165,450],{"title":625,"href":449},[658,15167,15168,15174,15177,15183,15186,15189,15192],{},[18,15169,15170,15171,15173],{},"Install one global ",[651,15172,14254],{},"; make its own reporting call fail closed (never throw).",[18,15175,15176],{},"Normalize HTTP errors once, at an interceptor — stores branch on the normalized shape.",[18,15178,15179,15180,15182],{},"Every store's error branch reports to observability ",[603,15181,4135],{}," sets UI-facing call state; one\nwithout the other is a gap, not a simplification.",[18,15184,15185],{},"Thread a correlation ID through requests and attach it to every error report.",[18,15187,15188],{},"Breadcrumb recent actions; never put PII in them.",[18,15190,15191],{},"Report Core Web Vitals per route, not just lab metrics from a local Lighthouse run.",[18,15193,15194],{},"Upload source maps to the reporting backend in CI; don't ship them publicly.",[617,15196],{},[620,15198,15200],{"id":15199},"part-7-forms",[22,15201,456],{"title":625,"href":455},[594,15203,15204],{},"Angular has three form systems now. Pick by version and by what already exists in the codebase\n— don't mix approaches within one feature.",[1645,15206,15207,15216],{},[1648,15208,15209],{},[1651,15210,15211,15214],{},[1654,15212,15213],{},"Situation",[1654,15215,8757],{},[1664,15217,15218,15232,15242,15256,15264],{},[1651,15219,15220,15223],{},[1669,15221,15222],{},"New form, Angular 22+",[1669,15224,15225,1106,15228,15231],{},[597,15226,15227],{},"Signal Forms",[651,15229,15230],{},"@angular\u002Fforms\u002Fsignals",") — stable",[1651,15233,15234,15237],{},[1669,15235,15236],{},"New form, Angular 21",[1669,15238,15239,15241],{},[597,15240,15227],{},", but they were still experimental; Reactive Forms if you need stability guarantees",[1651,15243,15244,15247],{},[1669,15245,15246],{},"New form, Angular 20",[1669,15248,15249,1106,15252,15255],{},[597,15250,15251],{},"Reactive Forms",[651,15253,15254],{},"FormGroup"," \u002F typed) — Signal Forms don't exist yet",[1651,15257,15258,15261],{},[1669,15259,15260],{},"Existing form",[1669,15262,15263],{},"Match whatever that feature already uses",[1651,15265,15266,15269],{},[1669,15267,15268],{},"Trivial single input, existing template-driven codebase",[1669,15270,15271],{},"Template-driven is acceptable",[594,15273,15274,15277],{},[597,15275,15276],{},"Never mix reactive and template-driven bindings on the same control."," It's a classic agent\nerror and produces state that updates through one path and not the other.",[1266,15279,15281],{"id":15280},"_71-signal-forms-v21-stable-since-v22",[22,15282,464],{"title":625,"href":463},[594,15284,15285],{},"Signal Forms derive the entire form structure from a signal model, which makes them a natural\nfit for everything else in this guide:",[644,15287,15289],{"className":831,"code":15288,"language":833,"meta":653,"style":653},"export class CarrierForm {\n  \u002F\u002F CRITICAL: never null or undefined as initial values.\n  \u002F\u002F '' for strings, 0 for numbers, [] for arrays.\n  protected readonly model = signal({\n    name: '',\n    contactEmail: '',\n    maxWeightKg: 0,\n    regions: [] as string[],\n  });\n\n  protected readonly form = form(this.model, (path) => {\n    required(path.name, { message: 'Name is required' });\n    maxLength(path.name, 80);\n    email(path.contactEmail);\n    min(path.maxWeightKg, 1);\n  });\n}\n",[651,15290,15291,15302,15307,15312,15329,15340,15351,15362,15379,15387,15391,15426,15455,15475,15489,15509,15517],{"__ignoreMap":653},[837,15292,15293,15295,15297,15300],{"class":839,"line":840},[837,15294,898],{"class":897},[837,15296,901],{"class":897},[837,15298,15299],{"class":904}," CarrierForm",[837,15301,914],{"class":862},[837,15303,15304],{"class":839,"line":847},[837,15305,15306],{"class":843},"  \u002F\u002F CRITICAL: never null or undefined as initial values.\n",[837,15308,15309],{"class":839,"line":894},[837,15310,15311],{"class":843},"  \u002F\u002F '' for strings, 0 for numbers, [] for arrays.\n",[837,15313,15314,15316,15318,15321,15323,15325,15327],{"class":839,"line":917},[837,15315,7677],{"class":897},[837,15317,2526],{"class":897},[837,15319,15320],{"class":920}," model",[837,15322,950],{"class":924},[837,15324,2534],{"class":854},[837,15326,859],{"class":937},[837,15328,1095],{"class":862},[837,15330,15331,15334,15336,15338],{"class":839,"line":944},[837,15332,15333],{"class":937},"    name",[837,15335,870],{"class":869},[837,15337,1928],{"class":873},[837,15339,1387],{"class":862},[837,15341,15342,15345,15347,15349],{"class":839,"line":959},[837,15343,15344],{"class":937},"    contactEmail",[837,15346,870],{"class":869},[837,15348,1928],{"class":873},[837,15350,1387],{"class":862},[837,15352,15353,15356,15358,15360],{"class":839,"line":984},[837,15354,15355],{"class":937},"    maxWeightKg",[837,15357,870],{"class":869},[837,15359,4913],{"class":953},[837,15361,1387],{"class":862},[837,15363,15364,15367,15369,15371,15373,15375,15377],{"class":839,"line":991},[837,15365,15366],{"class":937},"    regions",[837,15368,870],{"class":869},[837,15370,6213],{"class":937},[837,15372,8178],{"class":897},[837,15374,968],{"class":967},[837,15376,1075],{"class":930},[837,15378,1387],{"class":862},[837,15380,15381,15383,15385],{"class":839,"line":1017},[837,15382,5172],{"class":862},[837,15384,1011],{"class":937},[837,15386,941],{"class":862},[837,15388,15389],{"class":839,"line":1022},[837,15390,988],{"emptyLinePlaceholder":987},[837,15392,15393,15395,15397,15400,15402,15404,15406,15408,15410,15413,15415,15417,15420,15422,15424],{"class":839,"line":1033},[837,15394,7677],{"class":897},[837,15396,2526],{"class":897},[837,15398,15399],{"class":920}," form",[837,15401,950],{"class":924},[837,15403,15399],{"class":854},[837,15405,859],{"class":937},[837,15407,2317],{"class":1036},[837,15409,1040],{"class":869},[837,15411,15412],{"class":937},"model",[837,15414,877],{"class":862},[837,15416,1106],{"class":862},[837,15418,15419],{"class":1002},"path",[837,15421,1011],{"class":862},[837,15423,1114],{"class":924},[837,15425,914],{"class":862},[837,15427,15428,15431,15434,15436,15438,15440,15442,15444,15446,15449,15451,15453],{"class":839,"line":1053},[837,15429,15430],{"class":854},"    required",[837,15432,15433],{"class":937},"(path",[837,15435,1040],{"class":869},[837,15437,4006],{"class":937},[837,15439,877],{"class":862},[837,15441,1117],{"class":862},[837,15443,2914],{"class":937},[837,15445,870],{"class":869},[837,15447,15448],{"class":873}," 'Name is required'",[837,15450,888],{"class":862},[837,15452,1011],{"class":937},[837,15454,941],{"class":862},[837,15456,15457,15460,15462,15464,15466,15468,15471,15473],{"class":839,"line":1098},[837,15458,15459],{"class":854},"    maxLength",[837,15461,15433],{"class":937},[837,15463,1040],{"class":869},[837,15465,4006],{"class":937},[837,15467,877],{"class":862},[837,15469,15470],{"class":953}," 80",[837,15472,1011],{"class":937},[837,15474,941],{"class":862},[837,15476,15477,15480,15482,15484,15487],{"class":839,"line":1151},[837,15478,15479],{"class":854},"    email",[837,15481,15433],{"class":937},[837,15483,1040],{"class":869},[837,15485,15486],{"class":937},"contactEmail)",[837,15488,941],{"class":862},[837,15490,15491,15494,15496,15498,15501,15503,15505,15507],{"class":839,"line":1203},[837,15492,15493],{"class":854},"    min",[837,15495,15433],{"class":937},[837,15497,1040],{"class":869},[837,15499,15500],{"class":937},"maxWeightKg",[837,15502,877],{"class":862},[837,15504,5576],{"class":953},[837,15506,1011],{"class":937},[837,15508,941],{"class":862},[837,15510,15511,15513,15515],{"class":839,"line":1213},[837,15512,5172],{"class":862},[837,15514,1011],{"class":937},[837,15516,941],{"class":862},[837,15518,15519],{"class":839,"line":1219},[837,15520,1222],{"class":862},[594,15522,15523,15524,679,15526,679,15529,679,15532,679,15534,679,15537,679,15540,15543,15544,679,15547,679,15550,679,15552,15555,15556,679,15559,1387,15562,15565,15566,679,15569,15572,15573,15575,15576,1040],{},"Validators (",[651,15525,2376],{},[651,15527,15528],{},"email",[651,15530,15531],{},"min",[651,15533,7809],{},[651,15535,15536],{},"minLength",[651,15538,15539],{},"maxLength",[651,15541,15542],{},"pattern","), field-state\nrules (",[651,15545,15546],{},"disabled",[651,15548,15549],{},"hidden",[651,15551,8166],{},[651,15553,15554],{},"debounce","), schema helpers (",[651,15557,15558],{},"applyWhen",[651,15560,15561],{},"applyEach",[651,15563,15564],{},"schema","), and custom validation (",[651,15567,15568],{},"validate",[651,15570,15571],{},"validateHttp",") all come from\n",[651,15574,15230],{},". Submission goes through ",[651,15577,15578],{},"submit()",[594,15580,15581,15582,15590],{},"The one hard rule worth repeating because it bites immediately: ",[597,15583,4157,15584,12580,15586,15589],{},[651,15585,10471],{},[651,15587,15588],{},"undefined"," in\nthe model."," Use the type's empty value.",[594,15592,15593,15594,2973,15597,15600,15601,15604],{},"New in v22: ",[651,15595,15596],{},"minDate()",[651,15598,15599],{},"maxDate()"," validators, blur-based debouncing, and ",[651,15602,15603],{},"getError()"," for\nreading a specific validation error.",[594,15606,15607,15610],{},[597,15608,15609],{},"If you're moving off v21's experimental API",", three things changed at stabilisation:",[658,15612,15613,15623,15633],{},[18,15614,15615,15618,15619,15622],{},[651,15616,15617],{},"touched"," became an input paired with a ",[651,15620,15621],{},"touch()"," output (it was a model).",[18,15624,15625,15628,15629,15632],{},[651,15626,15627],{},"markAsTouched()"," now marks descendants by default — pass ",[651,15630,15631],{},"{ skipDescendants: true }"," to opt out.",[18,15634,15635,15636,15639],{},"Conditional validators take a ",[651,15637,15638],{},"when"," option instead of a reactive function passed directly.",[1266,15641,15643],{"id":15642},"_72-reactive-forms-v20-or-existing-code",[22,15644,470],{"title":625,"href":469},[594,15646,15647],{},"Use the typed APIs and validate declaratively. Two zoneless-era cautions:",[658,15649,15650,15667],{},[18,15651,15652,15653,15660,15661,15663,15664,3229],{},"Under zoneless, ",[597,15654,15655,3026,15657,15659],{},[651,15656,3413],{},[651,15658,3416],{}," do not schedule change detection."," If a template\ndepends on form state, bridge it with ",[651,15662,3423],{}," or reflect it through a signal\n(",[22,15665,15666],{"href":102},"§1.9",[18,15668,15669,15670,15673,15674,15676,15677,15679],{},"Don't subscribe to ",[651,15671,15672],{},"valueChanges"," in a component to keep a field in sync — that's a\nsubscription plus derived state. Convert to a signal with ",[651,15675,11219],{}," and derive with\n",[651,15678,682],{},", or let the form model be the source of truth.",[1266,15681,15683],{"id":15682},"_73-where-form-submission-lives",[22,15684,476],{"title":625,"href":475},[594,15686,15687,15688,15691,15692,15694],{},"Validation is the form's job. ",[597,15689,15690],{},"Submission is the store's job"," — it's a network call with\nloading state, error handling, and a success path, which is exactly what an ",[651,15693,702],{}," is for:",[644,15696,15698],{"className":831,"code":15697,"language":833,"meta":653,"style":653},"\u002F\u002F In the store — exhaustMap so a double-click can't submit twice.\nsaveCarrier: rxMethod\u003CCarrierDraft>(\n  pipe(\n    exhaustMap((draft) => {\n      actionPatchState('saveCarrier(): saving', store, setPending());\n      return api.saveCarrier(draft).pipe(\n        tapResponse({\n          next: (saved) => actionPatchState('saveCarrier(): saved', store, { carrier: saved }, setSuccess()),\n          error: (error: Error) => actionPatchState('saveCarrier(): failed', store, setError(error.message)),\n        })\n      );\n    })\n  )\n),\n",[651,15699,15700,15705,15723,15729,15747,15768,15787,15796,15841,15883,15890,15896,15902,15907],{"__ignoreMap":653},[837,15701,15702],{"class":839,"line":840},[837,15703,15704],{"class":843},"\u002F\u002F In the store — exhaustMap so a double-click can't submit twice.\n",[837,15706,15707,15710,15712,15714,15716,15719,15721],{"class":839,"line":847},[837,15708,15709],{"class":5594},"saveCarrier",[837,15711,870],{"class":862},[837,15713,5126],{"class":854},[837,15715,1069],{"class":1068},[837,15717,15718],{"class":904},"CarrierDraft",[837,15720,1078],{"class":1068},[837,15722,4484],{"class":937},[837,15724,15725,15727],{"class":839,"line":894},[837,15726,5612],{"class":854},[837,15728,4484],{"class":937},[837,15730,15731,15734,15736,15738,15741,15743,15745],{"class":839,"line":917},[837,15732,15733],{"class":854},"    exhaustMap",[837,15735,859],{"class":937},[837,15737,859],{"class":862},[837,15739,15740],{"class":1002},"draft",[837,15742,1011],{"class":862},[837,15744,1114],{"class":924},[837,15746,914],{"class":862},[837,15748,15749,15751,15753,15756,15758,15760,15762,15764,15766],{"class":839,"line":944},[837,15750,7492],{"class":854},[837,15752,859],{"class":937},[837,15754,15755],{"class":873},"'saveCarrier(): saving'",[837,15757,877],{"class":862},[837,15759,4896],{"class":937},[837,15761,877],{"class":862},[837,15763,5977],{"class":854},[837,15765,1384],{"class":937},[837,15767,941],{"class":862},[837,15769,15770,15772,15774,15776,15778,15781,15783,15785],{"class":839,"line":959},[837,15771,13652],{"class":897},[837,15773,2713],{"class":937},[837,15775,1040],{"class":869},[837,15777,15709],{"class":854},[837,15779,15780],{"class":937},"(draft)",[837,15782,1040],{"class":869},[837,15784,3898],{"class":854},[837,15786,4484],{"class":937},[837,15788,15789,15792,15794],{"class":839,"line":984},[837,15790,15791],{"class":854},"        tapResponse",[837,15793,859],{"class":937},[837,15795,1095],{"class":862},[837,15797,15798,15801,15803,15805,15808,15810,15812,15814,15816,15819,15821,15823,15825,15827,15829,15831,15833,15835,15837,15839],{"class":839,"line":991},[837,15799,15800],{"class":854},"          next",[837,15802,870],{"class":869},[837,15804,1106],{"class":862},[837,15806,15807],{"class":1002},"saved",[837,15809,1011],{"class":862},[837,15811,1114],{"class":924},[837,15813,6862],{"class":854},[837,15815,859],{"class":937},[837,15817,15818],{"class":873},"'saveCarrier(): saved'",[837,15820,877],{"class":862},[837,15822,4896],{"class":937},[837,15824,877],{"class":862},[837,15826,1117],{"class":862},[837,15828,4962],{"class":937},[837,15830,870],{"class":869},[837,15832,5529],{"class":937},[837,15834,5532],{"class":862},[837,15836,5535],{"class":854},[837,15838,1384],{"class":937},[837,15840,1387],{"class":862},[837,15842,15843,15846,15848,15850,15852,15854,15856,15858,15860,15862,15864,15867,15869,15871,15873,15875,15877,15879,15881],{"class":839,"line":1017},[837,15844,15845],{"class":854},"          error",[837,15847,870],{"class":869},[837,15849,1106],{"class":862},[837,15851,1312],{"class":1002},[837,15853,870],{"class":924},[837,15855,6311],{"class":904},[837,15857,1011],{"class":862},[837,15859,1114],{"class":924},[837,15861,6862],{"class":854},[837,15863,859],{"class":937},[837,15865,15866],{"class":873},"'saveCarrier(): failed'",[837,15868,877],{"class":862},[837,15870,4896],{"class":937},[837,15872,877],{"class":862},[837,15874,6047],{"class":854},[837,15876,6336],{"class":937},[837,15878,1040],{"class":869},[837,15880,6341],{"class":937},[837,15882,1387],{"class":862},[837,15884,15885,15888],{"class":839,"line":1022},[837,15886,15887],{"class":862},"        }",[837,15889,891],{"class":937},[837,15891,15892,15894],{"class":839,"line":1033},[837,15893,13693],{"class":937},[837,15895,941],{"class":862},[837,15897,15898,15900],{"class":839,"line":1053},[837,15899,1206],{"class":862},[837,15901,891],{"class":937},[837,15903,15904],{"class":839,"line":1098},[837,15905,15906],{"class":937},"  )\n",[837,15908,15909,15911],{"class":839,"line":1151},[837,15910,1011],{"class":937},[837,15912,1387],{"class":862},[594,15914,15915,15916,1040],{},"The component's submit handler is one line: validate, then ",[651,15917,15918],{},"store.saveCarrier(value)",[617,15920],{},[620,15922,15924],{"id":15923},"part-8-templates-styling-accessibility",[22,15925,482],{"title":625,"href":481},[1266,15927,15929],{"id":15928},"_81-templates",[22,15930,490],{"title":625,"href":489},[594,15932,15933,15934,679,15936,679,15939,15942],{},"Native control flow only — ",[651,15935,13755],{},[651,15937,15938],{},"@for",[651,15940,15941],{},"@switch",". The legacy structural directives are\nslower, need imports, and are on their way out.",[644,15944,15946],{"className":2828,"code":15945,"language":2830,"meta":653,"style":653},"@if (store.pending()) {\n  \u003Capp-spinner \u002F>\n} @else if (store.error(); as error) {\n  \u003Capp-error-banner [message]=\"error\" \u002F>\n} @else {\n  \u003Cul>\n    @for (carrier of carriers(); track carrier.id) {\n      \u003Cli>{{ carrier.name }}\u003C\u002Fli>\n    } @empty {\n      \u003Cli>No carriers in this period.\u003C\u002Fli>\n    }\n  \u003C\u002Ful>\n}\n",[651,15947,15948,15953,15961,15966,15981,15986,15994,15999,16017,16022,16039,16043,16052],{"__ignoreMap":653},[837,15949,15950],{"class":839,"line":840},[837,15951,15952],{"class":937},"@if (store.pending()) {\n",[837,15954,15955,15957,15959],{"class":839,"line":847},[837,15956,2842],{"class":869},[837,15958,2897],{"class":2845},[837,15960,2858],{"class":869},[837,15962,15963],{"class":839,"line":894},[837,15964,15965],{"class":937},"} @else if (store.error(); as error) {\n",[837,15967,15968,15970,15972,15974,15976,15979],{"class":839,"line":917},[837,15969,2842],{"class":869},[837,15971,2911],{"class":2845},[837,15973,8464],{"class":2849},[837,15975,934],{"class":869},[837,15977,15978],{"class":873},"\"error\"",[837,15980,2858],{"class":869},[837,15982,15983],{"class":839,"line":944},[837,15984,15985],{"class":937},"} @else {\n",[837,15987,15988,15990,15992],{"class":839,"line":959},[837,15989,2842],{"class":869},[837,15991,658],{"class":2845},[837,15993,2885],{"class":869},[837,15995,15996],{"class":839,"line":984},[837,15997,15998],{"class":937},"    @for (carrier of carriers(); track carrier.id) {\n",[837,16000,16001,16004,16006,16008,16011,16013,16015],{"class":839,"line":991},[837,16002,16003],{"class":869},"      \u003C",[837,16005,18],{"class":2845},[837,16007,1078],{"class":869},[837,16009,16010],{"class":937},"{{ carrier.name }}",[837,16012,13882],{"class":869},[837,16014,18],{"class":2845},[837,16016,2885],{"class":869},[837,16018,16019],{"class":839,"line":1017},[837,16020,16021],{"class":937},"    } @empty {\n",[837,16023,16024,16026,16028,16030,16033,16035,16037],{"class":839,"line":1022},[837,16025,16003],{"class":869},[837,16027,18],{"class":2845},[837,16029,1078],{"class":869},[837,16031,16032],{"class":937},"No carriers in this period.",[837,16034,13882],{"class":869},[837,16036,18],{"class":2845},[837,16038,2885],{"class":869},[837,16040,16041],{"class":839,"line":1033},[837,16042,14447],{"class":937},[837,16044,16045,16048,16050],{"class":839,"line":1053},[837,16046,16047],{"class":869},"  \u003C\u002F",[837,16049,658],{"class":2845},[837,16051,2885],{"class":869},[837,16053,16054],{"class":839,"line":1098},[837,16055,1222],{"class":937},[658,16057,16058,16072,16081,16093,16106,16118,16132,16135,16145],{},[18,16059,16060,16067,16068,16071],{},[597,16061,16062,16064,16065],{},[651,16063,9539],{}," is required on ",[651,16066,15938],{}," and must be a stable identity, not ",[651,16069,16070],{},"$index",". Getting this\nwrong destroys DOM reuse and, in lists with inputs or form controls, silently rebinds state\nto the wrong rows.",[18,16073,16074,3104,16077,16080],{},[651,16075,16076],{},"@if (expr; as alias)",[651,16078,16079],{},"@else if (expr; as alias)"," save recomputing a value you're\nalready testing — and give you correct type narrowing.",[18,16082,16083,1607,16086,16088,16089,16092],{},[651,16084,16085],{},"@empty",[651,16087,15938],{}," replaces the ",[651,16090,16091],{},"@if (list.length === 0)"," companion block.",[18,16094,16095,16097,16098,16101,16102,16105],{},[651,16096,15941],{}," uses strict equality with no fallthrough. ",[651,16099,16100],{},"@default never;"," makes the switch\n",[597,16103,16104],{},"exhaustive at compile time"," over a union — genuinely valuable, and almost never used.",[18,16107,16108,16111,16112,16115,16116,1040],{},[597,16109,16110],{},"Keep templates dumb."," No arithmetic chains, no ",[651,16113,16114],{},".filter().map().sort()",", no function calls\nthat allocate. Move it to a ",[651,16117,682],{},[18,16119,16120,2304,16123,3104,16126,16128,16129,3229],{},[597,16121,16122],{},"Don't assume globals.",[651,16124,16125],{},"new Date()",[651,16127,3219],{}," are not safe in a template (or under SSR\n— see ",[22,16130,16131],{"href":96},"§1.8",[18,16133,16134],{},"Import every pipe you use; the compiler won't find it for you.",[18,16136,3593,16137,16140,16141,16144],{},[651,16138,16139],{},"NgOptimizedImage"," for static images — it enforces width\u002Fheight, sets priority hints and\n",[651,16142,16143],{},"srcset",". It does not work for inline base64.",[18,16146,16147,16148,16151,16152,16155],{},"Prefer semantic HTML. A ",[651,16149,16150],{},"\u003Cbutton>"," gives you focus, keyboard activation, and the right role\nfor free; a ",[651,16153,16154],{},"\u003Cdiv (click)>"," gives you a bug report from a screen-reader user.",[1266,16157,16159],{"id":16158},"_82-styling",[22,16160,496],{"title":625,"href":495},[658,16162,16163,16172,16178,16191,16199,16205,16213],{},[18,16164,16165,16168,16169,1040],{},[597,16166,16167],{},"Every component's styles are its own."," Angular's default emulated encapsulation is the\nright setting — don't reach for ",[651,16170,16171],{},"ViewEncapsulation.None",[18,16173,16174,16177],{},[597,16175,16176],{},"A component should render correctly anywhere."," If it depends on an ancestor's styles to\nlook right, it isn't finished. Own your spacing and layout locally rather than assuming a\nparent provides it.",[18,16179,16180,16186,16187,16190],{},[597,16181,16182,16185],{},[651,16183,16184],{},"::ng-deep"," is a smell."," It's deprecated, it leaks, and its presence almost always means\na design-system component or token should have been used instead. If you must, scope it\nunder ",[651,16188,16189],{},":host"," and comment why.",[18,16192,16193,16198],{},[597,16194,16195,16196,1040],{},"Wrap component styles in ",[651,16197,16189],{}," It bounds the blast radius and makes it obvious the\nfile styles one component.",[18,16200,16201,16204],{},[597,16202,16203],{},"Tokens, not literals."," Colours, spacing, and typography come from design-system variables\n— and every new token needs both light and dark values. Hardcoded hex is how a theme rots.",[18,16206,16207,16208,4102,16210,16212],{},"Use logical properties (",[651,16209,3159],{},[651,16211,3167],{},") so RTL works for free —\nthis is what makes i18n's RTL support (§1.7) free rather than a parallel stylesheet.",[18,16214,16215,16216,16218],{},"Pick a class-naming convention (BEM is a fine default) and apply it consistently. Style with\nclasses, not ",[651,16217,3992],{}," selectors.",[1266,16220,16222],{"id":16221},"_83-accessibility",[22,16223,502],{"title":625,"href":501},[594,16225,16226],{},"Not optional, and cheap when done as you go. The official bar:",[3289,16228,16229],{},[594,16230,16231,16234],{},[597,16232,16233],{},"Must pass all AXE checks. Must meet WCAG AA minimums"," — focus management, colour\ncontrast, and ARIA attributes.",[594,16236,16237],{},"Practical checklist:",[658,16239,16240,16247,16250,16253,16259,16262,16265,16275],{},[18,16241,16242,16243,16246],{},"Semantic elements first; ARIA only to fill genuine gaps. A correct native element beats\n",[651,16244,16245],{},"role=\"button\""," every time.",[18,16248,16249],{},"Every interactive control is keyboard-reachable and has a visible focus indicator.",[18,16251,16252],{},"Manage focus on navigation, dialog open\u002Fclose, and dynamic content insertion.",[18,16254,16255,16256,1040],{},"Every input has a programmatically associated label; icon-only buttons get an ",[651,16257,16258],{},"aria-label",[18,16260,16261],{},"Announce async state changes — a live region for \"loading\", \"saved\", \"3 results\" — so the\nexperience isn't silent for screen-reader users.",[18,16263,16264],{},"Never encode meaning in colour alone; check contrast in both themes.",[18,16266,16267,16268,1241,16271,16274],{},"Images: meaningful ",[651,16269,16270],{},"alt",[651,16272,16273],{},"alt=\"\""," when decorative.",[18,16276,16277],{},"Building a complex widget (combobox, tree, tabs, grid)? Use a headless accessible primitive\nor a design-system component. Hand-rolled ARIA for these patterns is nearly always wrong.",[617,16279],{},[620,16281,16283],{"id":16282},"part-9-performance",[22,16284,508],{"title":625,"href":507},[594,16286,16287],{},"Ordered by how often it actually matters.",[594,16289,16290,16293,16294,16296],{},[597,16291,16292],{},"Bundle size first."," Lazy-load every feature route; ",[651,16295,2815],{}," heavy in-page content; check\nwhat a new dependency costs before adding it. This dominates real-world load performance and\nis the cheapest thing to get right early.",[594,16298,16299,16302,16303,16306],{},[597,16300,16301],{},"Paginate every list."," No unbounded fetches that grow with customer data. A query that's\nfast on your seed data and fatal on a large account is the most common production surprise in\nthis category. ",[597,16304,16305],{},"Blocking",", not a suggestion.",[594,16308,16309,16315,16316,16319],{},[597,16310,16311,16312,16314],{},"Keep ",[651,16313,682],{}," and template expressions cheap."," They re-evaluate on change detection.\nPure derivations are fine; anything iterating a large collection, formatting hundreds of rows,\nor allocating per call needs precomputing. This matters most in hot paths — grid cell\nrenderers, ",[651,16317,16318],{},"valueFormatter"," callbacks, table bindings.",[594,16321,16322,16325,16326,16328],{},[597,16323,16324],{},"Don't allocate in bindings."," A fresh object or array literal passed to an ",[651,16327,1594],{}," is a new\nreference every pass, which defeats every memoization downstream:",[644,16330,16332],{"className":2828,"code":16331,"language":2830,"meta":653,"style":653},"\u003C!-- ❌ new object every change detection cycle -->\n\u003Capp-chart [config]=\"{ height: 400, animated: true }\" \u002F>\n\n\u003C!-- ✅ stable reference from a computed or a readonly field -->\n\u003Capp-chart [config]=\"chartConfig()\" \u002F>\n",[651,16333,16334,16339,16356,16360,16365],{"__ignoreMap":653},[837,16335,16336],{"class":839,"line":840},[837,16337,16338],{"class":843},"\u003C!-- ❌ new object every change detection cycle -->\n",[837,16340,16341,16343,16346,16349,16351,16354],{"class":839,"line":847},[837,16342,1069],{"class":869},[837,16344,16345],{"class":2845},"app-chart",[837,16347,16348],{"class":2849}," [config]",[837,16350,934],{"class":869},[837,16352,16353],{"class":873},"\"{ height: 400, animated: true }\"",[837,16355,2858],{"class":869},[837,16357,16358],{"class":839,"line":894},[837,16359,988],{"emptyLinePlaceholder":987},[837,16361,16362],{"class":839,"line":917},[837,16363,16364],{"class":843},"\u003C!-- ✅ stable reference from a computed or a readonly field -->\n",[837,16366,16367,16369,16371,16373,16375,16378],{"class":839,"line":944},[837,16368,1069],{"class":869},[837,16370,16345],{"class":2845},[837,16372,16348],{"class":2849},[837,16374,934],{"class":869},[837,16376,16377],{"class":873},"\"chartConfig()\"",[837,16379,2858],{"class":869},[594,16381,16382,16389,16390,16392],{},[597,16383,16384,16386,16387,1040],{},[651,16385,9539],{}," correctly on ",[651,16388,15938],{}," A wrong ",[651,16391,9539],{}," turns a cheap list update into a full DOM\nteardown and rebuild.",[594,16394,16395,16398],{},[597,16396,16397],{},"No N+1 fetches."," Fetch a list with the fields you need, not a list followed by a\nper-row detail request. Requesting extra fields on one call beats fifty calls.",[594,16400,16401,16404,16405,16408],{},[597,16402,16403],{},"Keep the main thread free on interaction."," Heavy synchronous work in a click handler,\nconstructor, or ",[651,16406,16407],{},"ngOnInit"," is visible jank. Defer it, chunk it, or move it to a worker.",[594,16410,16411,2304,16414,16416],{},[597,16412,16413],{},"Then measure.",[651,16415,3361],{}," plus signals plus zoneless already eliminates most historic Angular\nchange-detection cost. Beyond the items above, profile before optimising — a Lighthouse run\nand a DevTools performance trace will tell you more than intuition.",[617,16418],{},[620,16420,16422],{"id":16421},"part-10-testing",[22,16423,514],{"title":625,"href":513},[594,16425,16426],{},"The layering changes what tests cost, and where they should live.",[1645,16428,16429,16441],{},[1648,16430,16431],{},[1651,16432,16433,16435,16438],{},[1654,16434,1656],{},[1654,16436,16437],{},"Test style",[1654,16439,16440],{},"Coverage weight",[1664,16442,16443,16459,16470,16485,16496,16506],{},[1651,16444,16445,16451,16456],{},[1669,16446,16447,16450],{},[651,16448,16449],{},"util\u002F"," pure functions",[1669,16452,16453,16454],{},"Plain unit tests, no ",[651,16455,2799],{},[1669,16457,16458],{},"Cheapest — cover thoroughly",[1651,16460,16461,16464,16467],{},[1669,16462,16463],{},"Services",[1669,16465,16466],{},"Assert request shape and response mapping",[1669,16468,16469],{},"Light",[1651,16471,16472,16477,16480],{},[1669,16473,16474],{},[597,16475,16476],{},"Stores",[1669,16478,16479],{},"Stub the service via DI, call methods, assert signals",[1669,16481,16482],{},[597,16483,16484],{},"Heaviest — behaviour lives here",[1651,16486,16487,16490,16493],{},[1669,16488,16489],{},"Presentational components",[1669,16491,16492],{},"Rendered output + interaction (Storybook \u002F harnesses)",[1669,16494,16495],{},"Moderate",[1651,16497,16498,16501,16504],{},[1669,16499,16500],{},"Container components",[1669,16502,16503],{},"E2E or interaction tests, not unit tests",[1669,16505,16469],{},[1651,16507,16508,16511,16514],{},[1669,16509,16510],{},"Critical flows",[1669,16512,16513],{},"E2E",[1669,16515,16516],{},"A few, high-value",[594,16518,16519,16520],{},"A useful heuristic: ",[597,16521,16522],{},"if a component is hard to test, the test is telling you the component is\ndoing a job that belongs to a store.",[1266,16524,16526],{"id":16525},"_101-store-tests-are-the-main-event",[22,16527,522],{"title":625,"href":521},[644,16529,16531],{"className":831,"code":16530,"language":833,"meta":653,"style":653},"describe('CarriersStore', () => {\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        CarriersStore,\n        { provide: CarriersApiService, useValue: { loadTopCarriers: () => of(FIXTURE) } },\n      ],\n    });\n  });\n\n  it('populates carriers and marks the call successful', async () => {\n    const store = TestBed.inject(CarriersStore);\n    store.loadTopCarriers(RANGE);\n    await Promise.resolve();\n\n    expect(store.carriers()).toHaveLength(5);\n    expect(store.pending()).toBe(false);\n    expect(store.error()).toBeNull();\n  });\n});\n",[651,16532,16533,16550,16563,16576,16585,16592,16631,16638,16646,16654,16658,16679,16697,16714,16732,16736,16763,16788,16809,16817],{"__ignoreMap":653},[837,16534,16535,16538,16540,16542,16544,16546,16548],{"class":839,"line":840},[837,16536,16537],{"class":854},"describe",[837,16539,859],{"class":937},[837,16541,4635],{"class":873},[837,16543,877],{"class":862},[837,16545,1976],{"class":862},[837,16547,1114],{"class":924},[837,16549,914],{"class":862},[837,16551,16552,16555,16557,16559,16561],{"class":839,"line":847},[837,16553,16554],{"class":854},"  beforeEach",[837,16556,859],{"class":937},[837,16558,1028],{"class":862},[837,16560,1114],{"class":924},[837,16562,914],{"class":862},[837,16564,16565,16568,16570,16572,16574],{"class":839,"line":894},[837,16566,16567],{"class":937},"    TestBed",[837,16569,1040],{"class":869},[837,16571,12605],{"class":854},[837,16573,859],{"class":937},[837,16575,1095],{"class":862},[837,16577,16578,16581,16583],{"class":839,"line":917},[837,16579,16580],{"class":937},"      providers",[837,16582,870],{"class":869},[837,16584,1368],{"class":937},[837,16586,16587,16590],{"class":839,"line":944},[837,16588,16589],{"class":937},"        CarriersStore",[837,16591,1387],{"class":862},[837,16593,16594,16597,16599,16601,16603,16605,16607,16609,16611,16613,16615,16617,16619,16621,16623,16625,16627,16629],{"class":839,"line":959},[837,16595,16596],{"class":862},"        {",[837,16598,1546],{"class":937},[837,16600,870],{"class":869},[837,16602,3726],{"class":937},[837,16604,877],{"class":862},[837,16606,1556],{"class":937},[837,16608,870],{"class":869},[837,16610,1117],{"class":862},[837,16612,12645],{"class":854},[837,16614,870],{"class":869},[837,16616,1976],{"class":862},[837,16618,1114],{"class":924},[837,16620,12654],{"class":854},[837,16622,859],{"class":937},[837,16624,12659],{"class":1348},[837,16626,2114],{"class":937},[837,16628,3505],{"class":862},[837,16630,1148],{"class":862},[837,16632,16633,16636],{"class":839,"line":984},[837,16634,16635],{"class":937},"      ]",[837,16637,1387],{"class":862},[837,16639,16640,16642,16644],{"class":839,"line":991},[837,16641,1206],{"class":862},[837,16643,1011],{"class":937},[837,16645,941],{"class":862},[837,16647,16648,16650,16652],{"class":839,"line":1017},[837,16649,5172],{"class":862},[837,16651,1011],{"class":937},[837,16653,941],{"class":862},[837,16655,16656],{"class":839,"line":1022},[837,16657,988],{"emptyLinePlaceholder":987},[837,16659,16660,16663,16665,16668,16670,16673,16675,16677],{"class":839,"line":1033},[837,16661,16662],{"class":854},"  it",[837,16664,859],{"class":937},[837,16666,16667],{"class":873},"'populates carriers and marks the call successful'",[837,16669,877],{"class":862},[837,16671,16672],{"class":897}," async",[837,16674,1976],{"class":862},[837,16676,1114],{"class":924},[837,16678,914],{"class":862},[837,16680,16681,16683,16685,16687,16689,16691,16693,16695],{"class":839,"line":1053},[837,16682,7752],{"class":897},[837,16684,4896],{"class":1348},[837,16686,950],{"class":924},[837,16688,12690],{"class":937},[837,16690,1040],{"class":869},[837,16692,5544],{"class":854},[837,16694,7688],{"class":937},[837,16696,941],{"class":862},[837,16698,16699,16701,16703,16705,16707,16710,16712],{"class":839,"line":1098},[837,16700,4937],{"class":937},[837,16702,1040],{"class":869},[837,16704,4101],{"class":854},[837,16706,859],{"class":937},[837,16708,16709],{"class":1348},"RANGE",[837,16711,1011],{"class":937},[837,16713,941],{"class":862},[837,16715,16716,16719,16723,16725,16728,16730],{"class":839,"line":1151},[837,16717,16718],{"class":897},"    await",[837,16720,16722],{"class":16721},"sSzVO"," Promise",[837,16724,1040],{"class":869},[837,16726,16727],{"class":854},"resolve",[837,16729,1028],{"class":937},[837,16731,941],{"class":862},[837,16733,16734],{"class":839,"line":1203},[837,16735,988],{"emptyLinePlaceholder":987},[837,16737,16738,16741,16743,16745,16747,16749,16751,16754,16756,16759,16761],{"class":839,"line":1213},[837,16739,16740],{"class":854},"    expect",[837,16742,5518],{"class":937},[837,16744,1040],{"class":869},[837,16746,1109],{"class":854},[837,16748,1384],{"class":937},[837,16750,1040],{"class":869},[837,16752,16753],{"class":854},"toHaveLength",[837,16755,859],{"class":937},[837,16757,16758],{"class":953},"5",[837,16760,1011],{"class":937},[837,16762,941],{"class":862},[837,16764,16765,16767,16769,16771,16773,16775,16777,16780,16782,16784,16786],{"class":839,"line":1219},[837,16766,16740],{"class":854},[837,16768,5518],{"class":937},[837,16770,1040],{"class":869},[837,16772,5711],{"class":854},[837,16774,1384],{"class":937},[837,16776,1040],{"class":869},[837,16778,16779],{"class":854},"toBe",[837,16781,859],{"class":937},[837,16783,10901],{"class":953},[837,16785,1011],{"class":937},[837,16787,941],{"class":862},[837,16789,16790,16792,16794,16796,16798,16800,16802,16805,16807],{"class":839,"line":3975},[837,16791,16740],{"class":854},[837,16793,5518],{"class":937},[837,16795,1040],{"class":869},[837,16797,1312],{"class":854},[837,16799,1384],{"class":937},[837,16801,1040],{"class":869},[837,16803,16804],{"class":854},"toBeNull",[837,16806,1028],{"class":937},[837,16808,941],{"class":862},[837,16810,16811,16813,16815],{"class":839,"line":4039},[837,16812,5172],{"class":862},[837,16814,1011],{"class":937},[837,16816,941],{"class":862},[837,16818,16819,16821,16823],{"class":839,"line":4507},[837,16820,3505],{"class":862},[837,16822,1011],{"class":937},[837,16824,941],{"class":862},[594,16826,16827,16828,16831],{},"No DOM, no component, no change-detection choreography. Assert on ",[597,16829,16830],{},"outcomes"," (signal values)\nrather than implementation details (which service method got called how many times).",[1266,16833,16835],{"id":16834},"_102-zoneless-testing-changes-the-rules",[22,16836,528],{"title":625,"href":527},[658,16838,16839,16854,16869],{},[18,16840,16841,16842,16845,16846,679,16848,16850,16851,16853],{},"If ",[651,16843,16844],{},"zone.js"," isn't in ",[651,16847,3303],{},[651,16849,2799],{}," runs zoneless automatically. If it is still\nloaded but the app is zoneless, add ",[651,16852,3299],{}," to the test providers\nso tests match production.",[18,16855,16856,2304,16865,16868],{},[597,16857,11423,16858,16861,16862,1040],{},[651,16859,16860],{},"await fixture.whenStable()"," over ",[651,16863,16864],{},"fixture.detectChanges()",[651,16866,16867],{},"detectChanges()","\nforces a pass Angular might never have scheduled, which hides real missing-notification bugs.",[18,16870,16871,16873,16874,16877,16878,16881],{},[651,16872,2799],{}," now enforces OnPush-compatibility and throws\n",[651,16875,16876],{},"ExpressionChangedAfterItHasBeenCheckedError"," if a template value changed without a\nnotification — including from a test doing ",[651,16879,16880],{},"fixture.componentInstance.value = 'x'",". That's a\nreal finding: fix the component to use signals rather than papering over it.",[1266,16883,16885],{"id":16884},"_103-test-hygiene-that-prevents-cross-suite-flakiness",[22,16886,534],{"title":625,"href":533},[658,16888,16889,16901,16933,16950,16957],{},[18,16890,16891,2304,16894,2973,16896,16898,16899,1040],{},[597,16892,16893],{},"Prefer DI overrides to module mocks.",[651,16895,12398],{},[651,16897,12401],{}," on a shared module mutates\nthe runner's module cache and can break unrelated suites — especially with test isolation\ndisabled for speed. A token or provider override is scoped to ",[651,16900,2799],{},[18,16902,16903,675,16906,679,16909,679,16912,679,16915,16917,16918,16921,16922,16925,16926,16929,16930,1040],{},[597,16904,16905],{},"Restore anything global you touch",[651,16907,16908],{},"Date",[651,16910,16911],{},"Math.random",[651,16913,16914],{},"crypto",[651,16916,3225],{},".\n",[651,16919,16920],{},"vi.spyOn(...)"," plus ",[651,16923,16924],{},"restoreAllMocks()"," in ",[651,16927,16928],{},"afterEach",", never a raw\n",[651,16931,16932],{},"Object.defineProperty(globalThis, ...)",[18,16934,16935,16941,16942,3026,16944,679,16946,16949],{},[597,16936,4076,16937,16940],{},[651,16938,16939],{},"done()"," callbacks."," Use ",[651,16943,2699],{},[651,16945,2741],{},[651,16947,16948],{},"firstValueFrom",", fake timers, or return a\npromise, so a failure is owned by the test that caused it.",[18,16951,16952,16953,16956],{},"Don't reach for ",[651,16954,16955],{},"isolate: true",", forked pools, or big timeout bumps to fix flakiness. Those\nhide shared-state bugs; find the leak.",[18,16958,16959],{},"Test through public behaviour, and use component harnesses rather than CSS selectors when\ndriving design-system components.",[617,16961],{},[620,16963,16965],{"id":16964},"anti-pattern-catalogue",[22,16966,540],{"title":625,"href":539},[594,16968,16969],{},"The compressed version — useful as an agent's review checklist.",[1266,16971,16973],{"id":16972},"architecture",[22,16974,548],{"title":625,"href":547},[1645,16976,16977,16990],{},[1648,16978,16979],{},[1651,16980,16981,16984,16987],{},[1654,16982,16983],{},"Anti-pattern",[1654,16985,16986],{},"Why it's wrong",[1654,16988,16989],{},"Do instead",[1664,16991,16992,17003,17016,17033,17048,17059,17070,17086],{},[1651,16993,16994,16997,17000],{},[1669,16995,16996],{},"API service injected into a component",[1669,16998,16999],{},"Bypasses state, invisible in devtools, untestable",[1669,17001,17002],{},"Inject the store; call a store method",[1651,17004,17005,17008,17011],{},[1669,17006,17007],{},"Fetch orchestration in the component",[1669,17009,17010],{},"No cancellation, duplicated retry logic",[1669,17012,17013,17015],{},[651,17014,702],{}," in the store",[1651,17017,17018,17021,17024],{},[1669,17019,17020],{},"One feature importing another feature",[1669,17022,17023],{},"Cyclic coupling; nothing can be extracted later",[1669,17025,17026,17027,2973,17029,2973,17031],{},"Move the shared part to ",[651,17028,1678],{},[651,17030,1681],{},[651,17032,1684],{},[1651,17034,17035,17038,17041],{},[1669,17036,17037],{},"Eager-loading every route",[1669,17039,17040],{},"Initial bundle grows without bound",[1669,17042,17043,2973,17045,17047],{},[651,17044,2283],{},[651,17046,2287],{}," per feature",[1651,17049,17050,17053,17056],{},[1669,17051,17052],{},"Guard treated as security",[1669,17054,17055],{},"Trivially bypassed client-side",[1669,17057,17058],{},"Enforce server-side; the guard is UX",[1651,17060,17061,17064,17067],{},[1669,17062,17063],{},"Resolver on a slow endpoint",[1669,17065,17066],{},"Navigation blocks with no feedback",[1669,17068,17069],{},"Navigate, then render store call state",[1651,17071,17072,17077,17080],{},[1669,17073,17074,17076],{},[651,17075,1276],{}," in new code",[1669,17078,17079],{},"Obsolete",[1669,17081,17082,17083,17085],{},"Standalone + ",[651,17084,1484],{}," functions",[1651,17087,17088,17095,17098],{},[1669,17089,17090,17091,679,17093,1011],{},"Directories by type (",[651,17092,1772],{},[651,17094,1776],{},[1669,17096,17097],{},"Every change touches four places",[1669,17099,17100],{},"Colocate by feature",[1266,17102,17104],{"id":17103},"reactivity",[22,17105,554],{"title":625,"href":553},[1645,17107,17108,17118],{},[1648,17109,17110],{},[1651,17111,17112,17114,17116],{},[1654,17113,16983],{},[1654,17115,16986],{},[1654,17117,16989],{},[1664,17119,17120,17136,17154,17170,17185,17204,17220,17233,17246,17260,17276,17290,17303],{},[1651,17121,17122,17127,17130],{},[1669,17123,17124,17126],{},[651,17125,692],{}," that writes state",[1669,17128,17129],{},"Feedback loop in the reactive graph",[1669,17131,17132,2973,17134],{},[651,17133,682],{},[651,17135,685],{},[1651,17137,17138,17143,17149],{},[1669,17139,17140,17142],{},[651,17141,692],{}," to trigger a fetch",[1669,17144,17145,17146,17148],{},"Reinvents ",[651,17147,702],{},", minus cancellation",[1669,17150,17151,17152],{},"Pass the signal to the ",[651,17153,702],{},[1651,17155,17156,17161,17164],{},[1669,17157,17158,17160],{},[651,17159,692],{}," for DOM work",[1669,17162,17163],{},"Runs before render",[1669,17165,17166,2973,17168],{},[651,17167,3389],{},[651,17169,10577],{},[1651,17171,17172,17178,17181],{},[1669,17173,17174,7236,17176],{},[651,17175,692],{},[651,17177,2785],{},[1669,17179,17180],{},"No injection context — throws",[1669,17182,17183],{},[651,17184,5649],{},[1651,17186,17187,17198,17201],{},[1669,17188,17189,17190,17192,17193,17195,17196],{},"Calling ",[651,17191,702],{}," with ",[651,17194,8772],{}," not ",[651,17197,678],{},[1669,17199,17200],{},"Fetches once; never re-fetches",[1669,17202,17203],{},"Pass the signal itself, unread",[1651,17205,17206,17212,17215],{},[1669,17207,17208,17209,17211],{},"Reading a signal after ",[651,17210,2741],{}," in a reactive context",[1669,17213,17214],{},"Silently untracked",[1669,17216,17217,17218],{},"Read before the ",[651,17219,2741],{},[1651,17221,17222,17227,17230],{},[1669,17223,17224,17225],{},"Pass-through ",[651,17226,682],{},[1669,17228,17229],{},"Pure noise",[1669,17231,17232],{},"Use the signal directly",[1651,17234,17235,17240,17243],{},[1669,17236,17237,17238],{},"Mutating an object inside ",[651,17239,5556],{},[1669,17241,17242],{},"Same reference; view may not update",[1669,17244,17245],{},"Return a new reference",[1651,17247,17248,17251,17254],{},[1669,17249,17250],{},"Copying an input into a signal in an effect",[1669,17252,17253],{},"Two sources of truth",[1669,17255,17256,1241,17258],{},[651,17257,682],{},[651,17259,685],{},[1651,17261,17262,17267,17270],{},[1669,17263,17264,17266],{},[651,17265,8082],{}," in a component",[1669,17268,17269],{},"Leak + shadow copy of state",[1669,17271,17272,17273,17275],{},"Signals, or ",[651,17274,2699],{}," pipe",[1651,17277,17278,17283,17286],{},[1669,17279,17280,17282],{},[651,17281,8082],{}," in a store method",[1669,17284,17285],{},"Leak; a thrown error kills the pipeline permanently",[1669,17287,17288],{},[651,17289,6436],{},[1651,17291,17292,17297,17300],{},[1669,17293,17294,17295],{},"Manual subscription without ",[651,17296,8090],{},[1669,17298,17299],{},"Leak",[1669,17301,17302],{},"Add it, or don't subscribe",[1651,17304,17305,17316,17319],{},[1669,17306,17307,2973,17309,2973,17312,17315],{},[651,17308,8925],{},[651,17310,17311],{},"@Input",[651,17313,17314],{},"@Output"," decorators in new code",[1669,17317,17318],{},"Legacy; worse ergonomics and typing",[1669,17320,17321,2973,17323,2973,17325],{},[651,17322,8917],{},[651,17324,1594],{},[651,17326,8898],{},[1266,17328,17329],{"id":3485},[22,17330,560],{"title":625,"href":559},[1645,17332,17333,17343],{},[1648,17334,17335],{},[1651,17336,17337,17339,17341],{},[1654,17338,16983],{},[1654,17340,16986],{},[1654,17342,16989],{},[1664,17344,17345,17364,17376,17387,17403,17419,17432,17446,17459],{},[1651,17346,17347,17355,17358],{},[1669,17348,17349,17350,2973,17352,17354],{},"Hand-rolled ",[651,17351,3264],{},[651,17353,1312],{}," booleans",[1669,17356,17357],{},"Reset paths get missed; error never clears on retry",[1669,17359,17360,17361,17363],{},"Shared ",[651,17362,8525],{}," feature",[1651,17365,17366,17369,17372],{},[1669,17367,17368],{},"Derived value stored in state",[1669,17370,17371],{},"Drifts from its source",[1669,17373,17374],{},[651,17375,4657],{},[1651,17377,17378,17381,17384],{},[1669,17379,17380],{},"Transient value stored in state",[1669,17382,17383],{},"State grows without a consumer",[1669,17385,17386],{},"Method parameter",[1651,17388,17389,17397,17400],{},[1669,17390,17391,17392,2973,17394,17396],{},"Multiple ",[651,17393,4657],{},[651,17395,2785],{}," blocks",[1669,17398,17399],{},"Invisible ordering coupling",[1669,17401,17402],{},"One of each",[1651,17404,17405,17411,17414],{},[1669,17406,17407,17408,17410],{},"Bare ",[651,17409,5397],{}," with no description",[1669,17412,17413],{},"Unreadable devtools timeline",[1669,17415,17416],{},[651,17417,17418],{},"actionPatchState('method(): why', ...)",[1651,17420,17421,17426,17429],{},[1669,17422,17423,17424],{},"Missing ",[651,17425,14238],{},[1669,17427,17428],{},"Anonymous store in devtools",[1669,17430,17431],{},"Always last, always named",[1651,17433,17434,17439,17442],{},[1669,17435,17436,17438],{},[651,17437,6525],{}," for a search",[1669,17440,17441],{},"Out-of-order responses overwrite newer state",[1669,17443,17444],{},[651,17445,5617],{},[1651,17447,17448,17451,17454],{},[1669,17449,17450],{},"Root-provided store holding per-view state",[1669,17452,17453],{},"Leaks between consumers",[1669,17455,17456,17457],{},"Component or route ",[651,17458,11851],{},[1651,17460,17461,17466,17469],{},[1669,17462,17463,17465],{},[651,17464,668],{}," in a component that also owns selection\u002Ffilters\u002Fwrites",[1669,17467,17468],{},"A store with no structure",[1669,17470,17471],{},"Migrate to a signal store at the second concern",[1266,17473,17475],{"id":17474},"di-templates-and-the-rest",[22,17476,566],{"title":625,"href":565},[1645,17478,17479,17489],{},[1648,17480,17481],{},[1651,17482,17483,17485,17487],{},[1654,17484,16983],{},[1654,17486,16986],{},[1654,17488,16989],{},[1664,17490,17491,17504,17519,17532,17547,17560,17575,17594,17615,17633,17650,17665,17679,17697,17709,17720,17733,17747,17764,17775,17787,17801,17816],{},[1651,17492,17493,17496,17499],{},[1669,17494,17495],{},"Constructor injection in new code",[1669,17497,17498],{},"Doesn't compose into functions",[1669,17500,17501,17503],{},[651,17502,2291],{}," in a field initialiser",[1651,17505,17506,17511,17514],{},[1669,17507,17508,17510],{},[651,17509,2291],{}," in a method",[1669,17512,17513],{},"Throws — not an injection context",[1669,17515,17516,17517],{},"Field initialiser, or ",[651,17518,11584],{},[1651,17520,17521,17526,17529],{},[1669,17522,17523,17525],{},[651,17524,11476],{}," with one provider and one consumer",[1669,17527,17528],{},"Indirection with no payoff",[1669,17530,17531],{},"Inject the class",[1651,17533,17534,17539,17542],{},[1669,17535,17536,17538],{},[651,17537,12398],{}," on a shared module",[1669,17540,17541],{},"Mutates the runner's module cache; leaks across suites",[1669,17543,17544,17545],{},"DI override in ",[651,17546,2799],{},[1651,17548,17549,17554,17557],{},[1669,17550,17551,17553],{},[651,17552,8074],{}," in a decorator",[1669,17555,17556],{},"Default since v19; an error in v20+",[1669,17558,17559],{},"Delete it",[1651,17561,17562,17567,17570],{},[1669,17563,17564],{},[651,17565,17566],{},"ChangeDetectionStrategy.Default",[1669,17568,17569],{},"Deprecated in v22, due to be removed",[1669,17571,17572,17574],{},[651,17573,3361],{}," (the v22+ default)",[1651,17576,17577,17581,17591],{},[1669,17578,17579],{},[651,17580,8062],{},[1669,17582,17583,17584,17586,17587,17590],{},"Opts out of ",[651,17585,3361],{},"; usually an ",[651,17588,17589],{},"ng update"," leftover, not a decision",[1669,17592,17593],{},"Delete it and fix whatever relied on eager checking",[1651,17595,17596,17604,17607],{},[1669,17597,17598,2973,17601],{},[651,17599,17600],{},"ngClass",[651,17602,17603],{},"ngStyle",[1669,17605,17606],{},"Slower, needs an import",[1669,17608,17609,2973,17612],{},[651,17610,17611],{},"[class.x]",[651,17613,17614],{},"[style.x]",[1651,17616,17617,17624,17627],{},[1669,17618,17619,2973,17621],{},[651,17620,13752],{},[651,17622,17623],{},"*ngFor",[1669,17625,17626],{},"Legacy",[1669,17628,17629,2973,17631],{},[651,17630,13755],{},[651,17632,15938],{},[1651,17634,17635,17642,17645],{},[1669,17636,17637,17639,17640],{},[651,17638,15938],{}," without stable ",[651,17641,9539],{},[1669,17643,17644],{},"Full DOM rebuild; state rebinds to wrong rows",[1669,17646,17647],{},[651,17648,17649],{},"track item.id",[1651,17651,17652,17659,17662],{},[1669,17653,17654,17656,17657],{},[651,17655,16070],{}," as ",[651,17658,9539],{},[1669,17660,17661],{},"Same as no track when the list reorders",[1669,17663,17664],{},"Stable identity",[1651,17666,17667,17673,17675],{},[1669,17668,17669,2973,17671],{},[651,17670,8135],{},[651,17672,8138],{},[1669,17674,17626],{},[1669,17676,17677,8132],{},[651,17678,8131],{},[1651,17680,17681,17691,17694],{},[1669,17682,17683,17684,17687,17688],{},"Importing ",[651,17685,17686],{},"MatXModule"," instead of ",[651,17689,17690],{},"MatX",[1669,17692,17693],{},"Pulls in the whole barrel",[1669,17695,17696],{},"Import the standalone symbol",[1651,17698,17699,17703,17706],{},[1669,17700,17701],{},[651,17702,16184],{},[1669,17704,17705],{},"Deprecated; leaks; signals a design-system bypass",[1669,17707,17708],{},"Design-system component or token",[1651,17710,17711,17714,17717],{},[1669,17712,17713],{},"Hardcoded colour \u002F spacing",[1669,17715,17716],{},"Breaks theming and dark mode",[1669,17718,17719],{},"Design tokens, both themes",[1651,17721,17722,17727,17730],{},[1669,17723,17724,17726],{},[651,17725,3992],{}," selectors in templates",[1669,17728,17729],{},"Collides; not reusable",[1669,17731,17732],{},"Classes",[1651,17734,17735,17740,17743],{},[1669,17736,17737,17739],{},[651,17738,16154],{}," for an action",[1669,17741,17742],{},"No keyboard, no role, fails AXE",[1669,17744,17745],{},[651,17746,16150],{},[1651,17748,17749,17756,17759],{},[1669,17750,17751,1241,17753,17755],{},[651,17752,8174],{},[651,17754,8178],{}," to silence an error",[1669,17757,17758],{},"Moves a compile error to runtime",[1669,17760,17761,17763],{},[651,17762,8183],{}," + narrowing; fix the type",[1651,17765,17766,17769,17772],{},[1669,17767,17768],{},"Unbounded list fetch",[1669,17770,17771],{},"Grows with customer data until it doesn't",[1669,17773,17774],{},"Paginate",[1651,17776,17777,17780,17783],{},[1669,17778,17779],{},"Object literal in a template binding",[1669,17781,17782],{},"New reference every cycle",[1669,17784,17785],{},[651,17786,682],{},[1651,17788,17789,17794,17797],{},[1669,17790,17791,17793],{},[651,17792,16864],{}," under zoneless",[1669,17795,17796],{},"Hides missing-notification bugs",[1669,17798,17799],{},[651,17800,16860],{},[1651,17802,17803,17810,17813],{},[1669,17804,17805,17806,679,17808,1011],{},"Trusting external data (params, ",[651,17807,3225],{},[651,17809,14046],{},[1669,17811,17812],{},"Injection and crash surface",[1669,17814,17815],{},"Narrow at the boundary",[1651,17817,17818,17825,17828],{},[1669,17819,17820,2973,17822,17824],{},[651,17821,14033],{},[651,17823,14014],{}," without cause",[1669,17826,17827],{},"XSS",[1669,17829,17830],{},"Don't; if unavoidable, prove safety at the call site",[1266,17832,17834],{"id":17833},"security-observability-and-scale",[22,17835,572],{"title":625,"href":571},[1645,17837,17838,17848],{},[1648,17839,17840],{},[1651,17841,17842,17844,17846],{},[1654,17843,16983],{},[1654,17845,16986],{},[1654,17847,16989],{},[1664,17849,17850,17867,17878,17888,17904,17918,17929,17940,17956,17967,17983,17994],{},[1651,17851,17852,17859,17862],{},[1669,17853,17854,17855,3026,17857],{},"Access\u002Frefresh token in ",[651,17856,3225],{},[651,17858,13345],{},[1669,17860,17861],{},"Any XSS reads it synchronously; full session compromise",[1669,17863,17864,17866],{},[651,17865,13362],{}," cookie, or in-memory signal only",[1651,17868,17869,17872,17875],{},[1669,17870,17871],{},"Each failed request triggers its own token refresh",[1669,17873,17874],{},"Refresh storm; can invalidate the token other requests are retrying with",[1669,17876,17877],{},"One shared, in-flight refresh Observable",[1651,17879,17880,17883,17885],{},[1669,17881,17882],{},"Guard treated as the security boundary",[1669,17884,17055],{},[1669,17886,17887],{},"Guards are UX; enforce every rule server-side too",[1651,17889,17890,17898,17901],{},[1669,17891,17892,17893,17895,17896],{},"Store's ",[651,17894,6436],{}," error branch only calls ",[651,17897,14654],{},[1669,17899,17900],{},"Failure renders correctly but is invisible outside that user's screen",[1669,17902,17903],{},"Also report to the observability service",[1651,17905,17906,17912,17915],{},[1669,17907,17908,17909,17911],{},"Only a global ",[651,17910,14254],{},", no per-store reporting",[1669,17913,17914],{},"Errors caught downstream lose the context of what the user was doing",[1669,17916,17917],{},"Report at the point of failure, with action context",[1651,17919,17920,17923,17926],{},[1669,17921,17922],{},"PII in breadcrumbs or error context",[1669,17924,17925],{},"Turns an incident tool into a privacy incident",[1669,17927,17928],{},"Opaque user\u002Fsession ID only",[1651,17930,17931,17934,17937],{},[1669,17932,17933],{},"Secret\u002FAPI key embedded in the client bundle",[1669,17935,17936],{},"Ships to every visitor as plain, readable JS",[1669,17938,17939],{},"Secrets stay server-side; call your own API",[1651,17941,17942,17948,17951],{},[1669,17943,17944,17945,17947],{},"Two stores independently ",[651,17946,5397],{}," the same entity",[1669,17949,17950],{},"Whichever wrote last wins; the other silently goes stale",[1669,17952,17953,17954],{},"One entity store owns the write; readers derive via ",[651,17955,682],{},[1651,17957,17958,17961,17964],{},[1669,17959,17960],{},"Translated strings built by concatenation",[1669,17962,17963],{},"Breaks word order and pluralization in most other languages",[1669,17965,17966],{},"The i18n library's interpolation\u002Fpluralization support",[1651,17968,17969,17972,17975],{},[1669,17970,17971],{},"Dates\u002Fcurrency formatted with a template literal",[1669,17973,17974],{},"Wrong symbol placement, decimal separator, digit grouping per locale",[1669,17976,17977,2973,17979,2973,17981],{},[651,17978,3126],{},[651,17980,3129],{},[651,17982,3132],{},[1651,17984,17985,17988,17991],{},[1669,17986,17987],{},"Feature flag with no owner or removal condition",[1669,17989,17990],{},"Permanent second code path, tested half as often as the primary one",[1669,17992,17993],{},"Owner + removal condition at creation; test both branches",[1651,17995,17996,18005,18008],{},[1669,17997,3216,17998,3026,18000,3026,18002,18004],{},[651,17999,3219],{},[651,18001,3222],{},[651,18003,3234],{}," during SSR render",[1669,18006,18007],{},"Server and client compute different output → hydration mismatch, silent client re-render",[1669,18009,18010],{},"Guard browser-only reads; keep render output deterministic",[617,18012],{},[620,18014,18016],{"id":18015},"appendix-rules-block-for-your-agent",[22,18017,578],{"title":625,"href":577},[594,18019,18020,18021,679,18023,679,18025,18027],{},"Paste into ",[651,18022,752],{},[651,18024,1240],{},[651,18026,1244],{},", or a system prompt. Deliberately terse and\nimperative — agents follow directives better than prose.",[644,18029,18033],{"className":18030,"code":18031,"language":18032,"meta":653,"style":653},"language-markdown shiki shiki-themes github-light catppuccin-mocha","## Modern Angular rules (v20+, NgRx Signals)\n\n### Version baseline\n- Current release is v22. v20 and v21 are in LTS. Check `@angular\u002Fcore` before applying\n  anything version-gated below.\n- v22: `OnPush` is the default strategy, Signal Forms and the Resource API are stable,\n  TypeScript 6 and Node 22+ required, `HttpClient` defaults to Fetch.\n- v21: zoneless became the default; Signal Forms shipped experimental.\n- v20: zoneless needs `provideZonelessChangeDetection()`; no Signal Forms.\n\n### Architecture\n- Standalone only. `bootstrapApplication` + `ApplicationConfig`. No NgModule.\n- Never write `standalone: true` — it is the default and an error in v20+.\n- Expose feature\u002Flibrary config via `provide*` functions returning `EnvironmentProviders`.\n- Enable `provideRouter(routes, withComponentInputBinding())` and bind route params as\n  signal `input()`s. Do not use ActivatedRoute subscriptions in new code.\n- Group by feature and colocate (component + template + styles + store + spec together).\n- Dependency direction: features → data-access\u002Fui\u002Futil → nothing. A feature MUST NOT\n  import another feature. Enforce with lint, not documentation; graduate to real Nx\n  libraries with `depConstraints` once folders and lint stop being enough.\n- Lazy-load every feature route (`loadChildren` \u002F `loadComponent`). Eager-load only shell\n  and landing.\n- `canMatch` for existence (feature flags, licensing); `canActivate` for permission.\n  Guards are UX — every rule must also be enforced server-side.\n- Gate feature flags at all three layers (route `canMatch`, template `@if`, store method)\n  through one `FeatureFlagsService`. Every flag ships with an owner and a removal condition.\n- Avoid resolvers on slow endpoints; navigate and render store call state instead.\n- `@defer (on viewport)` for heavy below-the-fold content, with `@placeholder` and\n  `@loading (after Xms; minimum Yms)` to prevent flicker.\n- Pick an i18n strategy (compile-time `$localize` vs a runtime library) before the first\n  component ships. Format dates\u002Fnumbers\u002Fcurrency via `Intl`\u002Fpipes, never string templates.\n  Never concatenate translated fragments.\n- Decide SSR\u002Fprerender per route, not per app. Avoid reading `window`\u002F`document`\u002F`Date.now()`\n  during render — it causes hydration mismatches.\n- Assume ZONELESS (default in v21+): signals drive the view. `OnPush` everywhere.\n  NgZone.onStable\u002FonMicrotaskEmpty\u002FisStable are dead — use afterNextRender\u002FafterEveryRender.\n  Reactive-forms setValue\u002FpatchValue do NOT trigger CD; bridge via signal or markForCheck.\n  Wrap SSR-critical async work in `PendingTasks.run()`.\n\n### Layering (non-negotiable)\n- Data flow is Component → Store → Service. A component MUST NOT inject or call a service\n  that performs I\u002FO (HttpClient, GraphQL client, fetch, resource).\n- Stateless, I\u002FO-free services (formatters, calculators) MAY be injected into components.\n- Fetch\u002Fpoll\u002Fretry\u002Fcancel orchestration belongs in a store `rxMethod`, never in a component.\n- Cross-cutting HTTP concerns (auth headers, retry, error normalisation, correlation IDs)\n  go in functional interceptors registered at bootstrap — not repeated per service.\n\n### Store vs. rxResource\n- Default to a signal store.\n- `rxResource` is permitted ONLY when all hold: one read; no writes; no state beyond the\n  fetched value and its status; keyed by one signal or one trivial computed; no other\n  consumer. (Stable as of v22.0; experimental through v21.)\n- Graduate to a store at the SECOND concern — a second fetch, any write-back, selection,\n  filtering, sorting, pagination, polling, debouncing, retry, or a second consumer. Never\n  accumulate an rxResource plus loose component signals.\n- `rxResource` takes `params` (a function) and `stream`. Never wrap it in an `effect()`.\n\n### Services\n- One method per operation, named for the operation. Return a cold Observable.\n- Map DTO → domain model inside the service. No state, no caching, no loading flags.\n- `providedIn: 'root'` unless scoping is required. Single responsibility.\n- Name I\u002FO services `*-api.service.ts` so the no-component-injection rule is lintable.\n\n### Stores (NgRx Signal Store)\n- Feature order: withState → withCallState → withEntities? → withComputed → withMethods\n  → withHooks? → withLogger. Exactly ONE withComputed and ONE withMethods block.\n- `withLogger('XxxStore')` mandatory, always last, named to match the export. Devtools-only\n  — pair it with production error reporting (see Observability), which is a separate concern.\n- State is flat. Derived values go in withComputed, never in state. Transient\u002Fsingle-use\n  values are method parameters, not state.\n- Use the shared `withCallState` feature for loading\u002Ferror. Never hand-roll boolean flags.\n- Never let two stores independently `patchState` the same conceptual entity. One entity\n  store (or a single owning store) writes; other stores derive via `computed`.\n- Every mutation: `actionPatchState('method(): why', store, ...)`.\n- Async flows use `rxMethod` + `tapResponse`. NEVER `subscribe()` inside a store.\n- To make a fetch reactive, call the rxMethod with the SIGNAL, not the read value:\n  `store.load(this.id)` — never `store.load(this.id())` inside an `effect()`. Combine\n  multiple dependencies into one `computed` and pass that. Call it from a field initialiser\n  or the constructor (needs an injection context).\n- Flattening operator is a decision: switchMap (latest wins, default for reads), exhaustMap\n  (one at a time, submits), concatMap (ordered mutations). mergeMap almost never.\n- Inject services as default params of withMethods\u002FwithComputed\u002FwithHooks — never at store\n  scope. `inject()` outside those blocks throws.\n- Effects belong in `withHooks.onInit` with `effect((onCleanup) => ...)`. Never in withMethods.\n- Default to component-scoped (`providers: [XxxStore]`); route `providers` for feature-scoped;\n  `providedIn: 'root'` only for genuinely shared state.\n- Over ~200 lines, extract pure functions to `*.computed.ts` \u002F `*.methods.ts`; store methods\n  become 1–5 line wrappers.\n\n### Reactivity\n- Pick from this menu, do not improvise:\n  local writable state → `signal()`; derived → `computed()`; writable-but-resets-on-source\n  → `linkedSignal()`; async keyed on signals → store `rxMethod` given the signal;\n  DOM after render → `afterNextRender`\u002F`afterRenderEffect`; sync to a non-reactive API →\n  `effect()` (last resort).\n- `set`\u002F`update` only — never `mutate`. Always return NEW references from `update`.\n- Expose `asReadonly()` from services\u002Fstores; never a public writable signal.\n- `computed` must be pure and cheap. NO pass-through computed.\n- Read signals BEFORE any `await` — tracking stops at the first await.\n- `untracked()` to read without depending.\n- NEVER write state from inside an `effect()` — no set\u002Fupdate\u002FpatchState. Hard blocker.\n- AVOID `effect()` wherever an alternative exists. Any surviving effect carries a comment\n  naming the alternative considered and why it didn't fit.\n- Signal APIs only: `input()`, `input.required()`, `model()`, `output()`, `viewChild()`,\n  `viewChildren()`, `contentChild()`, `contentChildren()`. Never the decorator versions.\n- Name outputs for the event (`carrierSelected`), not the handler (`onSelect`).\n- Keep RxJS inside store rxMethods. `toSignal` needs `initialValue`. Any manual subscription\n  requires `takeUntilDestroyed()`.\n\n### Components\n- Every component runs under `OnPush`. It is the DEFAULT in v22+ — do not write it there. On\n  v20\u002Fv21 set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly. Never use\n  `ChangeDetectionStrategy.Eager` or the deprecated `Default`.\n- NO `.subscribe()`. Only exception: dialog `afterClosed()`.\n- Small and single-responsibility. Inline templates for small components; external files use\n  paths relative to the component .ts.\n- `protected` for template-only members, `private` for internals, `readonly` on injected deps.\n- Host bindings in the `host` metadata object — not @HostBinding\u002F@HostListener.\n- Import standalone components (`MatButton`), not modules (`MatButtonModule`). No unused\n  entries in `imports`; no unused TS imports.\n\n### Dependency injection\n- `inject()` in field initialisers. No constructor injection in new code.\n- Injection contexts are: field initialisers, constructors, factory functions, and\n  Angular-invoked functional APIs (guards, resolvers, interceptors). Outside those use\n  `runInInjectionContext`; in helpers that call inject() use `assertInInjectionContext`.\n- `providedIn: 'root'` for stateless services; component\u002Froute `providers` for stateful\n  scoped things. Remember a component provider SHADOWS a root one for its subtree.\n- `viewProviders` when projected content must not see the service.\n- `InjectionToken` only for non-class deps, competing implementations, `multi` collections,\n  or a genuine test seam. Delete single-provider\u002Fsingle-consumer\u002Fnever-overridden tokens.\n- `useExisting` aliases the same instance; `useClass` creates a new one. Don't confuse them.\n- `inject(X, { optional: true })` for optional integrations.\n\n### Auth & security\n- NEVER store an access or refresh token in `localStorage`\u002F`sessionStorage`. `httpOnly`\n  cookie or an in-memory signal only.\n- Share ONE in-flight token-refresh Observable across concurrent 401s. Never let every\n  failed request trigger its own refresh call.\n- Guards (`canActivate`\u002F`canMatch`) and conditional rendering are UX only — every rule they\n  enforce MUST also be enforced server-side.\n- CSRF defense (`withXsrfConfiguration`) only applies to cookie-based auth.\n- Strict CSP: no `unsafe-inline`\u002F`unsafe-eval`. Nonce any inline script via `ngCspNonce`.\n- Sanitize\u002Fvalidate untrusted external data once, at the service boundary.\n- No secrets (API keys, signing keys, credentials) in the client bundle — ever.\n\n### Observability\n- Install one global `ErrorHandler` whose own reporting call cannot throw.\n- Normalize HTTP errors once, at an interceptor, into a shared domain-error shape.\n- Every store's `tapResponse` error branch must both set UI-facing call state AND report to\n  the observability service — one without the other is an incomplete implementation.\n- Thread a correlation ID through requests; attach it to every error report.\n- Breadcrumb recent actions for context; never put PII in breadcrumbs or error metadata.\n- Report Core Web Vitals (LCP\u002FINP\u002FCLS) per route in production, not just local Lighthouse.\n\n### Forms\n- v22+ new forms → Signal Forms (`@angular\u002Fforms\u002Fsignals`), now stable. v21 → Signal Forms were\n  experimental; prefer Reactive Forms if you need stability guarantees. v20 → Reactive Forms\n  (Signal Forms don't exist). Existing features → match what's there (typed Reactive Forms\n  preferred).\n- Signal Forms models: NEVER null\u002Fundefined initial values — '' for strings, 0 for numbers,\n  [] for arrays.\n- Never mix reactive and template-driven bindings on one control.\n- Validation is the form's job; SUBMISSION is a store rxMethod (exhaustMap).\n\n### Templates\n- `@if` \u002F `@for` \u002F `@switch` only. `track` on every `@for` and it must be a stable identity,\n  never `$index`. Use `@empty`. Use `@default never;` for exhaustive switches.\n- Keep templates dumb — no filter\u002Fmap\u002Fsort chains, no allocating calls. Move to `computed`.\n- No object\u002Farray literals in bindings (new reference every CD cycle).\n- Don't assume globals (`new Date()`, `window`) — this also causes SSR hydration mismatches.\n  Import every pipe used.\n- `NgOptimizedImage` for static images (not base64).\n- Semantic HTML: `\u003Cbutton>` for actions, never `\u003Cdiv (click)>`.\n\n### Styling\n- Wrap component styles in `:host`. Keep default encapsulation.\n- Components own their spacing\u002Flayout and must render correctly anywhere — never depend on an\n  ancestor's styles.\n- No `::ng-deep` (deprecated, leaks, signals a design-system bypass).\n- Design tokens for colour\u002Fspacing\u002Ftypography — never hardcoded values. New tokens need\n  light AND dark values.\n- Logical properties (`margin-inline-start`). Consistent class naming (BEM). Classes, not `id`.\n\n### Accessibility\n- MUST pass AXE. MUST meet WCAG AA: focus management, colour contrast, ARIA.\n- Semantic elements first; ARIA only for real gaps. Keyboard reachable + visible focus.\n- Labels on all inputs; `aria-label` on icon-only buttons. Announce async state via live\n  regions. Never colour alone. Meaningful `alt`, or `alt=\"\"` if decorative.\n- Complex widgets (combobox, tree, tabs, grid): use accessible primitives, don't hand-roll ARIA.\n\n### Typing\n- Strict mode. No `any` — prefer `unknown` and narrow. No `as` casts to silence a mismatch.\n- Prefer inference; annotate only when not obvious.\n- Treat all external data (API responses, route\u002Fquery params, localStorage, postMessage) as\n  untrusted and narrow at the boundary.\n- No `innerHTML` \u002F `bypassSecurityTrust*` without a cited, provable reason.\n\n### Performance\n- Lazy-load features; `@defer` heavy in-page content; check the cost of new dependencies.\n- Paginate every list endpoint. No unbounded fetches that grow with customer data.\n- Keep `computed` and template expressions cheap — they re-run per CD cycle. Precompute\n  anything iterating large collections or formatting many rows.\n- No N+1 fetch patterns. No heavy synchronous work in click handlers, constructors, ngOnInit.\n- Measure before optimising further.\n\n### Testing\n- Weight coverage at the STORE level: stub the service via DI, call the method, assert\n  signals. Assert outcomes, not implementation details.\n- Prefer DI\u002FTestBed overrides over module mocks (`vi.mock`\u002F`jest.mock` mutate the runner's\n  module cache and leak across suites).\n- Zoneless: prefer `await fixture.whenStable()` over `fixture.detectChanges()`.\n- Never mutate globals (Date, crypto, localStorage, Math.random) without restoring them.\n- No `done()` callbacks — use async\u002Fawait, `firstValueFrom`, or fake timers.\n- Don't paper over flakiness with isolation\u002Ftimeout overrides; find the shared-state leak.\n- Cover presentational components via rendered output \u002F harnesses; container components via E2E.\n- Override `FeatureFlagsService`\u002F`AuthStore` via DI to test both branches of a flag or an\n  authenticated vs. unauthenticated flow — never conditionally skip a test on real state.\n","markdown",[651,18034,18035,18040,18044,18049,18054,18059,18064,18069,18074,18079,18083,18088,18093,18098,18103,18108,18113,18118,18123,18128,18133,18138,18143,18148,18153,18158,18163,18168,18173,18178,18183,18188,18194,18200,18206,18212,18218,18224,18230,18235,18241,18247,18253,18259,18265,18271,18277,18282,18288,18294,18300,18306,18312,18318,18324,18330,18336,18341,18347,18353,18359,18365,18371,18376,18382,18388,18394,18400,18406,18412,18418,18424,18430,18436,18442,18448,18454,18460,18466,18472,18478,18484,18490,18496,18502,18508,18514,18520,18526,18531,18537,18543,18549,18555,18561,18567,18573,18579,18585,18591,18597,18603,18609,18615,18621,18627,18633,18639,18645,18650,18656,18662,18668,18674,18680,18686,18692,18698,18704,18710,18716,18721,18727,18733,18739,18745,18751,18757,18763,18769,18775,18781,18787,18793,18798,18804,18810,18816,18822,18828,18834,18840,18846,18852,18858,18864,18869,18875,18881,18887,18893,18899,18905,18911,18917,18922,18928,18934,18940,18946,18952,18958,18964,18970,18976,18981,18987,18993,18999,19005,19011,19017,19023,19029,19035,19040,19046,19052,19058,19064,19070,19076,19082,19088,19093,19099,19105,19111,19117,19123,19129,19134,19140,19146,19152,19158,19164,19170,19175,19181,19187,19193,19199,19205,19211,19217,19222,19228,19234,19240,19246,19252,19258,19264,19270,19276,19282,19288],{"__ignoreMap":653},[837,18036,18037],{"class":839,"line":840},[837,18038,18039],{},"## Modern Angular rules (v20+, NgRx Signals)\n",[837,18041,18042],{"class":839,"line":847},[837,18043,988],{"emptyLinePlaceholder":987},[837,18045,18046],{"class":839,"line":894},[837,18047,18048],{},"### Version baseline\n",[837,18050,18051],{"class":839,"line":917},[837,18052,18053],{},"- Current release is v22. v20 and v21 are in LTS. Check `@angular\u002Fcore` before applying\n",[837,18055,18056],{"class":839,"line":944},[837,18057,18058],{},"  anything version-gated below.\n",[837,18060,18061],{"class":839,"line":959},[837,18062,18063],{},"- v22: `OnPush` is the default strategy, Signal Forms and the Resource API are stable,\n",[837,18065,18066],{"class":839,"line":984},[837,18067,18068],{},"  TypeScript 6 and Node 22+ required, `HttpClient` defaults to Fetch.\n",[837,18070,18071],{"class":839,"line":991},[837,18072,18073],{},"- v21: zoneless became the default; Signal Forms shipped experimental.\n",[837,18075,18076],{"class":839,"line":1017},[837,18077,18078],{},"- v20: zoneless needs `provideZonelessChangeDetection()`; no Signal Forms.\n",[837,18080,18081],{"class":839,"line":1022},[837,18082,988],{"emptyLinePlaceholder":987},[837,18084,18085],{"class":839,"line":1033},[837,18086,18087],{},"### Architecture\n",[837,18089,18090],{"class":839,"line":1053},[837,18091,18092],{},"- Standalone only. `bootstrapApplication` + `ApplicationConfig`. No NgModule.\n",[837,18094,18095],{"class":839,"line":1098},[837,18096,18097],{},"- Never write `standalone: true` — it is the default and an error in v20+.\n",[837,18099,18100],{"class":839,"line":1151},[837,18101,18102],{},"- Expose feature\u002Flibrary config via `provide*` functions returning `EnvironmentProviders`.\n",[837,18104,18105],{"class":839,"line":1203},[837,18106,18107],{},"- Enable `provideRouter(routes, withComponentInputBinding())` and bind route params as\n",[837,18109,18110],{"class":839,"line":1213},[837,18111,18112],{},"  signal `input()`s. Do not use ActivatedRoute subscriptions in new code.\n",[837,18114,18115],{"class":839,"line":1219},[837,18116,18117],{},"- Group by feature and colocate (component + template + styles + store + spec together).\n",[837,18119,18120],{"class":839,"line":3975},[837,18121,18122],{},"- Dependency direction: features → data-access\u002Fui\u002Futil → nothing. A feature MUST NOT\n",[837,18124,18125],{"class":839,"line":4039},[837,18126,18127],{},"  import another feature. Enforce with lint, not documentation; graduate to real Nx\n",[837,18129,18130],{"class":839,"line":4507},[837,18131,18132],{},"  libraries with `depConstraints` once folders and lint stop being enough.\n",[837,18134,18135],{"class":839,"line":4520},[837,18136,18137],{},"- Lazy-load every feature route (`loadChildren` \u002F `loadComponent`). Eager-load only shell\n",[837,18139,18140],{"class":839,"line":4542},[837,18141,18142],{},"  and landing.\n",[837,18144,18145],{"class":839,"line":4575},[837,18146,18147],{},"- `canMatch` for existence (feature flags, licensing); `canActivate` for permission.\n",[837,18149,18150],{"class":839,"line":4606},[837,18151,18152],{},"  Guards are UX — every rule must also be enforced server-side.\n",[837,18154,18155],{"class":839,"line":4627},[837,18156,18157],{},"- Gate feature flags at all three layers (route `canMatch`, template `@if`, store method)\n",[837,18159,18160],{"class":839,"line":4645},[837,18161,18162],{},"  through one `FeatureFlagsService`. Every flag ships with an owner and a removal condition.\n",[837,18164,18165],{"class":839,"line":7969},[837,18166,18167],{},"- Avoid resolvers on slow endpoints; navigate and render store call state instead.\n",[837,18169,18170],{"class":839,"line":7974},[837,18171,18172],{},"- `@defer (on viewport)` for heavy below-the-fold content, with `@placeholder` and\n",[837,18174,18175],{"class":839,"line":7998},[837,18176,18177],{},"  `@loading (after Xms; minimum Yms)` to prevent flicker.\n",[837,18179,18180],{"class":839,"line":8017},[837,18181,18182],{},"- Pick an i18n strategy (compile-time `$localize` vs a runtime library) before the first\n",[837,18184,18185],{"class":839,"line":8022},[837,18186,18187],{},"  component ships. Format dates\u002Fnumbers\u002Fcurrency via `Intl`\u002Fpipes, never string templates.\n",[837,18189,18191],{"class":839,"line":18190},32,[837,18192,18193],{},"  Never concatenate translated fragments.\n",[837,18195,18197],{"class":839,"line":18196},33,[837,18198,18199],{},"- Decide SSR\u002Fprerender per route, not per app. Avoid reading `window`\u002F`document`\u002F`Date.now()`\n",[837,18201,18203],{"class":839,"line":18202},34,[837,18204,18205],{},"  during render — it causes hydration mismatches.\n",[837,18207,18209],{"class":839,"line":18208},35,[837,18210,18211],{},"- Assume ZONELESS (default in v21+): signals drive the view. `OnPush` everywhere.\n",[837,18213,18215],{"class":839,"line":18214},36,[837,18216,18217],{},"  NgZone.onStable\u002FonMicrotaskEmpty\u002FisStable are dead — use afterNextRender\u002FafterEveryRender.\n",[837,18219,18221],{"class":839,"line":18220},37,[837,18222,18223],{},"  Reactive-forms setValue\u002FpatchValue do NOT trigger CD; bridge via signal or markForCheck.\n",[837,18225,18227],{"class":839,"line":18226},38,[837,18228,18229],{},"  Wrap SSR-critical async work in `PendingTasks.run()`.\n",[837,18231,18233],{"class":839,"line":18232},39,[837,18234,988],{"emptyLinePlaceholder":987},[837,18236,18238],{"class":839,"line":18237},40,[837,18239,18240],{},"### Layering (non-negotiable)\n",[837,18242,18244],{"class":839,"line":18243},41,[837,18245,18246],{},"- Data flow is Component → Store → Service. A component MUST NOT inject or call a service\n",[837,18248,18250],{"class":839,"line":18249},42,[837,18251,18252],{},"  that performs I\u002FO (HttpClient, GraphQL client, fetch, resource).\n",[837,18254,18256],{"class":839,"line":18255},43,[837,18257,18258],{},"- Stateless, I\u002FO-free services (formatters, calculators) MAY be injected into components.\n",[837,18260,18262],{"class":839,"line":18261},44,[837,18263,18264],{},"- Fetch\u002Fpoll\u002Fretry\u002Fcancel orchestration belongs in a store `rxMethod`, never in a component.\n",[837,18266,18268],{"class":839,"line":18267},45,[837,18269,18270],{},"- Cross-cutting HTTP concerns (auth headers, retry, error normalisation, correlation IDs)\n",[837,18272,18274],{"class":839,"line":18273},46,[837,18275,18276],{},"  go in functional interceptors registered at bootstrap — not repeated per service.\n",[837,18278,18280],{"class":839,"line":18279},47,[837,18281,988],{"emptyLinePlaceholder":987},[837,18283,18285],{"class":839,"line":18284},48,[837,18286,18287],{},"### Store vs. rxResource\n",[837,18289,18291],{"class":839,"line":18290},49,[837,18292,18293],{},"- Default to a signal store.\n",[837,18295,18297],{"class":839,"line":18296},50,[837,18298,18299],{},"- `rxResource` is permitted ONLY when all hold: one read; no writes; no state beyond the\n",[837,18301,18303],{"class":839,"line":18302},51,[837,18304,18305],{},"  fetched value and its status; keyed by one signal or one trivial computed; no other\n",[837,18307,18309],{"class":839,"line":18308},52,[837,18310,18311],{},"  consumer. (Stable as of v22.0; experimental through v21.)\n",[837,18313,18315],{"class":839,"line":18314},53,[837,18316,18317],{},"- Graduate to a store at the SECOND concern — a second fetch, any write-back, selection,\n",[837,18319,18321],{"class":839,"line":18320},54,[837,18322,18323],{},"  filtering, sorting, pagination, polling, debouncing, retry, or a second consumer. Never\n",[837,18325,18327],{"class":839,"line":18326},55,[837,18328,18329],{},"  accumulate an rxResource plus loose component signals.\n",[837,18331,18333],{"class":839,"line":18332},56,[837,18334,18335],{},"- `rxResource` takes `params` (a function) and `stream`. Never wrap it in an `effect()`.\n",[837,18337,18339],{"class":839,"line":18338},57,[837,18340,988],{"emptyLinePlaceholder":987},[837,18342,18344],{"class":839,"line":18343},58,[837,18345,18346],{},"### Services\n",[837,18348,18350],{"class":839,"line":18349},59,[837,18351,18352],{},"- One method per operation, named for the operation. Return a cold Observable.\n",[837,18354,18356],{"class":839,"line":18355},60,[837,18357,18358],{},"- Map DTO → domain model inside the service. No state, no caching, no loading flags.\n",[837,18360,18362],{"class":839,"line":18361},61,[837,18363,18364],{},"- `providedIn: 'root'` unless scoping is required. Single responsibility.\n",[837,18366,18368],{"class":839,"line":18367},62,[837,18369,18370],{},"- Name I\u002FO services `*-api.service.ts` so the no-component-injection rule is lintable.\n",[837,18372,18374],{"class":839,"line":18373},63,[837,18375,988],{"emptyLinePlaceholder":987},[837,18377,18379],{"class":839,"line":18378},64,[837,18380,18381],{},"### Stores (NgRx Signal Store)\n",[837,18383,18385],{"class":839,"line":18384},65,[837,18386,18387],{},"- Feature order: withState → withCallState → withEntities? → withComputed → withMethods\n",[837,18389,18391],{"class":839,"line":18390},66,[837,18392,18393],{},"  → withHooks? → withLogger. Exactly ONE withComputed and ONE withMethods block.\n",[837,18395,18397],{"class":839,"line":18396},67,[837,18398,18399],{},"- `withLogger('XxxStore')` mandatory, always last, named to match the export. Devtools-only\n",[837,18401,18403],{"class":839,"line":18402},68,[837,18404,18405],{},"  — pair it with production error reporting (see Observability), which is a separate concern.\n",[837,18407,18409],{"class":839,"line":18408},69,[837,18410,18411],{},"- State is flat. Derived values go in withComputed, never in state. Transient\u002Fsingle-use\n",[837,18413,18415],{"class":839,"line":18414},70,[837,18416,18417],{},"  values are method parameters, not state.\n",[837,18419,18421],{"class":839,"line":18420},71,[837,18422,18423],{},"- Use the shared `withCallState` feature for loading\u002Ferror. Never hand-roll boolean flags.\n",[837,18425,18427],{"class":839,"line":18426},72,[837,18428,18429],{},"- Never let two stores independently `patchState` the same conceptual entity. One entity\n",[837,18431,18433],{"class":839,"line":18432},73,[837,18434,18435],{},"  store (or a single owning store) writes; other stores derive via `computed`.\n",[837,18437,18439],{"class":839,"line":18438},74,[837,18440,18441],{},"- Every mutation: `actionPatchState('method(): why', store, ...)`.\n",[837,18443,18445],{"class":839,"line":18444},75,[837,18446,18447],{},"- Async flows use `rxMethod` + `tapResponse`. NEVER `subscribe()` inside a store.\n",[837,18449,18451],{"class":839,"line":18450},76,[837,18452,18453],{},"- To make a fetch reactive, call the rxMethod with the SIGNAL, not the read value:\n",[837,18455,18457],{"class":839,"line":18456},77,[837,18458,18459],{},"  `store.load(this.id)` — never `store.load(this.id())` inside an `effect()`. Combine\n",[837,18461,18463],{"class":839,"line":18462},78,[837,18464,18465],{},"  multiple dependencies into one `computed` and pass that. Call it from a field initialiser\n",[837,18467,18469],{"class":839,"line":18468},79,[837,18470,18471],{},"  or the constructor (needs an injection context).\n",[837,18473,18475],{"class":839,"line":18474},80,[837,18476,18477],{},"- Flattening operator is a decision: switchMap (latest wins, default for reads), exhaustMap\n",[837,18479,18481],{"class":839,"line":18480},81,[837,18482,18483],{},"  (one at a time, submits), concatMap (ordered mutations). mergeMap almost never.\n",[837,18485,18487],{"class":839,"line":18486},82,[837,18488,18489],{},"- Inject services as default params of withMethods\u002FwithComputed\u002FwithHooks — never at store\n",[837,18491,18493],{"class":839,"line":18492},83,[837,18494,18495],{},"  scope. `inject()` outside those blocks throws.\n",[837,18497,18499],{"class":839,"line":18498},84,[837,18500,18501],{},"- Effects belong in `withHooks.onInit` with `effect((onCleanup) => ...)`. Never in withMethods.\n",[837,18503,18505],{"class":839,"line":18504},85,[837,18506,18507],{},"- Default to component-scoped (`providers: [XxxStore]`); route `providers` for feature-scoped;\n",[837,18509,18511],{"class":839,"line":18510},86,[837,18512,18513],{},"  `providedIn: 'root'` only for genuinely shared state.\n",[837,18515,18517],{"class":839,"line":18516},87,[837,18518,18519],{},"- Over ~200 lines, extract pure functions to `*.computed.ts` \u002F `*.methods.ts`; store methods\n",[837,18521,18523],{"class":839,"line":18522},88,[837,18524,18525],{},"  become 1–5 line wrappers.\n",[837,18527,18529],{"class":839,"line":18528},89,[837,18530,988],{"emptyLinePlaceholder":987},[837,18532,18534],{"class":839,"line":18533},90,[837,18535,18536],{},"### Reactivity\n",[837,18538,18540],{"class":839,"line":18539},91,[837,18541,18542],{},"- Pick from this menu, do not improvise:\n",[837,18544,18546],{"class":839,"line":18545},92,[837,18547,18548],{},"  local writable state → `signal()`; derived → `computed()`; writable-but-resets-on-source\n",[837,18550,18552],{"class":839,"line":18551},93,[837,18553,18554],{},"  → `linkedSignal()`; async keyed on signals → store `rxMethod` given the signal;\n",[837,18556,18558],{"class":839,"line":18557},94,[837,18559,18560],{},"  DOM after render → `afterNextRender`\u002F`afterRenderEffect`; sync to a non-reactive API →\n",[837,18562,18564],{"class":839,"line":18563},95,[837,18565,18566],{},"  `effect()` (last resort).\n",[837,18568,18570],{"class":839,"line":18569},96,[837,18571,18572],{},"- `set`\u002F`update` only — never `mutate`. Always return NEW references from `update`.\n",[837,18574,18576],{"class":839,"line":18575},97,[837,18577,18578],{},"- Expose `asReadonly()` from services\u002Fstores; never a public writable signal.\n",[837,18580,18582],{"class":839,"line":18581},98,[837,18583,18584],{},"- `computed` must be pure and cheap. NO pass-through computed.\n",[837,18586,18588],{"class":839,"line":18587},99,[837,18589,18590],{},"- Read signals BEFORE any `await` — tracking stops at the first await.\n",[837,18592,18594],{"class":839,"line":18593},100,[837,18595,18596],{},"- `untracked()` to read without depending.\n",[837,18598,18600],{"class":839,"line":18599},101,[837,18601,18602],{},"- NEVER write state from inside an `effect()` — no set\u002Fupdate\u002FpatchState. Hard blocker.\n",[837,18604,18606],{"class":839,"line":18605},102,[837,18607,18608],{},"- AVOID `effect()` wherever an alternative exists. Any surviving effect carries a comment\n",[837,18610,18612],{"class":839,"line":18611},103,[837,18613,18614],{},"  naming the alternative considered and why it didn't fit.\n",[837,18616,18618],{"class":839,"line":18617},104,[837,18619,18620],{},"- Signal APIs only: `input()`, `input.required()`, `model()`, `output()`, `viewChild()`,\n",[837,18622,18624],{"class":839,"line":18623},105,[837,18625,18626],{},"  `viewChildren()`, `contentChild()`, `contentChildren()`. Never the decorator versions.\n",[837,18628,18630],{"class":839,"line":18629},106,[837,18631,18632],{},"- Name outputs for the event (`carrierSelected`), not the handler (`onSelect`).\n",[837,18634,18636],{"class":839,"line":18635},107,[837,18637,18638],{},"- Keep RxJS inside store rxMethods. `toSignal` needs `initialValue`. Any manual subscription\n",[837,18640,18642],{"class":839,"line":18641},108,[837,18643,18644],{},"  requires `takeUntilDestroyed()`.\n",[837,18646,18648],{"class":839,"line":18647},109,[837,18649,988],{"emptyLinePlaceholder":987},[837,18651,18653],{"class":839,"line":18652},110,[837,18654,18655],{},"### Components\n",[837,18657,18659],{"class":839,"line":18658},111,[837,18660,18661],{},"- Every component runs under `OnPush`. It is the DEFAULT in v22+ — do not write it there. On\n",[837,18663,18665],{"class":839,"line":18664},112,[837,18666,18667],{},"  v20\u002Fv21 set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly. Never use\n",[837,18669,18671],{"class":839,"line":18670},113,[837,18672,18673],{},"  `ChangeDetectionStrategy.Eager` or the deprecated `Default`.\n",[837,18675,18677],{"class":839,"line":18676},114,[837,18678,18679],{},"- NO `.subscribe()`. Only exception: dialog `afterClosed()`.\n",[837,18681,18683],{"class":839,"line":18682},115,[837,18684,18685],{},"- Small and single-responsibility. Inline templates for small components; external files use\n",[837,18687,18689],{"class":839,"line":18688},116,[837,18690,18691],{},"  paths relative to the component .ts.\n",[837,18693,18695],{"class":839,"line":18694},117,[837,18696,18697],{},"- `protected` for template-only members, `private` for internals, `readonly` on injected deps.\n",[837,18699,18701],{"class":839,"line":18700},118,[837,18702,18703],{},"- Host bindings in the `host` metadata object — not @HostBinding\u002F@HostListener.\n",[837,18705,18707],{"class":839,"line":18706},119,[837,18708,18709],{},"- Import standalone components (`MatButton`), not modules (`MatButtonModule`). No unused\n",[837,18711,18713],{"class":839,"line":18712},120,[837,18714,18715],{},"  entries in `imports`; no unused TS imports.\n",[837,18717,18719],{"class":839,"line":18718},121,[837,18720,988],{"emptyLinePlaceholder":987},[837,18722,18724],{"class":839,"line":18723},122,[837,18725,18726],{},"### Dependency injection\n",[837,18728,18730],{"class":839,"line":18729},123,[837,18731,18732],{},"- `inject()` in field initialisers. No constructor injection in new code.\n",[837,18734,18736],{"class":839,"line":18735},124,[837,18737,18738],{},"- Injection contexts are: field initialisers, constructors, factory functions, and\n",[837,18740,18742],{"class":839,"line":18741},125,[837,18743,18744],{},"  Angular-invoked functional APIs (guards, resolvers, interceptors). Outside those use\n",[837,18746,18748],{"class":839,"line":18747},126,[837,18749,18750],{},"  `runInInjectionContext`; in helpers that call inject() use `assertInInjectionContext`.\n",[837,18752,18754],{"class":839,"line":18753},127,[837,18755,18756],{},"- `providedIn: 'root'` for stateless services; component\u002Froute `providers` for stateful\n",[837,18758,18760],{"class":839,"line":18759},128,[837,18761,18762],{},"  scoped things. Remember a component provider SHADOWS a root one for its subtree.\n",[837,18764,18766],{"class":839,"line":18765},129,[837,18767,18768],{},"- `viewProviders` when projected content must not see the service.\n",[837,18770,18772],{"class":839,"line":18771},130,[837,18773,18774],{},"- `InjectionToken` only for non-class deps, competing implementations, `multi` collections,\n",[837,18776,18778],{"class":839,"line":18777},131,[837,18779,18780],{},"  or a genuine test seam. Delete single-provider\u002Fsingle-consumer\u002Fnever-overridden tokens.\n",[837,18782,18784],{"class":839,"line":18783},132,[837,18785,18786],{},"- `useExisting` aliases the same instance; `useClass` creates a new one. Don't confuse them.\n",[837,18788,18790],{"class":839,"line":18789},133,[837,18791,18792],{},"- `inject(X, { optional: true })` for optional integrations.\n",[837,18794,18796],{"class":839,"line":18795},134,[837,18797,988],{"emptyLinePlaceholder":987},[837,18799,18801],{"class":839,"line":18800},135,[837,18802,18803],{},"### Auth & security\n",[837,18805,18807],{"class":839,"line":18806},136,[837,18808,18809],{},"- NEVER store an access or refresh token in `localStorage`\u002F`sessionStorage`. `httpOnly`\n",[837,18811,18813],{"class":839,"line":18812},137,[837,18814,18815],{},"  cookie or an in-memory signal only.\n",[837,18817,18819],{"class":839,"line":18818},138,[837,18820,18821],{},"- Share ONE in-flight token-refresh Observable across concurrent 401s. Never let every\n",[837,18823,18825],{"class":839,"line":18824},139,[837,18826,18827],{},"  failed request trigger its own refresh call.\n",[837,18829,18831],{"class":839,"line":18830},140,[837,18832,18833],{},"- Guards (`canActivate`\u002F`canMatch`) and conditional rendering are UX only — every rule they\n",[837,18835,18837],{"class":839,"line":18836},141,[837,18838,18839],{},"  enforce MUST also be enforced server-side.\n",[837,18841,18843],{"class":839,"line":18842},142,[837,18844,18845],{},"- CSRF defense (`withXsrfConfiguration`) only applies to cookie-based auth.\n",[837,18847,18849],{"class":839,"line":18848},143,[837,18850,18851],{},"- Strict CSP: no `unsafe-inline`\u002F`unsafe-eval`. Nonce any inline script via `ngCspNonce`.\n",[837,18853,18855],{"class":839,"line":18854},144,[837,18856,18857],{},"- Sanitize\u002Fvalidate untrusted external data once, at the service boundary.\n",[837,18859,18861],{"class":839,"line":18860},145,[837,18862,18863],{},"- No secrets (API keys, signing keys, credentials) in the client bundle — ever.\n",[837,18865,18867],{"class":839,"line":18866},146,[837,18868,988],{"emptyLinePlaceholder":987},[837,18870,18872],{"class":839,"line":18871},147,[837,18873,18874],{},"### Observability\n",[837,18876,18878],{"class":839,"line":18877},148,[837,18879,18880],{},"- Install one global `ErrorHandler` whose own reporting call cannot throw.\n",[837,18882,18884],{"class":839,"line":18883},149,[837,18885,18886],{},"- Normalize HTTP errors once, at an interceptor, into a shared domain-error shape.\n",[837,18888,18890],{"class":839,"line":18889},150,[837,18891,18892],{},"- Every store's `tapResponse` error branch must both set UI-facing call state AND report to\n",[837,18894,18896],{"class":839,"line":18895},151,[837,18897,18898],{},"  the observability service — one without the other is an incomplete implementation.\n",[837,18900,18902],{"class":839,"line":18901},152,[837,18903,18904],{},"- Thread a correlation ID through requests; attach it to every error report.\n",[837,18906,18908],{"class":839,"line":18907},153,[837,18909,18910],{},"- Breadcrumb recent actions for context; never put PII in breadcrumbs or error metadata.\n",[837,18912,18914],{"class":839,"line":18913},154,[837,18915,18916],{},"- Report Core Web Vitals (LCP\u002FINP\u002FCLS) per route in production, not just local Lighthouse.\n",[837,18918,18920],{"class":839,"line":18919},155,[837,18921,988],{"emptyLinePlaceholder":987},[837,18923,18925],{"class":839,"line":18924},156,[837,18926,18927],{},"### Forms\n",[837,18929,18931],{"class":839,"line":18930},157,[837,18932,18933],{},"- v22+ new forms → Signal Forms (`@angular\u002Fforms\u002Fsignals`), now stable. v21 → Signal Forms were\n",[837,18935,18937],{"class":839,"line":18936},158,[837,18938,18939],{},"  experimental; prefer Reactive Forms if you need stability guarantees. v20 → Reactive Forms\n",[837,18941,18943],{"class":839,"line":18942},159,[837,18944,18945],{},"  (Signal Forms don't exist). Existing features → match what's there (typed Reactive Forms\n",[837,18947,18949],{"class":839,"line":18948},160,[837,18950,18951],{},"  preferred).\n",[837,18953,18955],{"class":839,"line":18954},161,[837,18956,18957],{},"- Signal Forms models: NEVER null\u002Fundefined initial values — '' for strings, 0 for numbers,\n",[837,18959,18961],{"class":839,"line":18960},162,[837,18962,18963],{},"  [] for arrays.\n",[837,18965,18967],{"class":839,"line":18966},163,[837,18968,18969],{},"- Never mix reactive and template-driven bindings on one control.\n",[837,18971,18973],{"class":839,"line":18972},164,[837,18974,18975],{},"- Validation is the form's job; SUBMISSION is a store rxMethod (exhaustMap).\n",[837,18977,18979],{"class":839,"line":18978},165,[837,18980,988],{"emptyLinePlaceholder":987},[837,18982,18984],{"class":839,"line":18983},166,[837,18985,18986],{},"### Templates\n",[837,18988,18990],{"class":839,"line":18989},167,[837,18991,18992],{},"- `@if` \u002F `@for` \u002F `@switch` only. `track` on every `@for` and it must be a stable identity,\n",[837,18994,18996],{"class":839,"line":18995},168,[837,18997,18998],{},"  never `$index`. Use `@empty`. Use `@default never;` for exhaustive switches.\n",[837,19000,19002],{"class":839,"line":19001},169,[837,19003,19004],{},"- Keep templates dumb — no filter\u002Fmap\u002Fsort chains, no allocating calls. Move to `computed`.\n",[837,19006,19008],{"class":839,"line":19007},170,[837,19009,19010],{},"- No object\u002Farray literals in bindings (new reference every CD cycle).\n",[837,19012,19014],{"class":839,"line":19013},171,[837,19015,19016],{},"- Don't assume globals (`new Date()`, `window`) — this also causes SSR hydration mismatches.\n",[837,19018,19020],{"class":839,"line":19019},172,[837,19021,19022],{},"  Import every pipe used.\n",[837,19024,19026],{"class":839,"line":19025},173,[837,19027,19028],{},"- `NgOptimizedImage` for static images (not base64).\n",[837,19030,19032],{"class":839,"line":19031},174,[837,19033,19034],{},"- Semantic HTML: `\u003Cbutton>` for actions, never `\u003Cdiv (click)>`.\n",[837,19036,19038],{"class":839,"line":19037},175,[837,19039,988],{"emptyLinePlaceholder":987},[837,19041,19043],{"class":839,"line":19042},176,[837,19044,19045],{},"### Styling\n",[837,19047,19049],{"class":839,"line":19048},177,[837,19050,19051],{},"- Wrap component styles in `:host`. Keep default encapsulation.\n",[837,19053,19055],{"class":839,"line":19054},178,[837,19056,19057],{},"- Components own their spacing\u002Flayout and must render correctly anywhere — never depend on an\n",[837,19059,19061],{"class":839,"line":19060},179,[837,19062,19063],{},"  ancestor's styles.\n",[837,19065,19067],{"class":839,"line":19066},180,[837,19068,19069],{},"- No `::ng-deep` (deprecated, leaks, signals a design-system bypass).\n",[837,19071,19073],{"class":839,"line":19072},181,[837,19074,19075],{},"- Design tokens for colour\u002Fspacing\u002Ftypography — never hardcoded values. New tokens need\n",[837,19077,19079],{"class":839,"line":19078},182,[837,19080,19081],{},"  light AND dark values.\n",[837,19083,19085],{"class":839,"line":19084},183,[837,19086,19087],{},"- Logical properties (`margin-inline-start`). Consistent class naming (BEM). Classes, not `id`.\n",[837,19089,19091],{"class":839,"line":19090},184,[837,19092,988],{"emptyLinePlaceholder":987},[837,19094,19096],{"class":839,"line":19095},185,[837,19097,19098],{},"### Accessibility\n",[837,19100,19102],{"class":839,"line":19101},186,[837,19103,19104],{},"- MUST pass AXE. MUST meet WCAG AA: focus management, colour contrast, ARIA.\n",[837,19106,19108],{"class":839,"line":19107},187,[837,19109,19110],{},"- Semantic elements first; ARIA only for real gaps. Keyboard reachable + visible focus.\n",[837,19112,19114],{"class":839,"line":19113},188,[837,19115,19116],{},"- Labels on all inputs; `aria-label` on icon-only buttons. Announce async state via live\n",[837,19118,19120],{"class":839,"line":19119},189,[837,19121,19122],{},"  regions. Never colour alone. Meaningful `alt`, or `alt=\"\"` if decorative.\n",[837,19124,19126],{"class":839,"line":19125},190,[837,19127,19128],{},"- Complex widgets (combobox, tree, tabs, grid): use accessible primitives, don't hand-roll ARIA.\n",[837,19130,19132],{"class":839,"line":19131},191,[837,19133,988],{"emptyLinePlaceholder":987},[837,19135,19137],{"class":839,"line":19136},192,[837,19138,19139],{},"### Typing\n",[837,19141,19143],{"class":839,"line":19142},193,[837,19144,19145],{},"- Strict mode. No `any` — prefer `unknown` and narrow. No `as` casts to silence a mismatch.\n",[837,19147,19149],{"class":839,"line":19148},194,[837,19150,19151],{},"- Prefer inference; annotate only when not obvious.\n",[837,19153,19155],{"class":839,"line":19154},195,[837,19156,19157],{},"- Treat all external data (API responses, route\u002Fquery params, localStorage, postMessage) as\n",[837,19159,19161],{"class":839,"line":19160},196,[837,19162,19163],{},"  untrusted and narrow at the boundary.\n",[837,19165,19167],{"class":839,"line":19166},197,[837,19168,19169],{},"- No `innerHTML` \u002F `bypassSecurityTrust*` without a cited, provable reason.\n",[837,19171,19173],{"class":839,"line":19172},198,[837,19174,988],{"emptyLinePlaceholder":987},[837,19176,19178],{"class":839,"line":19177},199,[837,19179,19180],{},"### Performance\n",[837,19182,19184],{"class":839,"line":19183},200,[837,19185,19186],{},"- Lazy-load features; `@defer` heavy in-page content; check the cost of new dependencies.\n",[837,19188,19190],{"class":839,"line":19189},201,[837,19191,19192],{},"- Paginate every list endpoint. No unbounded fetches that grow with customer data.\n",[837,19194,19196],{"class":839,"line":19195},202,[837,19197,19198],{},"- Keep `computed` and template expressions cheap — they re-run per CD cycle. Precompute\n",[837,19200,19202],{"class":839,"line":19201},203,[837,19203,19204],{},"  anything iterating large collections or formatting many rows.\n",[837,19206,19208],{"class":839,"line":19207},204,[837,19209,19210],{},"- No N+1 fetch patterns. No heavy synchronous work in click handlers, constructors, ngOnInit.\n",[837,19212,19214],{"class":839,"line":19213},205,[837,19215,19216],{},"- Measure before optimising further.\n",[837,19218,19220],{"class":839,"line":19219},206,[837,19221,988],{"emptyLinePlaceholder":987},[837,19223,19225],{"class":839,"line":19224},207,[837,19226,19227],{},"### Testing\n",[837,19229,19231],{"class":839,"line":19230},208,[837,19232,19233],{},"- Weight coverage at the STORE level: stub the service via DI, call the method, assert\n",[837,19235,19237],{"class":839,"line":19236},209,[837,19238,19239],{},"  signals. Assert outcomes, not implementation details.\n",[837,19241,19243],{"class":839,"line":19242},210,[837,19244,19245],{},"- Prefer DI\u002FTestBed overrides over module mocks (`vi.mock`\u002F`jest.mock` mutate the runner's\n",[837,19247,19249],{"class":839,"line":19248},211,[837,19250,19251],{},"  module cache and leak across suites).\n",[837,19253,19255],{"class":839,"line":19254},212,[837,19256,19257],{},"- Zoneless: prefer `await fixture.whenStable()` over `fixture.detectChanges()`.\n",[837,19259,19261],{"class":839,"line":19260},213,[837,19262,19263],{},"- Never mutate globals (Date, crypto, localStorage, Math.random) without restoring them.\n",[837,19265,19267],{"class":839,"line":19266},214,[837,19268,19269],{},"- No `done()` callbacks — use async\u002Fawait, `firstValueFrom`, or fake timers.\n",[837,19271,19273],{"class":839,"line":19272},215,[837,19274,19275],{},"- Don't paper over flakiness with isolation\u002Ftimeout overrides; find the shared-state leak.\n",[837,19277,19279],{"class":839,"line":19278},216,[837,19280,19281],{},"- Cover presentational components via rendered output \u002F harnesses; container components via E2E.\n",[837,19283,19285],{"class":839,"line":19284},217,[837,19286,19287],{},"- Override `FeatureFlagsService`\u002F`AuthStore` via DI to test both branches of a flag or an\n",[837,19289,19291],{"class":839,"line":19290},218,[837,19292,19293],{},"  authenticated vs. unauthenticated flow — never conditionally skip a test on real state.\n",[617,19295],{},[620,19297,19299],{"id":19298},"closing-thought",[22,19300,584],{"title":625,"href":583},[594,19302,19303,19304,19307],{},"None of these rules are clever. That's the point. A layering discipline earns its keep by\nbeing ",[603,19305,19306],{},"boring and total",": every feature has the same shape, every mutation is named, every\nasync flow cancels the same way, every dependency arrives through the same door, auth and\nerrors are handled in exactly one place each, and there is exactly one place to look for any\ngiven behaviour.",[594,19309,19310,19311,11845,19313,19316],{},"That property is worth a great deal to a human maintainer. It is worth even more to an agent,\nwhich has no institutional memory, no sense of \"how we do things here,\" and infinite\nwillingness to write a plausible ",[651,19312,692],{},[651,19314,19315],{},"localStorage.setItem('token', ...)",", or a\nhardcoded English string at 2am. Make the rules explicit, make as many of them as you can\nmachine-checkable, and the code an agent writes starts looking like the code you'd have\nwritten yourself — at whatever scale the application actually needs to run at.",[594,19318,19319,19320,1040],{},"If you'd rather have an agent load these rules on demand instead of holding the whole\ndocument in context, the plugin this article is built from is at\n",[597,19321,19322],{},[22,19323,641],{"href":641,"rel":19324},[613],[617,19326],{},[1266,19328,19330],{"id":19329},"sources",[22,19331,592],{"title":625,"href":591},[594,19333,19334,19335,19338,19339,1387,19344,1387,19349,1387,19354,1387,19359,8518,19364,19369,19370,1040],{},"Angular guidance in this document was verified against ",[22,19336,614],{"href":611,"rel":19337},[613],"\n(v22 docs) and the official Angular best-practices guide bundled with the Angular CLI MCP\nserver, including the guides on\n",[22,19340,19343],{"href":19341,"rel":19342},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fzoneless",[613],"zoneless change detection",[22,19345,19348],{"href":19346,"rel":19347},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals",[613],"signals",[22,19350,19353],{"href":19351,"rel":19352},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fdi",[613],"dependency injection",[22,19355,19358],{"href":19356,"rel":19357},"https:\u002F\u002Fangular.dev\u002Fguide\u002Ftemplates\u002Fdefer",[613],"deferrable views",[22,19360,19363],{"href":19361,"rel":19362},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fcomponents\u002Fqueries",[613],"queries",[22,19365,19368],{"href":19366,"rel":19367},"https:\u002F\u002Fangular.dev\u002Fbest-practices\u002Fperformance",[613],"performance",".\nNgRx Signals patterns follow ",[22,19371,19374],{"href":19372,"rel":19373},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals",[613],"ngrx.io",[19376,19377,19378],"style",{},"html pre.shiki code .sv8KM, html code.shiki .sv8KM{--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}html pre.shiki code .sH6be, html code.shiki .sH6be{--shiki-default:#24292E;--shiki-default-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .stz6-, html code.shiki .stz6-{--shiki-default:#6F42C1;--shiki-default-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sh09K, html code.shiki .sh09K{--shiki-default:#24292E;--shiki-dark:#FAB387}html pre.shiki code .s04sq, html code.shiki .s04sq{--shiki-default:#24292E;--shiki-dark:#9399B2}html pre.shiki code .sT6rL, html code.shiki .sT6rL{--shiki-default:#24292E;--shiki-dark:#94E2D5}html pre.shiki code .sW9l_, html code.shiki .sW9l_{--shiki-default:#032F62;--shiki-dark:#A6E3A1}html pre.shiki code .snIPh, html code.shiki .snIPh{--shiki-default:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .sQ9xd, html code.shiki .sQ9xd{--shiki-default:#6F42C1;--shiki-default-font-style:inherit;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic}html pre.shiki code .s9puj, html code.shiki .s9puj{--shiki-default:#E36209;--shiki-dark:#CDD6F4}html pre.shiki code .sPHL9, html code.shiki .sPHL9{--shiki-default:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .smRSh, html code.shiki .smRSh{--shiki-default:#24292E;--shiki-default-font-style:inherit;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic}html pre.shiki code .s8Ev2, html code.shiki .s8Ev2{--shiki-default:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .sFYxX, html code.shiki .sFYxX{--shiki-default:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .s0fpT, html code.shiki .s0fpT{--shiki-default:#005CC5;--shiki-dark:#CBA6F7}html pre.shiki code .szDnw, html code.shiki .szDnw{--shiki-default:#005CC5;--shiki-default-font-style:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-style:italic}html pre.shiki code .s02lc, html code.shiki .s02lc{--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .saQYf, html code.shiki .saQYf{--shiki-default:#005CC5;--shiki-dark:#F38BA8}html pre.shiki code .seurA, html code.shiki .seurA{--shiki-default:#24292E;--shiki-dark:#89DCEB}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sY2Jy, html code.shiki .sY2Jy{--shiki-default:#005CC5;--shiki-dark:#CDD6F4}html pre.shiki code .sXECg, html code.shiki .sXECg{--shiki-default:#22863A;--shiki-dark:#89B4FA}html pre.shiki code .sNGvK, html code.shiki .sNGvK{--shiki-default:#6F42C1;--shiki-dark:#F9E2AF}html pre.shiki code .sv8vG, html code.shiki .sv8vG{--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#CDD6F4;--shiki-dark-font-style:italic}html pre.shiki code .s5eaX, html code.shiki .s5eaX{--shiki-default:#032F62;--shiki-dark:#9399B2}html pre.shiki code .sGr-V, html code.shiki .sGr-V{--shiki-default:#D73A49;--shiki-default-font-weight:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-weight:bold}html pre.shiki code .sj8sA, html code.shiki .sj8sA{--shiki-default:#6F42C1;--shiki-dark:#CDD6F4}html pre.shiki code .sksAd, html code.shiki .sksAd{--shiki-default:#032F62;--shiki-default-font-style:inherit;--shiki-dark:#A6E3A1;--shiki-dark-font-style:italic}html pre.shiki code .sSzVO, html code.shiki .sSzVO{--shiki-default:#005CC5;--shiki-default-font-style:inherit;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic}",{"title":653,"searchDepth":894,"depth":894,"links":19380},[19381,19382,19383,19384,19396,19415,19427,19437,19447,19457,19462,19467,19468,19473,19480,19481],{"id":622,"depth":847,"text":26},{"id":758,"depth":847,"text":32},{"id":823,"depth":847,"text":38},{"id":1259,"depth":847,"text":44,"children":19385},[19386,19387,19388,19389,19390,19391,19392,19393,19394,19395],{"id":1268,"depth":894,"text":55},{"id":1621,"depth":894,"text":61},{"id":1794,"depth":894,"text":67},{"id":1882,"depth":894,"text":73},{"id":2426,"depth":894,"text":79},{"id":2809,"depth":894,"text":85},{"id":3041,"depth":894,"text":91},{"id":3193,"depth":894,"text":97},{"id":3282,"depth":894,"text":103},{"id":3535,"depth":894,"text":109},{"id":3627,"depth":847,"text":115,"children":19397},[19398,19399,19400,19411,19412],{"id":3632,"depth":894,"text":123},{"id":3678,"depth":894,"text":129},{"id":4309,"depth":894,"text":135,"children":19401},[19402,19403,19404,19405,19406,19407,19408,19409,19410],{"id":4327,"depth":917,"text":146},{"id":4664,"depth":917,"text":152},{"id":5009,"depth":917,"text":158},{"id":5662,"depth":917,"text":164},{"id":6107,"depth":917,"text":170},{"id":6827,"depth":917,"text":176},{"id":7082,"depth":917,"text":182},{"id":7245,"depth":917,"text":188},{"id":7312,"depth":917,"text":194},{"id":7548,"depth":894,"text":200},{"id":8205,"depth":894,"text":206,"children":19413},[19414],{"id":8595,"depth":917,"text":214},{"id":8732,"depth":847,"text":220,"children":19416},[19417,19418,19419,19420,19421,19422,19423,19424,19425,19426],{"id":8740,"depth":894,"text":228},{"id":9018,"depth":894,"text":234},{"id":9357,"depth":894,"text":240},{"id":9470,"depth":894,"text":246},{"id":9744,"depth":894,"text":252},{"id":10032,"depth":894,"text":258},{"id":10569,"depth":894,"text":264},{"id":10792,"depth":894,"text":270},{"id":11090,"depth":894,"text":276},{"id":11265,"depth":894,"text":282},{"id":11346,"depth":847,"text":288,"children":19428},[19429,19430,19431,19432,19433,19434,19435,19436],{"id":11357,"depth":894,"text":296},{"id":11802,"depth":894,"text":302},{"id":11920,"depth":894,"text":308},{"id":12029,"depth":894,"text":314},{"id":12226,"depth":894,"text":320},{"id":12414,"depth":894,"text":326},{"id":12586,"depth":894,"text":332},{"id":12711,"depth":894,"text":338},{"id":12773,"depth":847,"text":344,"children":19438},[19439,19440,19441,19442,19443,19444,19445,19446],{"id":12781,"depth":894,"text":352},{"id":13331,"depth":894,"text":358},{"id":13395,"depth":894,"text":364},{"id":13727,"depth":894,"text":370},{"id":13962,"depth":894,"text":376},{"id":14025,"depth":894,"text":382},{"id":14169,"depth":894,"text":388},{"id":14180,"depth":894,"text":394},{"id":14231,"depth":847,"text":400,"children":19448},[19449,19450,19451,19452,19453,19454,19455,19456],{"id":14246,"depth":894,"text":408},{"id":14526,"depth":894,"text":414},{"id":14644,"depth":894,"text":420},{"id":14818,"depth":894,"text":426},{"id":14905,"depth":894,"text":432},{"id":14928,"depth":894,"text":438},{"id":15151,"depth":894,"text":444},{"id":15163,"depth":894,"text":450},{"id":15199,"depth":847,"text":456,"children":19458},[19459,19460,19461],{"id":15280,"depth":894,"text":464},{"id":15642,"depth":894,"text":470},{"id":15682,"depth":894,"text":476},{"id":15923,"depth":847,"text":482,"children":19463},[19464,19465,19466],{"id":15928,"depth":894,"text":490},{"id":16158,"depth":894,"text":496},{"id":16221,"depth":894,"text":502},{"id":16282,"depth":847,"text":508},{"id":16421,"depth":847,"text":514,"children":19469},[19470,19471,19472],{"id":16525,"depth":894,"text":522},{"id":16834,"depth":894,"text":528},{"id":16884,"depth":894,"text":534},{"id":16964,"depth":847,"text":540,"children":19474},[19475,19476,19477,19478,19479],{"id":16972,"depth":894,"text":548},{"id":17103,"depth":894,"text":554},{"id":3485,"depth":894,"text":560},{"id":17474,"depth":894,"text":566},{"id":17833,"depth":894,"text":572},{"id":18015,"depth":847,"text":578},{"id":19298,"depth":847,"text":584,"children":19482},[19483],{"id":19329,"depth":894,"text":592},null,"2026-08-06","This guide (and Claude plugin) covers the complete Angular architecture, including signals, NgRx Signal Store, and enterprise-ready practices.","md","Header image of Angular Robot","\u002Fpost_images\u002Fangular-ngrx-architecture-for-agents\u002Fheader_image.webp",{},"article","\u002Fblog\u002Fangular-ngrx-architecture-for-agents",{"title":5,"description":19486},"blog\u002Fangular-ngrx-architecture-for-agents",[19496,548,19497],"Angular","NgRx","CTQDUQ9pu5H05DX-6wJUonFtGVBuXN5W7b6mglDB1cA",1786034188056]