2025-11-25
This commit is contained in:
5
.obsidian/app.json
vendored
5
.obsidian/app.json
vendored
@@ -1 +1,4 @@
|
|||||||
{}
|
{
|
||||||
|
"showLineNumber": false,
|
||||||
|
"readableLineLength": false
|
||||||
|
}
|
||||||
7
.obsidian/appearance.json
vendored
7
.obsidian/appearance.json
vendored
@@ -1 +1,6 @@
|
|||||||
{}
|
{
|
||||||
|
"baseFontSize": 12,
|
||||||
|
"translucency": true,
|
||||||
|
"accentColor": "#95b1ea",
|
||||||
|
"baseFontSizeAction": true
|
||||||
|
}
|
||||||
1
.obsidian/community-plugins.json
vendored
Normal file
1
.obsidian/community-plugins.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
4
.obsidian/core-plugins.json
vendored
4
.obsidian/core-plugins.json
vendored
@@ -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
3
.obsidian/page-preview.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"preview": true
|
||||||
|
}
|
||||||
3
.obsidian/plugins/mermaid-popup/main.js
vendored
Normal file
3
.obsidian/plugins/mermaid-popup/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
.obsidian/plugins/mermaid-popup/manifest.json
vendored
Normal file
9
.obsidian/plugins/mermaid-popup/manifest.json
vendored
Normal 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
|
||||||
|
}
|
||||||
239
.obsidian/plugins/mermaid-popup/styles.css
vendored
Normal file
239
.obsidian/plugins/mermaid-popup/styles.css
vendored
Normal 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
2633
.obsidian/plugins/mermaid-tools/main.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
.obsidian/plugins/mermaid-tools/manifest.json
vendored
Normal file
11
.obsidian/plugins/mermaid-tools/manifest.json
vendored
Normal 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
|
||||||
|
}
|
||||||
149
.obsidian/plugins/mermaid-tools/styles.css
vendored
Normal file
149
.obsidian/plugins/mermaid-tools/styles.css
vendored
Normal 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);
|
||||||
|
}
|
||||||
51
.obsidian/workspace.json
vendored
51
.obsidian/workspace.json
vendored
@@ -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
89
AUDIO/KV2.md
Normal 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,000–5,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
BIN
PhenixRTS/.DS_Store
vendored
Normal file
Binary file not shown.
20
PhenixRTS/chat/Demo.md
Normal file
20
PhenixRTS/chat/Demo.md
Normal 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"]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
227
PhenixRTS/frontend-draining/19 days draining.md
Normal file
227
PhenixRTS/frontend-draining/19 days draining.md
Normal 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`
|
||||||
|
|
||||||
|
|
||||||
37
PhenixRTS/frontend-draining/Potential causes.md
Normal file
37
PhenixRTS/frontend-draining/Potential causes.md
Normal 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 |
|
||||||
|
|
||||||
35
PhenixRTS/frontend-draining/THE QUERY.md
Normal file
35
PhenixRTS/frontend-draining/THE QUERY.md
Normal 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
|
||||||
|
|
||||||
|
```
|
||||||
@@ -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
278
Untitled.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
508
WebSocket Lifecycle Sequence Diagram.md
Normal file
508
WebSocket Lifecycle Sequence Diagram.md
Normal 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
107
ZSH/Documentation.md
Normal 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` -
|
||||||
Reference in New Issue
Block a user