314 lines
9.3 KiB
Plaintext
314 lines
9.3 KiB
Plaintext
' ===================================================================
|
|
' File: response-buffer.puml
|
|
' Purpose: Single source of truth for module-level activity + per-method sequences.
|
|
' Module: response-buffer.js — Buffer result chunks; split safely; enqueue paste ops.
|
|
' 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 (response-buffer.js) ===============================
|
|
@startuml
|
|
title response-buffer.js — Branch Flow (full module)
|
|
|
|
start
|
|
:response-buffer;
|
|
|
|
fork
|
|
' -------- chunkByLines(s, limit) --------
|
|
partition "chunkByLines(s, limit)" #E7FAE3 {
|
|
:chunkByLines;
|
|
:walk by soft breaks at \\n near limit;\nappend slice; advance;
|
|
:return chunks[];
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- isSingleFence(s) --------
|
|
partition "isSingleFence(s)" #FFF6D1 {
|
|
:isSingleFence;
|
|
:/^```[^\\n]*\\n[\\s\\S]*\\n```$/ on trim(s);
|
|
:return boolean;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- splitRespectingFence(text, limit) --------
|
|
partition "splitRespectingFence(text, limit)" #FFE1DB {
|
|
:splitRespectingFence;
|
|
:if not single fence -> chunkByLines(text, limit);\nelse split inner with adjusted limit;\nre-wrap each in same fence;
|
|
:return chunks[];
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- ResponseBuffer.constructor() --------
|
|
partition "ResponseBuffer.constructor()" #DCF9EE {
|
|
:constructor;
|
|
:pending=[]; timer=null; flushing=false;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- ResponseBuffer.push({label,content}) --------
|
|
partition "push({label,content})" #FFE6F0 {
|
|
:push;
|
|
:ignore if no content; pending.push({label,content});\n_schedule();
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- ResponseBuffer._schedule() --------
|
|
partition "_schedule()" #E6F3FF {
|
|
:_schedule;
|
|
:clearTimeout(timer);\n timer = setTimeout(() => flush(), 500);
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- ResponseBuffer._build() --------
|
|
partition "_build()" #F0E6FA {
|
|
:_build;
|
|
:showHeadings=true; concat "### label" + content blocks;\nreturn big string;
|
|
kill
|
|
}
|
|
fork again
|
|
' -------- ResponseBuffer.flush() --------
|
|
partition "flush()" #E7FAF7 {
|
|
:flush;
|
|
:guard: if flushing or empty -> return;\nflushing=true; toPaste=_build(); pending.length=0;
|
|
:if toPaste.length > limit -> splitRespectingFence(...);\n enqueue each Part i payload;\nelse enqueue whole payload;
|
|
:finally flushing=false;
|
|
kill
|
|
}
|
|
end fork
|
|
@enduml
|
|
|
|
' ==== METHOD: chunkByLines(s, limit) =======================================
|
|
@startuml
|
|
title response-buffer:chunkByLines(s, limit): \n Split on newline boundaries near the limit
|
|
|
|
participant "Caller" as CL
|
|
participant "chunkByLines(s, limit)" as CBL
|
|
|
|
activate CL
|
|
CL -> CBL : s, limit
|
|
activate CBL
|
|
|
|
CBL -> CBL : out=[]; start=0
|
|
loop while start < s.length
|
|
CBL -> CBL : soft = s.lastIndexOf('\\n', min(start+limit, s.length))
|
|
CBL -> CBL : end = (soft > start ? soft+1 : min(start+limit, s.length))
|
|
CBL -> CBL : out.push(s.slice(start,end)); start=end
|
|
end
|
|
|
|
CBL --> CL : chunks[]
|
|
deactivate CBL
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: isSingleFence(s) =============================================
|
|
@startuml
|
|
title response-buffer:isSingleFence(s): \n Detect single fenced block (```lang ... ```)
|
|
|
|
participant "Caller" as CL
|
|
participant "isSingleFence(s)" as ISF
|
|
|
|
activate CL
|
|
CL -> ISF : s
|
|
activate ISF
|
|
ISF -> ISF : test /^```[^\\n]*\\n[\\s\\S]*\\n```$/ on s.trim()
|
|
ISF --> CL : true/false
|
|
deactivate ISF
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: splitRespectingFence(text, limit) =============================
|
|
@startuml
|
|
title response-buffer:splitRespectingFence(text, limit): \n Chunk text, keeping fenced blocks intact
|
|
|
|
participant "Caller" as CL
|
|
participant "splitRespectingFence(text, limit)" as SRF
|
|
participant "isSingleFence(s)" as ISF
|
|
participant "chunkByLines(s, limit)" as CBL
|
|
|
|
activate CL
|
|
CL -> SRF : text, limit
|
|
activate SRF
|
|
|
|
SRF -> SRF : t = text.trim()
|
|
SRF -> ISF : isSingleFence(t)
|
|
ISF --> SRF : single (bool)
|
|
|
|
alt single == false
|
|
SRF -> CBL : chunkByLines(text, limit)
|
|
CBL --> SRF : chunks[]
|
|
SRF --> CL : chunks[]
|
|
else single == true
|
|
SRF -> SRF : m = /^```([^\\n]*)\\n([\\s\\S]*)\\n```$/.exec(t)\nlang=(m?.[1]||'text').trim(); inner=m?.[2]||''
|
|
SRF -> SRF : innerLimit = limit - 16 - lang.length
|
|
SRF -> CBL : chunkByLines(inner, innerLimit)
|
|
CBL --> SRF : innerChunks[]
|
|
SRF -> SRF : chunks = innerChunks.map(c => '```'+lang+'\\n'+c.replace(/\\n?$/,'\\n')+'```')
|
|
SRF --> CL : chunks
|
|
end
|
|
|
|
deactivate SRF
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: ResponseBuffer.constructor() ==================================
|
|
@startuml
|
|
title response-buffer:constructor(): \n Initialize pending buffer, timer, and state
|
|
|
|
participant "Caller" as CL
|
|
participant "constructor()" as CTOR
|
|
|
|
activate CL
|
|
CL -> CTOR : new ResponseBuffer()
|
|
activate CTOR
|
|
CTOR -> CTOR : pending=[]; timer=null; flushing=false
|
|
CTOR --> CL : instance
|
|
deactivate CTOR
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: ResponseBuffer.push({label, content}) =========================
|
|
@startuml
|
|
title response-buffer:push({label, content}): \n Collect a piece for later batched paste
|
|
|
|
participant "Caller" as CL
|
|
participant "push({label,content})" as PUSH
|
|
participant "_schedule()" as SCH
|
|
|
|
activate CL
|
|
CL -> PUSH : {label, content}
|
|
activate PUSH
|
|
|
|
alt !content
|
|
PUSH --> CL : (void)
|
|
deactivate PUSH
|
|
deactivate CL
|
|
return
|
|
end
|
|
|
|
PUSH -> PUSH : pending.push({label,content})
|
|
PUSH -> SCH : _schedule()
|
|
SCH --> PUSH : scheduled
|
|
|
|
PUSH --> CL : (void)
|
|
deactivate PUSH
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: ResponseBuffer._schedule() ====================================
|
|
@startuml
|
|
title response-buffer:_schedule(): \n Debounce flush with a short timer
|
|
|
|
participant "Caller" as CL
|
|
participant "_schedule()" as SCH
|
|
participant "Timer" as TMR
|
|
|
|
activate CL
|
|
CL -> SCH : initial request
|
|
activate SCH
|
|
|
|
SCH -> TMR : clearTimeout(timer)
|
|
TMR --> SCH : cleared
|
|
SCH -> TMR : timer = setTimeout( flush, 500 )
|
|
TMR --> SCH : id
|
|
|
|
SCH --> CL : (void)
|
|
deactivate SCH
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: ResponseBuffer._build() =======================================
|
|
@startuml
|
|
title response-buffer:_build(): \n Build a readable combined payload with headings
|
|
|
|
participant "Caller" as CL
|
|
participant "_build()" as BLD
|
|
|
|
activate CL
|
|
CL -> BLD : initial request
|
|
activate BLD
|
|
|
|
BLD -> BLD : showHeadings = true; parts=[]
|
|
loop for each {label,content} in pending
|
|
BLD -> BLD : if label -> parts.push('### '+label+'\\n')\nparts.push(String(content).trimEnd(), '')
|
|
end
|
|
BLD --> CL : parts.join('\\n')
|
|
deactivate BLD
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== METHOD: ResponseBuffer.flush() ========================================
|
|
@startuml
|
|
title response-buffer:flush(): \n Debounced paste: split if huge, enqueue parts on ExecutionQueue
|
|
|
|
participant "Caller" as CL
|
|
participant "flush()" as FLS
|
|
participant "_build()" as BLD
|
|
participant "splitRespectingFence(text, limit)" as SRF
|
|
participant "Queue" as QUE
|
|
participant "Paste.submitToComposer(text)" as PST
|
|
|
|
activate CL
|
|
CL -> FLS : initial request
|
|
activate FLS
|
|
|
|
' guard + state
|
|
FLS -> FLS : if (flushing || pending.length==0) return
|
|
FLS -> FLS : flushing = true
|
|
FLS -> BLD : _build()
|
|
BLD --> FLS : toPaste
|
|
FLS -> FLS : pending.length = 0
|
|
|
|
' branch by length limit
|
|
FLS -> FLS : limit = 250_000
|
|
alt toPaste.length > limit
|
|
FLS -> SRF : splitRespectingFence(toPaste, limit)
|
|
SRF --> FLS : chunks[]
|
|
loop for each c, i
|
|
FLS -> FLS : header = `### Part ${i+1}/${chunks.length}\\n`; payload = header + c
|
|
FLS -> QUE : push(async () => PST.submitToComposer(payload))
|
|
QUE --> FLS : queued
|
|
end
|
|
else within limit
|
|
FLS -> QUE : push(async () => PST.submitToComposer(toPaste))
|
|
QUE --> FLS : queued
|
|
end
|
|
|
|
FLS --> CL : (finally flushing=false)
|
|
deactivate FLS
|
|
deactivate CL
|
|
@enduml
|
|
|
|
' ==== LEGEND ===============================================================
|
|
@startuml
|
|
legend bottom
|
|
== response-buffer 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/Per-method Sequence diagrams for each exported or significant internal function.
|
|
|
|
• Sequence conventions:
|
|
1) First participant is the external caller (use "Caller" or a concrete origin).
|
|
2) Do NOT add a module/class lifeline; the name appears in the title only.
|
|
3) Include every directly-called method or subsystem as a participant
|
|
(e.g., "chunkByLines()", "isSingleFence()", "splitRespectingFence()", "push()", "_schedule()", "_build()", "flush()", "Queue", "Paste").
|
|
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., 250_000 limit, fence shape, headings format).
|
|
|
|
• Color palette (soft pastels)
|
|
• Use --> for returns; -> for calls.
|
|
• Participants use quoted method names for internals (e.g., "flush()"), and plain nouns for systems ("Queue", "Paste", "Timer").
|
|
• Keep this legend at the end of the file to standardize edits.
|
|
endlegend
|
|
@enduml
|