281 lines
7.6 KiB
Plaintext
281 lines
7.6 KiB
Plaintext
' ===================================================================
|
|
' 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
|