' =================================================================== ' File: ConfigManager.puml ' Purpose: Single source of truth for class-level activity and per-method sequences. ' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. ' =================================================================== ' (Optional) neutral defaults — typography/layout only (keeps your colors intact) skinparam Shadowing false skinparam SequenceMessageAlign center skinparam SequenceLifeLineBorderColor #666666 skinparam SequenceLifeLineBorderThickness 1 ' ==== VIEW: Branch Flow (full class) ========================================== @startuml title ConfigManager — Branch Flow (full class) start :ConfigManager; ' Fan-out to each method fork ' -------- constructor() -------- partition "constructor()" #E7FAE3 { :constructor(); :this.config = load(); kill } fork again ' -------- load() -------- partition "load()" #FFF6D1 { :load(); :raw = localStorage.getItem(STORAGE_KEYS.cfg); if (raw is null/empty?) then (yes) :config = deepClone(DEFAULT_CONFIG); kill else (no) :try parse = JSON.parse(raw); if (parse ok?) then (yes) :saved = parse; :config = mergeConfigs(DEFAULT_CONFIG, saved); kill else (no / parse error) :config = deepClone(DEFAULT_CONFIG); kill endif endif } fork again ' -------- save() -------- partition "save()" #FFE1DB { :save(); :persistable = deepClone(config); if (persistable has runtime?) then (yes) :delete persistable.runtime; endif :try json = JSON.stringify(persistable); if (stringify ok?) then (yes) :localStorage.setItem(STORAGE_KEYS.cfg, json); kill else (no / stringify error) :/* log/notify failure */; kill endif } fork again ' -------- get(keyPath) -------- partition "get(keyPath)" #DCF9EE { :get(keyPath); :parts = keyPath.split('.'); :node = config; while (more parts?) :p = next part; if (node[p] exists?) then (yes) :node = node[p]; else (no) :return undefined; kill endif endwhile :return node; kill } fork again ' -------- set(keyPath, value) -------- partition "set(keyPath, value)" #FFE6F0 { :set(keyPath, value); :parts = keyPath.split('.'); :node = config; while (more parts?) :p = next part; if (node[p] exists?) then (yes) :node = node[p]; else (no) :node[p] = {}; :node = node[p]; endif endwhile :assign value at final key; :save(); kill } fork again ' -------- mergeConfigs(defaults, saved) -------- partition "mergeConfigs(defaults, saved)" #E6F3FF { :mergeConfigs(defaults, saved); :result = deepClone(defaults); if (saved is object?) then (yes) :for each key k in saved; while (keys left?) :k = next key; if (k == "runtime"?) then (yes) :skip; else (no) if (both result[k] and saved[k] are plain objects?) then (yes) :result[k] = mergeConfigs(result[k], saved[k]); else (no) :result[k] = deepClone(saved[k]); endif endif endwhile else (no) :/* nothing to merge */; endif :return result; kill } fork again ' -------- deepClone(o) -------- partition "deepClone(o)" #F0E6FA { :deepClone(o); if (o is null or primitive?) then (yes) :return o; kill else (no) if (o is Array?) then (yes) :clone = []; :for each item -> push( deepClone(item) ); :return clone; kill else (no) :clone = {}; :for each key -> clone[key] = deepClone(o[key]); :return clone; kill endif endif } end fork @enduml ' ==== METHOD: constructor() ================================================ @startuml title ConfigManager:constructor(): \n Populate this.config at instantiation actor Page as PG participant "constructor()" as CTOR participant "load()" as LD PG -> CTOR : new ConfigManager() activate CTOR CTOR -> LD : populate this.config LD --> CTOR : config object CTOR --> PG : this.config set deactivate CTOR @enduml ' ==== METHOD: load() ======================================================= @startuml title ConfigManager:load(): \n Read from localStorage, parse+merge or fallback to defaults participant "Caller" as CL participant "load()" as LD participant "localStorage" as LS participant "mergeConfigs()" as MC participant "deepClone()" as DC activate CL activate LD CL -> LD : initial request activate LS LD -> LS : getItem(STORAGE_KEYS.cfg) LS --> LD : getItem(STORAGE_KEYS.cfg) deactivate LS alt STORAGE_KEYS.cfg (Empty) activate DC LD -> DC : deepClone(DEFAULT_CONFIG) LD <-- DC : defaults clone deactivate DC LD --> CL : return defaults else STORAGE_KEYS.cfg (Not empty) LD --> LD : try parse STORAGE_KEYS.cfg alt parse ok LD --> LD : saved = parsed activate MC LD -> MC : mergeConfigs(DEFAULT_CONFIG, saved) MC --> LD : merged config deactivate MC LD --> CL : return merged config else parse error activate DC LD -> DC : deepClone(DEFAULT_CONFIG) LD <-- DC : defaults clone deactivate DC LD --> CL : return defaults end end @enduml ' ==== METHOD: save() ======================================================= @startuml title ConfigManager:save(): \n Strip runtime, stringify, persist to localStorage participant "Caller" as CL participant "save()" as SV participant "deepClone()" as DC participant "JSON" as JS participant "localStorage" as LS participant "Console" as CLG activate CL CL -> SV : initial request deactivate CL activate SV activate DC SV -> DC : deepClone(config) DC --> SV : persistable clone deactivate DC SV -> SV : delete any persistable.runtime activate JS SV -> JS : JSON.stringify(persistable) alt stringify ok JS --> SV : json string activate LS SV -> LS : setItem(STORAGE_KEYS.cfg, json) deactivate LS else stringify error JS --> SV : error activate CLG SV -> CLG : log/notify failure deactivate CLG end deactivate JS @enduml ' ==== METHOD: get(keyPath) ================================================ @startuml title ConfigManager:get(keyPath): \n Resolve a dotted path or return undefined participant "Caller" as CL participant "get(keyPath)" as GET activate CL CL -> GET : initial request (keyPath) activate GET GET -> GET : parts = keyPath.split('.'); node = config loop for each part alt node has part GET -> GET : node = node[part] else missing segment GET --> CL : undefined end end GET --> CL : value (final node) deactivate GET deactivate CL @enduml ' ==== METHOD: set(keyPath, value) ========================================= @startuml title ConfigManager:set(keyPath, value): \n Create missing path segments, assign, then persist participant "Caller" as CL participant "set(keyPath, value)" as SET participant "save()" as SV activate CL CL -> SET : initial request (keyPath, value) activate SET SET -> SET : parts = keyPath.split('.'); node = config loop for each part (except last) alt node has part SET -> SET : node = node[part] else missing SET -> SET : node[part] = {}; node = node[part] end end SET -> SET : assign value at final key SET -> SV : save() SV --> SET : persisted SET --> CL : done deactivate SET deactivate CL @enduml ' ==== METHOD: mergeConfigs(defaults, saved) ================================ @startuml title ConfigManager:mergeConfigs(defaults, saved): \n Deep merge saved over defaults (skip runtime) participant "Caller" as CL participant "mergeConfigs()" as MC participant "deepClone()" as DC activate CL CL -> MC : initial request (defaults, saved) activate MC MC -> DC : deepClone(defaults) DC --> MC : result (clone of defaults) alt saved is plain object loop for each key k in saved alt k == "runtime" MC -> MC : skip key else not runtime alt both result[k] and saved[k] are plain objects MC -> MC : result[k] = mergeConfigs(result[k], saved[k]) ' recursive else overwrite MC -> DC : deepClone(saved[k]) DC --> MC : cloned value MC -> MC : result[k] = cloned value end end end else saved not object MC -> MC : nothing to merge end MC --> CL : result deactivate MC deactivate CL @enduml ' ==== METHOD: deepClone(o) ================================================ @startuml title ConfigManager:deepClone(o): \n Structural clone for arrays/objects; primitives by value participant "Caller" as CL participant "deepClone()" as DC activate CL CL -> DC : initial request (o) activate DC alt o is null or primitive DC --> CL : o deactivate DC deactivate CL return else non-primitive alt Array DC -> DC : clone = [] loop each item DC -> DC : clone.push( deepClone(item) ) end DC --> CL : clone else Object DC -> DC : clone = {} loop each key DC -> DC : clone[key] = deepClone(o[key]) end DC --> CL : clone end end deactivate DC deactivate CL @enduml ' ==== LEGEND =============================================================== @startuml legend bottom == Config UML Style Guide (for future edits) == • Scope: One .puml per class or file. Keep two views: (1) Activity "Branch Flow" for all methods for the class or functions in the file. - (partitions + soft colors), (2) Per-method Sequence diagrams for each of the methods or functions. • Sequence conventions: 1) First participant is the external caller (use "Caller" or "Page"). 2) Do NOT add the class lifeline unless needed (constructor). Class name appears in title only. 3) Include every directly-called method or subsystem as a participant (e.g., "load()", "mergeConfigs()", "deepClone()", "JSON", "localStorage", "Console"). 4) Prefer simple messages. 5) Use activate/deactivate for the method under focus and key collaborators. 6) Use alt blocks only when branches meaningfully change the message flow. For trivial checks (e.g., delete runtime if exists), inline the action. 7) Titles: "ClassName:method(): \n Detailed description of the flow". • Activity view conventions: A) Start with Class(or filename) node then fork partitions for each method or function. B) One partition per method; soft background color; terminate branches with 'kill'. C) Keep wording aligned with code (e.g., "deepClone(DEFAULT_CONFIG)", "mergeConfigs(...)"). • Color palette (soft pastels) • Use --> for returns; -> for calls. • Participants use quoted method names for internals (e.g., "save()"), and plain nouns for systems ("JSON", "localStorage", "Console"). • When modifying this file keep this legend at the end of the file to standardize edits. UML_Example ------------------------------------------ title ClassName:methodName(args): \n Detailed description of what this method does participant "Caller" as CL participant "methodName()" as M ' the method under focus ' Add collaborators as needed: ' participant "SomeDependency" as DEP ' participant "AnotherMethod()" as AM ' participant "JSON" as JS ' participant "localStorage" as LS activate CL CL -> M : initial request (args) activate M ' -- inner flow (keep alt blocks only if they clarify) -- ' Example pattern: activate LS ' M -> LS : getItem(KEY) ' LS --> M : value deactivate LS ' alt branch condition activate AM ' M -> AM : call anotherMethod(...) ' AM --> M : result deactivate AM ' else other branch activate DEP ' M -> DEP : do something ' DEP -> M : ok deactivate DEP ' end ' Return to caller M -> CL : return value deactivate M deactivate CL --------------------------------------------- endlegend @enduml