' =================================================================== ' File: queue.puml ' Purpose: Single source of truth for module-level activity + per-method sequences. ' Module: queue.js — Rate-limited FIFO queue (min delay, max per minute), async drain. ' 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 (queue.js) ========================================= @startuml title queue.js — Branch Flow (full module) start :queue; fork ' -------- constructor(opts) -------- partition "constructor(opts)" #E7FAE3 { :constructor; :minDelayMs = opts.minDelayMs ?? cfg.queue.minDelayMs ?? 1500; :maxPerMinute = opts.maxPerMinute ?? cfg.queue.maxPerMinute ?? 15; :q=[]; running=false; timestamps=[]; onSizeChange=null; kill } fork again ' -------- push(task) -------- partition "push(task)" #FFF6D1 { :push; :q.push(task); onSizeChange?.(q.length); :if !running -> _drain(); kill } fork again ' -------- clear() -------- partition "clear()" #FFE1DB { :clear; :q.length = 0; onSizeChange?.(0); kill } fork again ' -------- size() -------- partition "size()" #DCF9EE { :size; :return q.length; kill } fork again ' -------- _withinBudget() -------- partition "_withinBudget()" #FFE6F0 { :_withinBudget; :timestamps = timestamps.filter(now - t < 60000); :return timestamps.length < maxPerMinute; kill } fork again ' -------- _drain() -------- partition "_drain()" #E6F3FF { :_drain; :guard running; set running=true; :while q.length > 0; :while !_withinBudget -> _delay(400); :fn = q.shift(); onSizeChange?.(q.length); :try await fn(); catch -> log.warn("Queue task error"); :timestamps.push(Date.now()); :await _delay(minDelayMs); :end while; running=false; kill } fork again ' -------- _delay(ms) -------- partition "_delay(ms)" #F0E6FA { :_delay; :return new Promise(resolve after setTimeout(ms)); kill } end fork @enduml ' ==== METHOD: constructor(opts) ============================================ @startuml title queue:constructor(opts): \n Initialize rate limits, internal state, and callbacks participant "Caller" as CL participant "constructor(opts)" as CTOR participant "Config" as CFG activate CL CL -> CTOR : new ExecutionQueue(opts) activate CTOR CTOR -> CFG : get('queue.minDelayMs') / get('queue.maxPerMinute') CFG --> CTOR : minCfg / maxCfg CTOR -> CTOR : minDelayMs = opts.minDelayMs ?? minCfg ?? 1500 CTOR -> CTOR : maxPerMinute = opts.maxPerMinute ?? maxCfg ?? 15 CTOR -> CTOR : q=[]; running=false; timestamps=[]; onSizeChange=null CTOR --> CL : instance deactivate CTOR deactivate CL @enduml ' ==== METHOD: push(task) ==================================================== @startuml title queue:push(task): \n Enqueue a task and start the drain loop if idle participant "Caller" as CL participant "push(task)" as PUSH participant "_drain()" as DRN activate CL CL -> PUSH : task (async function) activate PUSH PUSH -> PUSH : q.push(task) PUSH -> PUSH : onSizeChange?.(q.length) PUSH -> PUSH : if (!running) -> start drain PUSH -> DRN : _drain() DRN --> PUSH : (started | already running) PUSH --> CL : (void) deactivate PUSH deactivate CL @enduml ' ==== METHOD: clear() ======================================================= @startuml title queue:clear(): \n Drop all pending tasks and notify size change participant "Caller" as CL participant "clear()" as CLR activate CL CL -> CLR : initial request activate CLR CLR -> CLR : q.length = 0 CLR -> CLR : onSizeChange?.(0) CLR --> CL : (void) deactivate CLR deactivate CL @enduml ' ==== METHOD: size() ======================================================== @startuml title queue:size(): \n Return current queue length participant "Caller" as CL participant "size()" as SIZE activate CL CL -> SIZE : initial request activate SIZE SIZE --> CL : q.length deactivate SIZE deactivate CL @enduml ' ==== METHOD: _withinBudget() ============================================== @startuml title queue:_withinBudget(): \n Enforce rolling 60s window and max tasks per minute participant "Caller" as CL participant "_withinBudget()" as WBG activate CL CL -> WBG : initial request activate WBG WBG -> WBG : now = Date.now() WBG -> WBG : timestamps = timestamps.filter(now - t < 60000) WBG --> CL : (timestamps.length < maxPerMinute) deactivate WBG deactivate CL @enduml ' ==== METHOD: _drain() ====================================================== @startuml title queue:_drain(): \n Process tasks while respecting rate limits and min spacing participant "Caller" as CL participant "_drain()" as DRN participant "_withinBudget()" as WBG participant "_delay(ms)" as DLY participant "Logger" as LOG activate CL CL -> DRN : initial request activate DRN ' guard DRN -> DRN : if (running) return DRN -> DRN : running = true loop while q.length > 0 ' wait for budget DRN -> WBG : _withinBudget() WBG --> DRN : ok (bool) alt !ok DRN -> DLY : _delay(400) DLY --> DRN : wake DRN -> WBG : _withinBudget() WBG --> DRN : ok (bool) end ' get next task DRN -> DRN : fn = q.shift() DRN -> DRN : onSizeChange?.(q.length) ' execute task safely alt try/await fn() DRN -> DRN : await fn() else error DRN -> LOG : warn("Queue task error", {error}) end ' record + spacing DRN -> DRN : timestamps.push(Date.now()) DRN -> DLY : _delay(minDelayMs) DLY --> DRN : wake end DRN -> DRN : running = false DRN --> CL : (void) deactivate DRN deactivate CL @enduml ' ==== METHOD: _delay(ms) ==================================================== @startuml title queue:_delay(ms): \n Resolve after a timeout participant "Caller" as CL participant "_delay(ms)" as DLY participant "Timer" as TMR activate CL CL -> DLY : ms activate DLY DLY -> TMR : setTimeout(ms) TMR --> DLY : wake DLY --> CL : (void) deactivate DLY deactivate CL @enduml ' ==== LEGEND =============================================================== @startuml legend bottom == queue 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 internal method. • Sequence conventions: 1) First participant is the external caller (use "Caller" or a concrete origin). 2) Do NOT add a module lifeline; the module name appears in the title only. 3) Include every directly-called method or subsystem as a participant (e.g., "push()", "clear()", "size()", "_withinBudget()", "_drain()", "_delay()", "Logger", "Config", "Timer"). 4) Prefer simple messages; Use --> for returns; -> for calls. 5) Use activate/deactivate as you see fit for clarity (no strict rule). 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., 60_000s window, minDelayMs, maxPerMinute). • Color palette (soft pastels) • Use --> for returns; -> for calls. • Participants use quoted method names for internals (e.g., "_drain()"), and plain nouns for systems ("Logger", "Timer"). • Keep this legend at the end of the file to standardize edits. endlegend @enduml