' =================================================================== ' 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