2025-11-25

This commit is contained in:
2025-11-25 21:37:46 -05:00
parent 78b0203064
commit acbe4f639c
23 changed files with 4592 additions and 14 deletions

5
.obsidian/app.json vendored
View File

@@ -1 +1,4 @@
{} {
"showLineNumber": false,
"readableLineLength": false
}

View File

@@ -1 +1,6 @@
{} {
"baseFontSize": 12,
"translucency": true,
"accentColor": "#95b1ea",
"baseFontSizeAction": true
}

1
.obsidian/community-plugins.json vendored Normal file
View File

@@ -0,0 +1 @@
[]

View File

@@ -14,7 +14,7 @@
"templates": true, "templates": true,
"note-composer": true, "note-composer": true,
"command-palette": true, "command-palette": true,
"slash-command": false, "slash-command": true,
"editor-status": true, "editor-status": true,
"bookmarks": true, "bookmarks": true,
"markdown-importer": false, "markdown-importer": false,
@@ -22,7 +22,7 @@
"random-note": false, "random-note": false,
"outline": true, "outline": true,
"word-count": true, "word-count": true,
"slides": false, "slides": true,
"audio-recorder": false, "audio-recorder": false,
"workspaces": false, "workspaces": false,
"file-recovery": true, "file-recovery": true,

3
.obsidian/page-preview.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"preview": true
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
{
"id": "mermaid-popup",
"name": "Diagram Popup",
"version": "0.2.69",
"minAppVersion": "0.12.0",
"description": "Show diagrams, from Mermaid, PlantUML, Graphviz and so on, in a draggable and zoomable popup",
"author": "ChenPengyuan",
"isDesktopOnly": false
}

View File

@@ -0,0 +1,239 @@
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000; /* Ensure the overlay is on top */
}
.popup-content {
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
cursor: pointer; /* 悬停时鼠标变为手掌 */
}
.popup-content.dragging {
cursor: grabbing; /* 拖动时鼠标变为抓手 */
}
.popup-content > * {
max-width: 300% !important;
}
.popup-content > img {
pointer-events: none
}
.theme-light .popup-content {
background-color: var(--background-primary) !important; /* 白色背景 */
}
/* 深色模式下的弹窗背景 */
.theme-dark .popup-content {
background-color: var(--background-primary) !important; /* 深灰色背景 */
}
/*开启弹窗按钮*/
div.mermaid-popup-button, div.mermaid-popup-button-reading {
position: absolute !important;
right: 35px;
top: 4px;
color: var(--text-muted);
width: 30px;
height: 26px;
padding: 3px 0px 3px 5px;
border-radius: 4px;
cursor: move; /* 鼠标显示为拖动图标 */
z-index: 9999;
background-color: rgba(206, 206, 206, 0.4);
border: 1px var(--text-muted) solid;
}
div.mermaid-popup-button:hover, div.mermaid-popup-button-reading:hover {
background-color: rgb(92, 92, 92, 0.8); /* 鼠标悬停时颜色变化 */
cursor:default;
border-color: rgba(206, 206, 206, 0.4);
color: rgba(206, 206, 206, 0.4);
}
/*操作弹窗按钮*/
.button-container {
position: absolute;
bottom: 50px; /* Adjusted for desired position */
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
justify-content: center;
background: rgba(255, 255, 255, 0.7); /* Semi-transparent background for the button container */
border-radius: 5px;
padding: 5px;
backdrop-filter: blur(5px); /* Background semi-blur */
z-index: 1002; /* Ensure the button container is on top of the content */
}
.control-button {
background: rgba(255, 255, 255, 0.7); /* Semi-transparent background */
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
cursor: pointer;
font-size: 16px;
}
.control-button:hover {
background: rgba(255, 255, 255, 0.9);
}
.arrow-up, .arrow-down, .arrow-left, .arrow-right, .zoom-in, .zoom-out, .close-popup {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
/* 按钮的 hover 样式 */
.arrow-up:hover, .arrow-down:hover, .arrow-left:hover, .arrow-right:hover,
.zoom-in:hover, .zoom-out:hover, .close-popup:hover {
background-color: var(--button-bg-hover);
color: var(--button-color-hover);
}
.setting-item-on-top-line{
border-top: 0px;
}
/*以下是配置中已存图表来源的表格样式*/
/* 定义 kv-row 的 flex 布局样式 */
.kv-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
/* 输入框样式,可以定义宽度和边距 */
.kv-row input {
margin-right: 10px;
width: 200px;
}
.kv-row .kv-chk {
margin-right: 10px;
width: 20px;
}
/* 保存按钮的样式 */
.kv-row button {
margin-right: 10px;
}
/* 键值对显示区域的样式 */
.kv-display {
margin-top: 20px;
}
/* 布局 */
.setting-table table {
width: 100%;
}
.setting-table td {
padding-right: 30px;
text-align: left;
}
.setting-table .ori_diagram_height{
display:ruby;
}
.setting-table .ori_diagram_height_cur{
margin-left: 30px;
}
.setting-table .ori_diagram_height_val{
width: 50px;
}
.setting-table .settings-icon{
display: block;
border-top: 0px;
}
.open_btn_pos_slide_title{
margin-right: 5px !important;
}
.open_btn_pos_slide_width{
width: 20% !important;
}
.open_btn_pos_cur_title{
margin-left: 20px;
}
.open_btn_pos_cur_val{
width: 30px;
}
.open_btn_pos_cur_per{
margin-right: 30px;
}
.open_btn_pos .settings-icon{
display: block;
border-top: 0px;
}
/* 表格样式 */
.kv-display table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
border: 2px solid #444 !important; /* 外边框 */
}
/* 表格标题栏样式 */
.kv-display th {
background-color: var(--interactive-accent) !important;
padding: 10px;
border: 1px solid #444 !important; /* 表头的边框 */
text-align: left;
}
/* 表格行样式 */
.kv-display td {
padding: 10px;
border: 1px solid #444 !important; /* 单元格边框 */
}
/* 表格行的背景颜色交替 */
.kv-display tr:nth-child(even) {
background-color: var(--interactive-accent);
}
/* 鼠标悬停时突出显示表格行 */
.kv-display tr:hover {
background-color: #4a4a4a;
}
.config-text {
border-bottom: 1px solid gray; /* 灰色横线 */
padding-bottom: 10px; /* 给横线和文字之间添加一些间距 */
margin-bottom: 10px; /* 横线与下面内容之间的间距 */
}
.config-text-connect{
border-bottom: 1px solid gray; /* 灰色横线 */
padding-bottom: 10px; /* 给横线和文字之间添加一些间距 */
margin-bottom: 10px; /* 横线与下面内容之间的间距 */
margin-top: 30px;
}

2633
.obsidian/plugins/mermaid-tools/main.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"id": "mermaid-tools",
"name": "Mermaid Tools",
"version": "1.3.0",
"minAppVersion": "1.4.0",
"description": "Improved Mermaid.js experience for Obsidian: visual toolbar with common elements & more",
"author": "dartungar",
"authorUrl": "https://dartungar.com",
"fundingUrl": "https://www.paypal.com/paypalme/dartungar",
"isDesktopOnly": false
}

View File

@@ -0,0 +1,149 @@
.mermaid-toolbar-container, .mermaid-toolbar-container * {
max-width: 100%;
max-height: 100%;
}
.mermaid-toolbar-top-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.mermaid-toolbar-elements-container {
padding-top: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.mermaid-toolbar-element {
font-size: var(--font-ui-small);
cursor: pointer;
padding: 2px 2px 2px 5px;
border-radius: 3px;
flex: 1 0 auto;
}
.mermaid-toolbar-element:hover {
background-color: var(--interactive-hover);
}
.mermaid-tools-element-category-header::before {
content: "▼ ";
font-size: 70%;
padding-bottom: 2px;
}
.mermaid-tools-element-category-header.collapsed::before {
content: "▶ ";
font-size: 70%;
padding-bottom: 2px;
}
.mermaid-tools-element-container {
padding-top: 6px;
border-bottom: var(--border-width) solid var(--color-base-35);
}
.mermaid-tools-edit-element-modal > div {
margin-bottom: 0.5rem;
}
.mermaid-tools-edit-element-modal label {
margin-right: 1rem;
}
/* Custom Category Management Styles */
.mermaid-tools-category-management {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid var(--color-base-25);
border-radius: 8px;
background-color: var(--color-base-00);
}
.mermaid-tools-category-management h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
color: var(--text-accent);
}
.mermaid-tools-category-management button.mod-cta {
margin-bottom: 1rem;
}
/* Edit Category Modal Styles */
.mermaid-tools-edit-category-modal {
min-width: 500px;
}
.mermaid-tools-edit-category-modal .setting-item {
padding: 8px 0;
border: none;
}
.mermaid-tools-edit-category-modal .setting-item-info {
flex-grow: 1;
margin-right: 12px;
}
.mermaid-tools-edit-category-modal .setting-item-name {
font-weight: 600;
color: var(--text-normal);
}
.mermaid-tools-edit-category-modal .setting-item-description {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.mermaid-tools-edit-category-modal input,
.mermaid-tools-edit-category-modal textarea {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--color-base-30);
border-radius: 4px;
background-color: var(--color-base-00);
color: var(--text-normal);
}
.mermaid-tools-edit-category-modal input:focus,
.mermaid-tools-edit-category-modal textarea:focus {
border-color: var(--color-accent);
outline: none;
box-shadow: 0 0 0 2px var(--color-accent-2);
}
.modal-button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--color-base-25);
}
.modal-button-container button {
padding: 6px 16px;
border: 1px solid var(--color-base-30);
border-radius: 4px;
background-color: var(--color-base-10);
color: var(--text-normal);
cursor: pointer;
font-size: var(--font-ui-small);
}
.modal-button-container button:hover {
background-color: var(--color-base-20);
}
.modal-button-container button.mod-cta {
background-color: var(--color-accent);
color: var(--text-on-accent);
border-color: var(--color-accent);
}
.modal-button-container button.mod-cta:hover {
background-color: var(--color-accent-hover);
}

View File

@@ -4,24 +4,39 @@
"type": "split", "type": "split",
"children": [ "children": [
{ {
"id": "6bd8d5cd0b9fe171", "id": "fab387a4144c5e24",
"type": "tabs", "type": "tabs",
"children": [ "children": [
{ {
"id": "c5205a20b44266cb", "id": "91f1f775b766a709",
"type": "leaf", "type": "leaf",
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "README.md", "file": "AUDIO/KV2.md",
"mode": "source", "mode": "source",
"source": false "source": false
}, },
"icon": "lucide-file", "icon": "lucide-file",
"title": "README" "title": "KV2"
}
},
{
"id": "f4b542a9ab6d55a4",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "PhenixRTS/frontend-draining/19 days draining.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "19 days draining"
} }
} }
] ],
"currentTab": 1
} }
], ],
"direction": "vertical" "direction": "vertical"
@@ -53,7 +68,7 @@
"state": { "state": {
"type": "search", "type": "search",
"state": { "state": {
"query": "", "query": "dec",
"matchingCase": false, "matchingCase": false,
"explainSearch": false, "explainSearch": false,
"collapseAll": false, "collapseAll": false,
@@ -74,11 +89,12 @@
"title": "Bookmarks" "title": "Bookmarks"
} }
} }
] ],
"currentTab": 1
} }
], ],
"direction": "horizontal", "direction": "horizontal",
"width": 300 "width": 200
}, },
"right": { "right": {
"id": "755a200fbb7115db", "id": "755a200fbb7115db",
@@ -166,10 +182,25 @@
"bases:Create new base": false "bases:Create new base": false
} }
}, },
"active": "c5205a20b44266cb", "active": "f4b542a9ab6d55a4",
"lastOpenFiles": [ "lastOpenFiles": [
"ESP32/Setting up ESP32.md", "PhenixRTS/frontend-draining/THE QUERY.md",
"PhenixRTS/frontend-draining/19 days draining.md",
"AUDIO/KV2.md",
"WebSocket Lifecycle Sequence Diagram.md",
"AUDIO",
"Untitled.md",
"Untitled 2.md",
"README.md", "README.md",
"PhenixRTS/chat/Demo.md",
"PhenixRTS/frontend-draining/Potential causes.md",
"PhenixRTS/chat",
"PhenixRTS/frontend-draining/WebSocket Lifecycle Sequence Diagram.md",
"ZSH/Documentation.md",
"ZSH",
"ESP32/Setting up ESP32 Development Environment.md",
"PhenixRTS/frontend-draining",
"PhenixRTS",
"ESP32" "ESP32"
] ]
} }

89
AUDIO/KV2.md Normal file
View File

@@ -0,0 +1,89 @@
This business plan outlines the acquisition of a flagship **Kv2 Audio VHD (Very High Definition)** system.
This configuration is designed for high-impact electronic music events, outdoor festivals, and large-scale premium corporate AV. With 12 double-18" subwoofers, this system is capable
of delivering extreme sound pressure levels (SPL) while maintaining the audiophile clarity Kv2 is famous for.
### 1. Executive Summary
**Objective:** To acquire a turnkey high-performance audio system capable of covering audiences of 2,0005,000 people with distinct "club-like" pressure and fidelity.
**Total Estimated Investment:** **$230,000 $260,000**
**Primary Target Market:** Electronic Music Festivals, High-End Touring DJs, Large Nightclub Installs, and Boutique Production Rental.
### 2. Equipment Configuration & Capital Expenditure (CapEx)
The VHD system is unique because it requires specific proprietary amplification/control units to function.
**The "Main" System Ratio:**
For electronic music, a ratio of 1 Top to 3 Subs is aggressive and desirable.
* **Tops:** 4x VHD2.0 (Long throw mid/highs)
* **Subs:** 12x VHD2.18J (The dual 18" low frequency units)
* **Monitors:** A "Texas Headphones" style DJ booth (2x EX12 Tops + 2x EX2.5 MkII Subs).
| Item | Qty | Unit Estimate ($) | Total Estimate ($) | Notes |
| :----------------------- | :------- | :----------------------- | :----------------- | :-------------------------------------- |
| **Main PA Tops** | | | | |
| VHD2.0 Mid/High Cabinet | 4 | $9,500 | $38,000 | 2 per side (Main L/R) |
| VHD2000 Amp/Controller | 4 | $7,000 | $28,000 | 1 amp per top |
| **Main PA Subs** | | | | |
| VHD2.18J Dual 18" Sub | 12 | $6,500 | $78,000 | 6 per side |
| VHD3200 Amp/Controller | 6 | $6,000 | $36,000 | 1 amp drives 2 subs |
| **DJ Monitoring** | | | | |
| EX12 Active Top | 2 | $3,200 | $6,400 | High output active top |
| EX2.5 MkII Active Sub | 2 | $4,500 | $9,000 | High output active sub |
| **Infrastructure** | | | | |
| Cabling (EP6 Heavy Duty) | 1 | $8,000 | $8,000 | VHD requires specific heavy gauge cable |
| Power Distro (3-Phase) | 1 | $5,000 | $5,000 | Amps require significant current |
| Cases & Rigging | 1 | $15,000 | $15,000 | Touring grade protection |
| Total CapEx | $223,400 | (Excluding tax/shipping) | | |
### 3. Market Analysis & Strategy
#### The Competitive Advantage
Unlike competitors using Line Arrays (L-Acoustics, d&b), the Kv2 VHD is a **Point Source** system.
1. **Setup Time:** A VHD stack can be set up by 2 people in 20 minutes. Line arrays require fly-bars, motors, and complex angle calculations.
2. **Sound Quality:** Electronic music purists prefer point source for its immediate transient response and lack of comb filtering.
3. **Throw:** One VHD2.0 cabinet can throw intelligible audio over 300 ft.
#### Rental Rates (Revenue Model)
Industry standard rental rates are typically 3% to 5% of the equipment value per show day.
* **Full System Rate (Festival Package):** $6,500 $8,500 per day
* **Split System Rate (2 Smaller Rooms):** $3,500 per room/day
* **Dry Hire (B2B cross-rental):** $4,000 per day
### 4. Financial Projections (ROI)
**Assumptions:**
* **Average Rental Rate:** $5,500 (blended rate allowing for discounts/packages)
* **Crew/Logistics Cost per show:** -$1,500 (trucking and system tech)
* **Net Profit per Show:** $4,000
**Utilization Strategy:**
We aim for 4 major deployments per month (weekends).
**Monthly Revenue Calculation:**
\[ \text{Monthly Net} = 4 \text{ shows} \times \$4,000 = \$16,000 \]
**Break-Even Analysis:**
\[ \text{Break Even Point} = \frac{\text{Total CapEx}}{\text{Net Profit per Show}} \]
\[ \frac{225,000}{4,000} \approx 56.25 \text{ Shows} \]
At 4 shows a month, the system pays for itself in roughly **14 months**.
### 5. Operational Logistics
* **Power Requirements:** The VHD3200 amps are hungry. You will need a dedicated 3
* phase power distro (likely 60A or 100A 3-phase) to run 12 subs and tops without tripping breakers on bass drops.
* **Transport:** This system will **not** fit in a cargo van. The VHD2.18J is massive (approx 3.5 ft wide). You will need a **16ft or 24ft Box Truck** with a lift gate.
* **Weight:** A VHD2.18J weighs nearly 200 lbs. Everything must be on wheels.
### 6. Risk Assessment
* **Rider Acceptability:** Kv2 is a boutique brand. Some touring engineers for pop/rock acts may demand L-Acoustics or d&b exclusively.
* *Mitigation:* Market heavily to the **Electronic/Techno/House** scenes where Kv2 is considered "God-tier" audio.
* **Scalability:** Unlike line arrays, you cannot simply "add more boxes" to widen coverage indefinitely due to comb filtering.
* *Mitigation:* This system is designed for focused, long-throw energy. It is perfect for dance floors, not wide, dispersed crowds.
### 7. Conclusion
Purchasing 12 VHD2.18J units creates a "Bass-First" system that outperforms almost any line array of equivalent cost in terms of raw visceral impact. By targeting the niche electronic music market, this asset can command premium rental rates with lower setup labor costs than traditional arrays.

BIN
PhenixRTS/.DS_Store vendored Normal file

Binary file not shown.

20
PhenixRTS/chat/Demo.md Normal file
View File

@@ -0,0 +1,20 @@
```sh
curl https://pcast-stg.phenixrts.com/pcast/channel/us-central%23phenixrts.com-alex.zinn%23frontendWebsocket.LnQnpYeK26hH/message \
-u "${APPLICATION_ID}:${SECRET_STG}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-X PUT \
-d '{
"message": {
"from": {
"screenName": "Me",
"role": "Moderator",
"lastUpdate": 0
},
"mimeType": "text/plain",
"message": "This is my chat message",
"tags": ["my-tag", "my-other-tag"]
}
}'
```

View File

@@ -0,0 +1,227 @@
Hostname: `frontend-us-northeast-3-vm4w`
InstanceId: `us-northeast#us-east4-c.Iqb8nNAA`
```SQL
DECLARE
hostName STRING DEFAULT "frontend-us-northeast-3-vm4w";
DECLARE
lookbackDays INT64 DEFAULT 41;
DECLARE
start_time TIMESTAMP DEFAULT TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -lookbackDays DAY);
-----------------------------------------------------------------
-- Step 1: Find the most recent log message indicating draining connections for a specific host
WITH LatestDrainLog AS (
SELECT Message
FROM `phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp > start_time
AND Facility = 'platform'
AND HostName = hostName
AND Message LIKE 'Websocket connectionids preventing drain%'
ORDER BY Timestamp DESC LIMIT 1
-- Step 2: Extract all connection IDs from that single log message
DrainingConnectionIds AS (
SELECT connectionId
FROM
LatestDrainLog,
UNNEST(REGEXP_EXTRACT_ALL(Message, r"'([^']*)'")) AS connectionId )
),
-- Step 3: Find all logs that associate sessions with connections
SessionConnections AS (
Timestamp
SELECT
REGEXP_EXTRACT(Message, r'\] \[(.*?)\] Session started with connection') AS sessionId,
REGEXP_EXTRACT(Message, r'connection \[(.*?)\] and roles') AS connectionId,
FROM `phenix-pcast.pcast_logs_us.syslog`
WHERE Timestamp > start_time
AND Facility = 'platform'
AND Message LIKE '%Session started with connection%'
AND REGEXP_EXTRACT(Message, r'\] \[(.*?)\] Session started with connection') IS NOT NULL
AND REGEXP_EXTRACT(Message, r'connection \[(.*?)\] and roles') IS NOT NULL
),
-- Pattern 2: "Session [sessionId] has a new connection [connectionId], previously [oldConnectionId]"
SessionNewConnections AS (
SELECT
Timestamp
REGEXP_EXTRACT(Message, r'Session \[(.*?)\] has a new connection') AS sessionId,
REGEXP_EXTRACT(Message, r'connection \[(.*?)\], previously') AS connectionId
FROM `phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp > start_time
AND Facility = 'platform'
AND Message LIKE '%Session%has a new connection%'
AND REGEXP_EXTRACT(Message, r'Session \[(.*?)\] has a new connection') IS NOT NULL
AND REGEXP_EXTRACT(Message, r'connection \[(.*?)\], previously') IS NOT NULL
AllSessionConnections AS (
SELECT Timestamp, sessionId, connectionId
FROM SessionConnections
UNION DISTINCT
SELECT Timestamp, sessionId, connectionId
FROM SessionNewConnections )
)
SELECT *
FROM AllSessionConnections
ORDER BY Timestmap
```
------
```SQL
DECLARE TargetHostName STRING DEFAULT "frontend-us-northeast-3-vm4w";
DECLARE TargetInstanceId STRING DEFAULT "us-northeast#us-east4-c.Iqb8nNAA";
DECLARE TargetConnectionId STRING DEFAULT "us-central#ZQiUCdymrZHbmeF12NhUZQ8xZZXxviWD";
------------
With ConnectionIdsPreventingDrain AS (
SELECT
Message
FROM
`phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -2 MINUTE)
AND Facility = 'platform'
AND HostName = hostName
AND Message LIKE 'Websocket connectionids preventing drain%'
ORDER BY
Timestamp DESC
LIMIT
1
)
```
```SQL
DECLARE HostName STRING DEFAULT "frontend-us-northeast-3-vm4w";
CREATE TEMPORARY FUNCTION GET_METRIC_VALUE(statusJson STRING, metricName STRING) RETURNS FLOAT64 AS (
COALESCE(
(
SELECT
CAST(
JSON_EXTRACT_SCALAR(metric, '$.value') AS FLOAT64
)
FROM
UNNEST(
JSON_EXTRACT_ARRAY(JSON_EXTRACT(statusJson, '$.load'))
) AS metric
WHERE
JSON_EXTRACT_SCALAR(metric, '$.name') = metricName
LIMIT
1
),
0
)
);
-- WITH LatestInstanceMetricForHost AS (
SELECT
Timestamp,
Status,
(GET_METRIC_VALUE(Status, 'uptime/os/seconds') / 3600 ) AS UptimeHours,
(GET_METRIC_VALUE(Status, 'status/seconds') / 3600 ) AS DrainingHours
FROM `phenix-pcast.pcast.InstanceMetrics`
WHERE Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -1 MINUTE)
AND Hostname = Hostname
QUALIFY ROW_NUMBER() OVER (PARTITION BY InstanceId ORDER BY Timestamp DESC) = 1
ORDER BY Timestamp DESC
LIMIT 1
```
```SQL
DECLARE TargetInstanceId STRING DEFAULT "us-northeast#us-east4-c.Iqb8nNAA";
CREATE TEMPORARY FUNCTION GET_METRIC_VALUE(statusJson STRING, metricName STRING) RETURNS FLOAT64 AS (
COALESCE(
(
SELECT
CAST(
JSON_EXTRACT_SCALAR(metric, '$.value') AS FLOAT64
)
FROM
UNNEST(
JSON_EXTRACT_ARRAY(JSON_EXTRACT(statusJson, '$.load'))
) AS metric
WHERE
JSON_EXTRACT_SCALAR(metric, '$.name') = metricName
LIMIT
1
),
0
)
);
SELECT
Timestamp,
Status,
InstanceId,
HostName,
Health,
HealthAlert,
FORMAT('%.2f', (GET_METRIC_VALUE(Status, 'uptime/os/seconds') / 3600 )) AS UptimeHours,
FORMAT('%.2f', (GET_METRIC_VALUE(Status, 'status/seconds') / 3600 )) AS DrainingHours,
GET_METRIC_VALUE(Status, 'connections/open') AS connectionsOpen,
GET_METRIC_VALUE(Status, 'clients') AS clients,
GET_METRIC_VALUE(Status, 'clients/subscriptions') AS clientsSubscriptions,
GET_METRIC_VALUE(Status, 'clients/replay/events') AS clientsReplayEvents,
GET_METRIC_VALUE(Status, 'mq/incoming/pending') AS mqIncomingPending,
GET_METRIC_VALUE(Status, 'mq/outgoing/pending') AS mqOutgoingPending,
GET_METRIC_VALUE(Status, 'mq/incoming/rate') AS mqIncomingRate,
FROM `phenix-pcast.pcast.InstanceMetrics`
WHERE Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -1 MINUTE)
AND InstanceId = TargetInstanceId
QUALIFY ROW_NUMBER() OVER (PARTITION BY InstanceId ORDER BY Timestamp DESC) = 1
ORDER BY Timestamp DESC
LIMIT 1
```
Using
```SQL
SELECT
Timestamp,
Category,
Severity,
Message,
HostName,
Region,
Zone,
FROM
`phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -90 DAY)
AND Facility = 'platform'
AND Service = 'frontend'
AND Message LIKE "%Drain instance%"
AND HostName = 'frontend-us-northeast-3-vm4w'
ORDER BY
Timestamp
```
`HostName`: `frontend-us-northeast-3-vm4w`
Went into draining
`2025-11-03 19:49:37.012998 UTC` - `[us-northeast#us-east4-c.Iqb8nNAA] Drain instance (undoable=[false])`
`Skipping ping as previous ping is still pending since [1760474289916]`
1760474289916 --> `2025-10-14T20:38:09.916Z`

View File

@@ -0,0 +1,37 @@
- Close path can loop forever if peer never finishes closing. When `close()` is called we just invoke `socket.close()` and wait for a future `disconnectDelegate` If the FIN/ACK never arrives, we keep the entry and never fall back to terminate() (despite adding that method). Consider scheduling a timeout so that a close automatically escalates to terminate() and disconnectDelegate cleanup.
```sql
-- Check for send errors during drain
SELECT
timestamp,
message
FROM
`phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -10 DAY)
AND Facility = 'platform'
AND HostName = "frontend-australia-southeast-1-k3ms"
AND (
message LIKE '%Failed to send%'
OR message LIKE '%not opened%'
OR message LIKE '%not found%'
)
ORDER BY timestamp DESC
LIMIT 100;
```
| id | Timestamp | Message | Hostname | Thread |
| :-- | :----------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | :----- |
| 0 | 2025-11-22 14:09:32.453398 UTC | [australia-southeast#WOkAeFYOsT3gWMwVbTLpcHYqIEYttN9k] Rejecting message [chat.RoomEvent] for closed client: Client MQ adapter for australia-southeast#WOkAeFYOsT3gWMwVbTLpcHYqIEYttN9k not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 1 | 2025-11-22 14:04:56.692624 UTC | [australia-southeast#f0AOMhXqggfsFmog5FYfDOfHzl4guVTp] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#f0AOMhXqggfsFmog5FYfDOfHzl4guVTp not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 2 | 2025-11-22 14:04:55.976509 UTC | [australia-southeast#01let42yTTGJJViCmDNG5YkfPAhr1Fk5] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#01let42yTTGJJViCmDNG5YkfPAhr1Fk5 not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 3 | 2025-11-22 14:03:48.854516 UTC | [australia-southeast#x1o42d2VicP5Cfq9pYH1k9eW6sKi5djC] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#x1o42d2VicP5Cfq9pYH1k9eW6sKi5djC not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 4 | 2025-11-22 10:24:52.469294 UTC | [australia-southeast#TVhflpoNzOK6dSloQCFqoYcY1ttbiOiM] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#TVhflpoNzOK6dSloQCFqoYcY1ttbiOiM not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 5 | 2025-11-22 10:00:42.335501 UTC | [australia-southeast#Kb5C372wLecwyTihj1yO1jZT5t6ghNmU] Rejecting message [chat.RoomEvent] for closed client: Client MQ adapter for australia-southeast#Kb5C372wLecwyTihj1yO1jZT5t6ghNmU not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 6 | 2025-11-22 06:46:44.278841 UTC | [australia-southeast#f4cOuMd2wMGQ7v8DWLVYbCu6andAXeKO] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#f4cOuMd2wMGQ7v8DWLVYbCu6andAXeKO not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 7 | 2025-11-22 06:37:02.677132 UTC | [australia-southeast#vKYJCpFY1rXi4VT0Rn1A5ufFvbHuUNX4] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#vKYJCpFY1rXi4VT0Rn1A5ufFvbHuUNX4 not found | frontend-australia-southeast-1-k3ms | PID:35 |
| 8 | 2025-11-22 06:36:11.151866 UTC | [australia-southeast#S8vqusQFBG4s7zK4IS1hlqIg5ADM4KwB] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#S8vqusQFBG4s7zK4IS1hlqIg5ADM4KwB not found | frontend-australia-southeast-1-k3ms | PID:35 |

View File

@@ -0,0 +1,35 @@
```SQL
WITH DrainingConnectionIds AS (
SELECT DISTINCT
connectionId
FROM
`phenix-pcast.pcast_logs_us.syslog`,
UNNEST(REGEXP_EXTRACT_ALL(Message, r"'([^']+)'")) AS connectionId
WHERE
Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -2 MINUTE)
AND Message LIKE "%Websocket connectionids preventing drain%"
),
SessionStartLogs AS (
SELECT
REGEXP_EXTRACT(Message, r"\[([^\]]+)\]") AS ApplicationId,
REGEXP_EXTRACT(Message, r"connection \[([^\]]+)\]") AS ConnectionId,
Message
FROM
`phenix-pcast.pcast_logs_us.syslog`
WHERE
Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -90 DAY)
AND Message LIKE '%Session started with connection%'
)
SELECT
s.ApplicationId,
s.ConnectionId,
REGEXP_EXTRACT_ALL(s.Message, r"connection \[(\]]+)\]")[SAFE_OFFSET(1)] AS SessionId
FROM
SessionStartLogs AS s
INNER JOIN
DrainingConnectionIds AS d
ON
s.ConnectionId = d.connectionId
```

View File

@@ -0,0 +1,190 @@
****
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>WebSocketServer: handleUpgrade()
WebSocketServer->>WebSocketServer: Generate connection.id (32 chars)
WebSocketServer->>WebSocketServer: Get remoteAddress (from X-Forwarded-For)
WebSocketServer->>WebSocketServer: Setup event handlers<br/>(message, close, pong, error)
WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers)
ClientMQAdapter->>ClientMQAdapter: Set __connectionStart<br/>Set __connectionId<br/>Set __meta {headers}
ClientMQAdapter->>ClientMQAdapter: Register in _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = true
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
ClientMQWorker->>ClientMQWorker: Increment _connectionsTotal<br/>Increment _connectionsOpen
alt Connection limit exceeded (>2000)
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
Note over ClientMQAdapter: See "Server-Initiated Close" below
end
Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) ===
Client->>WebSocketServer: Message (binary or base64)
WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data)
ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol)
alt Unsupported message format
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Socket not connected
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Message type = 'Request'
ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta)
alt Request timeout (>15s)
MQWorker-->>ClientMQAdapter: TimeoutError
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request failed
MQWorker-->>ClientMQAdapter: Error
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request succeeded
MQWorker-->>ClientMQAdapter: {type, payload, wallTime}
ClientMQAdapter->>ClientMQAdapter: Build Response message<br/>(messageType: 'Response', requestId, type, payload)
alt Socket not open
ClientMQAdapter->>ClientMQAdapter: Drop response, log debug
else Socket open
ClientMQAdapter->>Client: Response (binary or base64)
ClientMQAdapter->>ClientMQ: emit('request', connectionId, data)
ClientMQ->>ClientMQWorker: emit('request', connectionId, data)
ClientMQWorker->>ClientMQWorker: Handle subscription updates<br/>(JoinRoom, LeaveRoom, etc.)
end
end
else Message type = 'Response'
alt Request handler not found
ClientMQAdapter->>ClientMQAdapter: Log warning, ignore
else Request handler exists
ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise
end
else Message type = 'Event'
ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported)
end
Note over Client,ClientMQWorker: === Server-Initiated Request ===
ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message)
ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message)
ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter)
ClientMQAdapter->>ClientMQAdapter: Register in _requests map
alt Socket not found
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found'
else Socket not connected
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened'
else Socket valid
ClientMQAdapter->>Client: Request (binary or base64)
ClientMQAdapter->>ClientMQAdapter: Start timeout (15s)
Client->>WebSocketServer: Response message
WebSocketServer->>ClientMQAdapter: messageDelegate (Response)
ClientMQAdapter->>ClientMQAdapter: Resolve request promise
ClientMQAdapter-->>ClientMQWorker: Response message
alt Response timeout
ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout
end
end
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
PingInterval->>ClientMQAdapter: Ping interval trigger
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
Client->>WebSocketServer: pong()
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each]
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>Client: socket.close(code, reason)
Client->>WebSocketServer: close event
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
ClientMQWorker->>ClientMQWorker: Decrement _connectionsOpen
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
Note over Client,ClientMQWorker: === Client-Initiated Close ===
Client->>WebSocketServer: close connection
WebSocketServer->>WebSocketServer: close event (reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
Note over ClientMQAdapter: Same disconnect flow as above
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```

278
Untitled.md Normal file
View File

@@ -0,0 +1,278 @@
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQWebsocketAdapter as ClientMQAdapter
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
activate Client
Client->>HTTPServer: HTTP Upgrade Request
activate HTTPServer
HTTPServer->>WebSocketServer: upgrade event
deactivate HTTPServer
activate WebSocketServer
WebSocketServer->>WebSocketServer: handleUpgrade()
WebSocketServer->>WebSocketServer: ...
WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers)
deactivate WebSocketServer
activate ClientMQAdapter
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
deactivate ClientMQAdapter
activate ClientMQ
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
deactivate ClientMQ
activate ClientMQWorker
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
deactivate ClientMQWorker
deactivate Client
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
activate PingInterval
PingInterval->>ClientMQAdapter: Ping interval trigger
deactivate PingInterval
activate ClientMQAdapter
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
deactivate ClientMQAdapter
activate ClientMQ
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
deactivate ClientMQ
activate ClientMQWorker
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
deactivate ClientMQWorker
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
activate Client
Client->>WebSocketServer: pong()
deactivate Client
activate WebSocketServer
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
deactivate WebSocketServer
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
deactivate ClientMQAdapter
Note over Client,ClientMQWorker: === Server-Initiated Close ===
activate ClientMQWorker
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
deactivate ClientMQWorker
activate ClientMQ
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
deactivate ClientMQ
activate ClientMQAdapter
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
deactivate ClientMQWorker
activate ClientMQ
loop For each connection
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
activate ClientMQAdapter
Note over ClientMQAdapter: Full close processing happens<br/>for each connection in loop
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>WebSocketServer: socket.close(code, reason)
activate WebSocketServer
WebSocketServer->>Client: Send Close Frame
activate Client
deactivate Client
activate Client
Client->>WebSocketServer: Send Close Frame (acknowledgment)
deactivate Client
WebSocketServer->>WebSocketServer: 'close' event fires
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
deactivate WebSocketServer
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
activate ClientMQ
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
deactivate ClientMQ
activate ClientMQWorker
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
activate MQWorker
deactivate MQWorker
deactivate ClientMQWorker
end
deactivate ClientMQAdapter
end
deactivate ClientMQ
activate ClientMQAdapter
Note over ClientMQAdapter: All connections processed in loop above<br/>Processing code below will be no-op (socket not found)
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
deactivate ClientMQWorker
activate ClientMQ
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
deactivate ClientMQ
activate ClientMQAdapter
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
deactivate ClientMQWorker
activate ClientMQ
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
deactivate ClientMQ
activate ClientMQAdapter
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
deactivate ClientMQWorker
activate ClientMQ
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
deactivate ClientMQ
activate ClientMQAdapter
end
Note over ClientMQAdapter: Processing for single connection close<br/>(ClientMQAdapter active for non-drain cases only)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
deactivate ClientMQAdapter
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
deactivate ClientMQAdapter
activate ClientMQ
deactivate ClientMQ
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
rect rgb(200, 255, 200)
Note over ClientMQAdapter,Client: 📤 SERVER SENDS CLOSE FRAME
ClientMQAdapter->>WebSocketServer: socket.close(code, reason)
activate WebSocketServer
WebSocketServer->>Client: Send Close Frame (code, reason)
activate Client
Note over Client: Client receives server's close frame
deactivate Client
end
rect rgb(255, 200, 200)
Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME<br/>(WebSocket Protocol Requirement)
alt Client sends close frame
activate Client
Client->>WebSocketServer: Send Close Frame (acknowledgment)
deactivate Client
Note over WebSocketServer: Platform receives client's close frame
else Client does NOT send close frame
Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)<br/>Connection marked as forcefully closed
Note over ClientMQAdapter: Next disconnect will use<br/>disconnect-after-forcefull-close path
end
end
rect rgb(200, 200, 255)
Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING
WebSocketServer->>WebSocketServer: 'close' event fires<br/>(reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
deactivate WebSocketServer
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early<br/>(_connectionsOpen NOT decremented)
deactivate ClientMQAdapter
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')<br/>(_connectionsOpen NOT decremented)
deactivate ClientMQAdapter
activate ClientMQ
deactivate ClientMQ
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')<br/>(_connectionsOpen NOT decremented)
deactivate ClientMQAdapter
activate ClientMQ
deactivate ClientMQ
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
deactivate ClientMQAdapter
activate ClientMQ
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
deactivate ClientMQ
activate ClientMQWorker
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION<br/>⚠️ Only happens after receiving client's close frame
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
activate MQWorker
deactivate MQWorker
deactivate ClientMQWorker
end
end
end
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
activate Client
Client->>WebSocketServer: error event
deactivate Client
activate WebSocketServer
WebSocketServer->>WebSocketServer: Log error
deactivate WebSocketServer
else Handler error
activate WebSocketServer
WebSocketServer->>WebSocketServer: Log error, continue
deactivate WebSocketServer
else Send error (not opened)
activate ClientMQWorker
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
deactivate ClientMQWorker
activate ClientMQ
deactivate ClientMQ
activate ClientMQWorker
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
activate MQWorker
deactivate MQWorker
deactivate ClientMQWorker
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
activate ClientMQWorker
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
deactivate ClientMQWorker
end
```

View File

@@ -0,0 +1,508 @@
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>WebSocketServer: handleUpgrade()
WebSocketServer->>WebSocketServer: Generate connection.id (32 chars)
WebSocketServer->>WebSocketServer: Get remoteAddress (from X-Forwarded-For)
WebSocketServer->>WebSocketServer: Setup event handlers<br/>(message, close, pong, error)
WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers)
ClientMQAdapter->>ClientMQAdapter: Set __connectionStart<br/>Set __connectionId<br/>Set __meta {headers}
ClientMQAdapter->>ClientMQAdapter: Register in _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = true
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
alt Connection limit exceeded (>2000)
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
Note over ClientMQAdapter: See "Server-Initiated Close" below
end
Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) ===
Client->>WebSocketServer: Message (binary or base64)
WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data)
ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol)
alt Unsupported message format
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Socket not connected
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Message type = 'Request'
ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta)
alt Request timeout (>15s)
MQWorker-->>ClientMQAdapter: TimeoutError
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request failed
MQWorker-->>ClientMQAdapter: Error
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request succeeded
MQWorker-->>ClientMQAdapter: {type, payload, wallTime}
ClientMQAdapter->>ClientMQAdapter: Build Response message<br/>(messageType: 'Response', requestId, type, payload)
alt Socket not open
ClientMQAdapter->>ClientMQAdapter: Drop response, log debug
else Socket open
ClientMQAdapter->>Client: Response (binary or base64)
ClientMQAdapter->>ClientMQ: emit('request', connectionId, data)
ClientMQ->>ClientMQWorker: emit('request', connectionId, data)
ClientMQWorker->>ClientMQWorker: Handle subscription updates<br/>(JoinRoom, LeaveRoom, etc.)
end
end
else Message type = 'Response'
alt Request handler not found
ClientMQAdapter->>ClientMQAdapter: Log warning, ignore
else Request handler exists
ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise
end
else Message type = 'Event'
ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported)
end
Note over Client,ClientMQWorker: === Server-Initiated Request ===
ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message)
ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message)
ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter)
ClientMQAdapter->>ClientMQAdapter: Register in _requests map
alt Socket not found
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found'
else Socket not connected
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened'
else Socket valid
ClientMQAdapter->>Client: Request (binary or base64)
ClientMQAdapter->>ClientMQAdapter: Start timeout (15s)
Client->>WebSocketServer: Response message
WebSocketServer->>ClientMQAdapter: messageDelegate (Response)
ClientMQAdapter->>ClientMQAdapter: Resolve request promise
ClientMQAdapter-->>ClientMQWorker: Response message
alt Response timeout
ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout
end
end
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
PingInterval->>ClientMQAdapter: Ping interval trigger
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
Client->>WebSocketServer: pong()
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each]
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>Client: socket.close(code, reason)
Client->>WebSocketServer: close event
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
Note over Client,ClientMQWorker: === Client-Initiated Close ===
Client->>WebSocketServer: close connection
WebSocketServer->>WebSocketServer: close event (reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
Note over ClientMQAdapter: Same disconnect flow as above
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```
## connectionsOpen
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>WebSocketServer: handleUpgrade()
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>Client: socket.close(code, reason)
Client->>WebSocketServer: close event
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```
# WebSocket Lifecycle Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQWebsocketAdapter as ClientMQAdapter
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>+ClientMQAdapter: connectDelegate(socket, headers)
ClientMQAdapter->>ClientMQAdapter: Set __connectionStart<br/>Set __connectionId<br/>Set __meta {headers}
ClientMQAdapter->>ClientMQAdapter: Register in _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = true
ClientMQAdapter->>-ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
alt Connection limit exceeded (>2000)
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
Note over ClientMQAdapter: See "Server-Initiated Close" below
end
Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) ===
Client->>WebSocketServer: Message (binary or base64)
WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data)
ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol)
alt Unsupported message format
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Socket not connected
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Message type = 'Request'
ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta)
alt Request timeout (>15s)
MQWorker-->>ClientMQAdapter: TimeoutError
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request failed
MQWorker-->>ClientMQAdapter: Error
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request succeeded
MQWorker-->>ClientMQAdapter: {type, payload, wallTime}
ClientMQAdapter->>ClientMQAdapter: Build Response message<br/>(messageType: 'Response', requestId, type, payload)
alt Socket not open
ClientMQAdapter->>ClientMQAdapter: Drop response, log debug
else Socket open
ClientMQAdapter->>Client: Response (binary or base64)
ClientMQAdapter->>ClientMQ: emit('request', connectionId, data)
ClientMQ->>ClientMQWorker: emit('request', connectionId, data)
ClientMQWorker->>ClientMQWorker: Handle subscription updates<br/>(JoinRoom, LeaveRoom, etc.)
end
end
else Message type = 'Response'
alt Request handler not found
ClientMQAdapter->>ClientMQAdapter: Log warning, ignore
else Request handler exists
ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise
end
else Message type = 'Event'
ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported)
end
Note over Client,ClientMQWorker: === Server-Initiated Request ===
ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message)
ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message)
ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter)
ClientMQAdapter->>ClientMQAdapter: Register in _requests map
alt Socket not found
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found'
else Socket not connected
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened'
else Socket valid
ClientMQAdapter->>Client: Request (binary or base64)
ClientMQAdapter->>ClientMQAdapter: Start timeout (15s)
Client->>WebSocketServer: Response message
WebSocketServer->>ClientMQAdapter: messageDelegate (Response)
ClientMQAdapter->>ClientMQAdapter: Resolve request promise
ClientMQAdapter-->>ClientMQWorker: Response message
alt Response timeout
ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout
end
end
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
PingInterval->>ClientMQAdapter: Ping interval trigger
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
Client->>WebSocketServer: pong()
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each]
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
rect rgb(200, 255, 200)
Note over ClientMQAdapter,Client: 📤 SERVER SENDS CLOSE FRAME
ClientMQAdapter->>WebSocketServer: socket.close(code, reason)
WebSocketServer->>Client: Send Close Frame (code, reason)
Note over Client: Client receives server's close frame
end
rect rgb(255, 200, 200)
Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME<br/>(WebSocket Protocol Requirement)
alt Client sends close frame
Client->>WebSocketServer: Send Close Frame (acknowledgment)
Note over WebSocketServer: Platform receives client's close frame
else Client does NOT send close frame
Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)<br/>Connection marked as forcefully closed
Note over ClientMQAdapter: Next disconnect will use<br/>disconnect-after-forcefull-close path
end
end
rect rgb(200, 200, 255)
Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING
WebSocketServer->>WebSocketServer: 'close' event fires<br/>(reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early<br/>(_connectionsOpen NOT decremented)
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')<br/>(_connectionsOpen NOT decremented)
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')<br/>(_connectionsOpen NOT decremented)
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION<br/>⚠️ Only happens after receiving client's close frame
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
end
Note over Client,ClientMQWorker: === Client-Initiated Close ===
rect rgb(255, 200, 200)
Note over Client,WebSocketServer: 📥 CLIENT SENDS CLOSE FRAME
Client->>WebSocketServer: Send Close Frame (code, reason)
Note over WebSocketServer: Platform receives client's close frame
end
rect rgb(200, 255, 200)
Note over WebSocketServer,Client: 📤 SERVER RESPONDS WITH CLOSE FRAME<br/>(WebSocket Protocol Requirement)
WebSocketServer->>Client: Send Close Frame (acknowledgment)
end
rect rgb(200, 200, 255)
Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING
WebSocketServer->>WebSocketServer: 'close' event fires<br/>(reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
Note over ClientMQAdapter: Same disconnect flow as server-initiated<br/>(_connectionsOpen decremented after close event)
end
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```

107
ZSH/Documentation.md Normal file
View File

@@ -0,0 +1,107 @@
[source](https://zsh.sourceforge.io/Guide/zshguide02.html)
### Options
The usual way to set and unset options
```zsh
setopt <string> # sets an option
unsetopt <string> # unsets the option
# Note
set -o
# is the equivelant to
setopt
# NOTE:
set # without the `-o` does something else -- sets the positional paramters
```
both `~/.zshrc` and `~/.zshenv` run for every shell
`~/.zshrc` is executed upon a shell starting for every **interactive** shell
### Parameters
Simple parameters can be assigned
```zsh
foo='This is a parameter'
```
Note no spaces between the `foo` and `=`
Single quotes, as here, are the nuclear option of quotes: everything up to another single quote is treated as a simple string --- newlines, equal signs, unprintable characters, the lot, in this example all would be assigned to the variable; for example,
```zsh
foo='This is a parameter.
This is still the same parameter.'
```
So they're the best thing to use until you know what you're doing with double quotes, which have extra effects. Sometimes you don't need them, for example:
```
foo=oneword
```
because there's nothing in `oneword` to confuse the shell; but you could still put quotes there anyway.
#### Parameter Expansion
```zsh
foo='This is a parameter'
print -- '$foo is "'$foo'"'
```
prints
```zsh
$foo is "This is a parameter"
```
expansion happens anywhere the parameter is not quoted; it doesn't have to be on its own, just separated from anything which might make it look like a different parameter. This is one of those things that can help make shell scripts look so barbaric.
Note: why the `---` after the `print`?
because **print**, like many UNIX commands, can take options after it which begin with a -` `--` says that there are no more options; so if what you're trying to print begins with a ``-`', it will still print out.
### Arrays
There is a special type of parameter called an **array** which zsh inherited from both ksh and csh. This is a slightly shaky marriage, since some of the things those two shells do with them are not compatible, and zsh has elements of both, so you need to be careful if you've used arrays in either. The option `KSH_ARRAYS` is something you can set to make them behave more like they do in ksh, but a lot of zsh users write functions and scripts assuming it isn't set, so it can be dangerous.
Unlike normal parameters (known as **scalars**), arrays have more than one word in them. In the examples above, we made the parameter `$foo` get a string with spaces in, but the spaces weren't significant. If we'd done
foo=(This is a parameter.)
(note the absence of quotes), it would have created an array. Again, there must be no space between the ``=`' and the `(', though inside the parentheses spaces separate words just like they do on a command line. The difference isn't obvious if you try and print it --- it looks just the same --- but now try this:
print -- ${foo[4]}
and you get ``parameter.`'. The array stores the words separately, and you can retrieve them separately by putting the number of the element of the array in square brackets. Note also the braces ``{...}`' --- zsh doesn't always require them, but they make things much clearer when things get complicated, and it's never wrong to put them in: you could have said ``${foo}`' when you wanted to print out the complete parameter, and it would be treated identically to ``$foo`'. The braces simply screen off the expansion from whatever else might be lying around to confuse the shell. It's useful too in expressions like ``${foo}s`' to keep the ``s`' from being part of the parameter name; and, finally, with `KSH_ARRAYS` set, the braces are compulsory, though unfortunately arrays are indexed from 0 in that case.
You can use quotes when defining arrays; as before, this protects against the shell thinking the spaces are between different elements of the array. Try:
foo=('first element' 'second element')
print -- ${foo[2]}
Arrays are useful when the shell needs to keep a whole series of different things together, so we'll meet some you may want to put in a startup file. Users of ksh will have noticed that things are a bit different in zsh, but for now I'll just assume you're using the normal zsh way of doing things.
### Compatibility Options
- `SH_WORD_SPLT` - Split string variables into arrays. ZSH Default: `not set`
- `NO_BANG_HIST`
- `BSD_ECHO` (sh only)
- `IGNORE_BRACES`
- `INTERACTIVE_COMMENTS`
- `KSH_OPTION_PRINT`
- `NO_MULTIOS` 
- `POSIX_BUILTINS`
- `PROMPT_BANG`
- `SINGLE_LINE_ZLE`
**`BG_NICE`& `NOTIFY`**
All UNIX shells allow you to start a _background_ job by putting `&` at the end of the line; then the shell doesn't wait for the job to finish, so you can type something else. In zsh, such jobs are usually run at a lower priority (a `higher nice value` in UNIX-speak), so that they don't use so much of the processor's time as foreground jobs (all the others, without the &`) do. This is so that jobs like editing or using the shell don't get slowed down, which can be highly annoying. You can turn this feature off by setting `NO_BG_NICE`
When a background job finishes, zsh usually tells you immediately by printing a message, which interrupts whatever you're doing. You can stop this by setting `NO_NOTIFY`. Actually, this is an option in most versions of ksh, too, but it's a little less annoying in zsh because if it happens while you're typing something else to the shell, the shell will reprint the line you were on as far as you've got. For example:
### HUP
- `SIGINT` - what the shell interprets `^C` (CNTRL + C)
- `SIGHUP` -