419 lines
11 KiB
Plaintext
419 lines
11 KiB
Plaintext
' ===================================================================
|
|
' File: storage.puml
|
|
' Purpose: Single source of truth for module-level activity + per-method sequences.
|
|
' Module: storage.js — Conversation-aware de-duplication via localStorage.
|
|
' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
|
|
' ===================================================================
|
|
|
|
' Neutral defaults — typography/layout only (keeps partition colors intact)
|
|
skinparam Shadowing false
|
|
skinparam SequenceMessageAlign center
|
|
skinparam SequenceLifeLineBorderColor #666666
|
|
skinparam SequenceLifeLineBorderThickness 1
|
|
|
|
' ==== VIEW: Branch Flow (storage.js) =======================================
|
|
@startuml
|
|
title storage.js — Branch Flow (full module)
|
|
|
|
start
|
|
:storage;
|
|
|
|
fork
|
|
' -------- ConversationHistory.constructor() --------
|
|
partition "ConversationHistory.constructor()" #E7FAE3 {
|
|
:constructor;
|
|
:conversationId = _getConversationId();\nkey = `ai_rc:conv:${conversationId}:processed`;
|
|
:cache = _load(); _cleanupExpired();
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _getConversationId() --------
|
|
partition "_getConversationId()" #FFF6D1 {
|
|
:_getConversationId;
|
|
:host = location.hostname.replace('chat.openai.com','chatgpt.com');\nreturn `${host}:${location.pathname||'/'}`;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _load() / _save() --------
|
|
partition "_load() / _save()" #FFE1DB {
|
|
:_load -> JSON.parse(localStorage.getItem(key)||'{}')\n(catch -> {});
|
|
:_save -> localStorage.setItem(key, JSON.stringify(cache))\n(catch -> logger.warn(...));
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- isProcessed(el, idx=0) --------
|
|
partition "isProcessed(el, idx=0)" #DCF9EE {
|
|
:isProcessed;
|
|
:fp = _fingerprint(el, idx);\nreturn cache.hasOwnProperty(fp);
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- markProcessed(el, idx=0) --------
|
|
partition "markProcessed(el, idx=0)" #FFE6F0 {
|
|
:markProcessed;
|
|
:fp = _fingerprint(el, idx);\ncache[fp] = Date.now(); _save();\nif cfg.ui.showExecutedMarker -> _mark(el);
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _fingerprint(el, idx) --------
|
|
partition "_fingerprint(el, idx)" #E6F3FF {
|
|
:_fingerprint;
|
|
:base = (window.AI_REPO_STABLE_FINGERPRINT? stable(el)\n : window.AI_REPO_FINGERPRINT? compat(el)\n : _hash((el.textContent||'').slice(0,1000)));
|
|
:return `${base}|idx:${idx}`;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _hash(str) --------
|
|
partition "_hash(str)" #F0E6FA {
|
|
:_hash;
|
|
:djb2-xor over up to 1000 chars;\nreturn unsigned base36;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _mark(el) --------
|
|
partition "_mark(el)" #E7FAF7 {
|
|
:_mark;
|
|
:el.style.borderLeft = '3px solid #10B981' (try/catch);
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- _cleanupExpired() --------
|
|
partition "_cleanupExpired()" #FFF2E7 {
|
|
:_cleanupExpired;
|
|
:ttl = 30d; now = Date.now();\nfor (k,ts in cache) if !ts || now - ts > ttl -> delete;\nif dirty -> _save();
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- clear() --------
|
|
partition "clear()" #E7F7FF {
|
|
:clear;
|
|
:cache = {}; _save();
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- bootstrap (global) --------
|
|
partition "bootstrap (global)" #FFF7E7 {
|
|
:window.AI_REPO_HISTORY = new ConversationHistory();
|
|
kill
|
|
}
|
|
end fork
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: ConversationHistory.constructor() ============================
|
|
@startuml
|
|
title storage:constructor(): \n Build per-conversation key, load cache, prune expired
|
|
|
|
participant "Caller" as CL
|
|
participant "constructor()" as CTOR
|
|
participant "_getConversationId()" as GID
|
|
participant "_load()" as LOAD
|
|
participant "_cleanupExpired()" as CLEAN
|
|
|
|
activate CL
|
|
CL -> CTOR : new ConversationHistory()
|
|
activate CTOR
|
|
|
|
CTOR -> GID : _getConversationId()
|
|
GID --> CTOR : conversationId
|
|
CTOR -> CTOR : key = `ai_rc:conv:${conversationId}:processed`
|
|
|
|
CTOR -> LOAD : _load()
|
|
LOAD --> CTOR : cache object
|
|
|
|
CTOR -> CLEAN : _cleanupExpired()
|
|
CLEAN --> CTOR : done
|
|
|
|
CTOR --> CL : instance
|
|
deactivate CTOR
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _getConversationId() ========================================
|
|
@startuml
|
|
title storage:_getConversationId(): \n host+path key (normalize chat.openai.com → chatgpt.com)
|
|
|
|
participant "Caller" as CL
|
|
participant "_getConversationId()" as GID
|
|
participant "Location" as LOC
|
|
|
|
activate CL
|
|
CL -> GID : initial request
|
|
activate GID
|
|
GID -> LOC : read location.hostname, location.pathname
|
|
LOC --> GID : hostname, pathname
|
|
GID -> GID : host = hostname.replace('chat.openai.com','chatgpt.com')
|
|
GID --> CL : `${host}:${pathname || '/'}`
|
|
deactivate GID
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _load() / _save() ===========================================
|
|
@startuml
|
|
title storage:_load() / _save(): \n Safely read/write JSON cache in localStorage
|
|
|
|
participant "Caller" as CL
|
|
participant "_load()" as LOAD
|
|
participant "_save()" as SAVE
|
|
participant "localStorage" as LS
|
|
participant "Logger" as LOG
|
|
|
|
' _load()
|
|
activate CL
|
|
CL -> LOAD : initial request
|
|
activate LOAD
|
|
LOAD -> LS : getItem(key)
|
|
LS --> LOAD : json | null
|
|
LOAD -> LOAD : JSON.parse(json||'{}') catch -> {}
|
|
LOAD --> CL : cache object
|
|
deactivate LOAD
|
|
|
|
' _save()
|
|
CL -> SAVE : cache
|
|
activate SAVE
|
|
SAVE -> LS : setItem(key, JSON.stringify(cache))
|
|
alt success
|
|
LS --> SAVE : ok
|
|
SAVE --> CL : done
|
|
else error
|
|
LS --> SAVE : throw e
|
|
SAVE -> LOG : warn('Failed to save history cache', {error:e.message})
|
|
LOG --> SAVE : logged
|
|
SAVE --> CL : done
|
|
end
|
|
deactivate SAVE
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: isProcessed(el, idx=0) ======================================
|
|
@startuml
|
|
title storage:isProcessed(el, idx): \n Check cache using fingerprint+index key
|
|
|
|
participant "Caller" as CL
|
|
participant "isProcessed(el,idx)" as ISP
|
|
participant "_fingerprint(el,idx)" as FP
|
|
|
|
activate CL
|
|
CL -> ISP : el, idx
|
|
activate ISP
|
|
ISP -> FP : _fingerprint(el, idx)
|
|
FP --> ISP : fp
|
|
ISP -> ISP : return Object.prototype.hasOwnProperty.call(cache, fp)
|
|
ISP --> CL : true/false
|
|
deactivate ISP
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: markProcessed(el, idx=0) =====================================
|
|
@startuml
|
|
title storage:markProcessed(el, idx): \n Record timestamp, persist, optionally mark DOM
|
|
|
|
participant "Caller" as CL
|
|
participant "markProcessed(el,idx)" as MK
|
|
participant "_fingerprint(el,idx)" as FP
|
|
participant "_save()" as SAVE
|
|
participant "_mark(el)" as MRK
|
|
participant "Config" as CFG
|
|
|
|
activate CL
|
|
CL -> MK : el, idx
|
|
activate MK
|
|
|
|
MK -> FP : _fingerprint(el, idx)
|
|
FP --> MK : fp
|
|
MK -> MK : cache[fp] = Date.now()
|
|
MK -> SAVE : _save()
|
|
SAVE --> MK : ok
|
|
|
|
MK -> CFG : get('ui.showExecutedMarker')
|
|
CFG --> MK : bool
|
|
alt true
|
|
MK -> MRK : _mark(el)
|
|
MRK --> MK : done
|
|
end
|
|
|
|
MK --> CL : (void)
|
|
deactivate MK
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _fingerprint(el, idx) ========================================
|
|
@startuml
|
|
title storage:_fingerprint(el, idx): \n Use stable FP if available; else compat; else inline hash
|
|
|
|
participant "Caller" as CL
|
|
participant "_fingerprint(el,idx)" as FP
|
|
participant "AI_REPO_STABLE_FINGERPRINT" as STAB
|
|
participant "AI_REPO_FINGERPRINT" as COMP
|
|
participant "_hash(str)" as HSH
|
|
|
|
activate CL
|
|
CL -> FP : el, idx
|
|
activate FP
|
|
|
|
FP -> STAB : exists?
|
|
STAB --> FP : yes/no
|
|
alt stable available
|
|
FP -> STAB : STABLE_FINGERPRINT(el)
|
|
STAB --> FP : base
|
|
else no stable
|
|
FP -> COMP : exists?
|
|
COMP --> FP : yes/no
|
|
alt compat available
|
|
FP -> COMP : FINGERPRINT(el)
|
|
COMP --> FP : base
|
|
else none
|
|
FP -> FP : text = (el.textContent||'').slice(0,1000)
|
|
FP -> HSH : _hash(text)
|
|
HSH --> FP : base
|
|
end
|
|
end
|
|
|
|
FP --> CL : `${base}|idx:${idx}`
|
|
deactivate FP
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _hash(str) ===================================================
|
|
@startuml
|
|
title storage:_hash(str): \n djb2-xor over up to 1000 chars → base36
|
|
|
|
participant "Caller" as CL
|
|
participant "_hash(str)" as HSH
|
|
|
|
activate CL
|
|
CL -> HSH : str
|
|
activate HSH
|
|
HSH -> HSH : h=5381; for i<min(len,1000): h=((h<<5)+h)^charCode
|
|
HSH --> CL : (h>>>0).toString(36)
|
|
deactivate HSH
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _mark(el) ====================================================
|
|
@startuml
|
|
title storage:_mark(el): \n Visual left border to indicate executed command
|
|
|
|
participant "Caller" as CL
|
|
participant "_mark(el)" as MRK
|
|
participant "DOM" as DOM
|
|
|
|
activate CL
|
|
CL -> MRK : el
|
|
activate MRK
|
|
MRK -> DOM : el.style.borderLeft = '3px solid #10B981' (try/catch)
|
|
DOM --> MRK : ok | ignored
|
|
MRK --> CL : (void)
|
|
deactivate MRK
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: _cleanupExpired() ============================================
|
|
@startuml
|
|
title storage:_cleanupExpired(): \n TTL pruning (30 days) + conditional save
|
|
|
|
participant "Caller" as CL
|
|
participant "_cleanupExpired()" as CLN
|
|
participant "_save()" as SAVE
|
|
|
|
activate CL
|
|
CL -> CLN : initial request
|
|
activate CLN
|
|
|
|
CLN -> CLN : ttl=30*24*60*60*1000; now=Date.now(); dirty=false
|
|
loop for each [k, ts] in Object.entries(cache)
|
|
alt !ts or now - ts > ttl
|
|
CLN -> CLN : delete cache[k]; dirty=true
|
|
end
|
|
end
|
|
|
|
alt dirty
|
|
CLN -> SAVE : _save()
|
|
SAVE --> CLN : ok
|
|
end
|
|
|
|
CLN --> CL : (void)
|
|
deactivate CLN
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: clear() ======================================================
|
|
@startuml
|
|
title storage:clear(): \n Reset cache and persist
|
|
|
|
participant "Caller" as CL
|
|
participant "clear()" as CLR
|
|
participant "_save()" as SAVE
|
|
|
|
activate CL
|
|
CL -> CLR : initial request
|
|
activate CLR
|
|
CLR -> CLR : cache = {}
|
|
CLR -> SAVE : _save()
|
|
SAVE --> CLR : ok
|
|
CLR --> CL : (void)
|
|
deactivate CLR
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== METHOD: bootstrap (global) ===========================================
|
|
@startuml
|
|
title storage:bootstrap: \n Expose ConversationHistory singleton on window
|
|
|
|
participant "Caller" as CL
|
|
participant "bootstrap" as BOOT
|
|
participant "ConversationHistory()" as CTOR
|
|
participant "window" as WIN
|
|
|
|
activate CL
|
|
CL -> BOOT : module load
|
|
activate BOOT
|
|
BOOT -> CTOR : new ConversationHistory()
|
|
CTOR --> BOOT : instance
|
|
BOOT -> WIN : window.AI_REPO_HISTORY = instance
|
|
WIN --> BOOT : ok
|
|
BOOT --> CL : (void)
|
|
deactivate BOOT
|
|
deactivate CL
|
|
@enduml
|
|
|
|
|
|
' ==== LEGEND ===============================================================
|
|
@startuml
|
|
legend bottom
|
|
== storage UML Style Guide (for future edits) ==
|
|
• Scope: One .puml per module. Keep two views:
|
|
(1) Activity "Branch Flow" for the whole module (partitions + soft colors),
|
|
(2) Per-function Sequence diagrams for each exported or significant helper.
|
|
|
|
• Sequence conventions:
|
|
1) First participant is the external caller (use "Caller" or a concrete origin).
|
|
2) Do NOT add a class lifeline; the module/class name appears in the title only.
|
|
3) Include every directly-called helper/system as a participant
|
|
(e.g., "_fingerprint()", "_hash()", "_mark()", "Config", "Logger", "localStorage", "Location").
|
|
4) Prefer simple messages; Use --> for returns; -> for calls.
|
|
5) Use activate/deactivate as you see fit for clarity.
|
|
6) Use alt blocks only when branches meaningfully change the message flow.
|
|
|
|
• Activity view conventions:
|
|
A) Start with module node then fork partitions for each function/method.
|
|
B) One partition per function; soft background color; terminate branches with 'kill'.
|
|
C) Keep wording aligned with code (e.g., 30-day TTL, base36 hashes, idx suffix, key prefix `ai_rc:conv:`).
|
|
|
|
• Color palette (soft pastels)
|
|
• Use --> for returns; -> for calls.
|
|
• Participants use quoted method names for internals; plain nouns for systems ("localStorage", "Location", "window").
|
|
• Keep this legend at the end of the file to standardize edits.
|
|
endlegend
|
|
@enduml
|