446 lines
12 KiB
Plaintext
446 lines
12 KiB
Plaintext
' ===================================================================
|
|
' 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
|