cabloy 5.1.58 → 5.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/cabloy-contract-loop/SKILL.md +16 -0
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +26 -0
- package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +144 -0
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +18 -0
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +267 -0
- package/.claude/skills/cabloy-resource-field-update/evals/evals.json +53 -0
- package/.claude/skills/cabloy-resource-field-update/references/custom-renderer-demo-checklist.md +102 -0
- package/.claude/skills/cabloy-resource-field-update/references/field-update-decision-tree.md +120 -0
- package/.claude/skills/cabloy-resource-field-update/references/follow-up-checklist.md +80 -0
- package/.claude/skills/cabloy-resource-field-update/references/verification-checklist.md +97 -0
- package/.github/workflows/docs-pages.yml +2 -0
- package/.github/workflows/vona-cov-pg.yml +2 -0
- package/.github/workflows/vona-test-crud.yml +4 -2
- package/.github/workflows/vona-test-mysql.yml +2 -0
- package/.github/workflows/vona-test-pg.yml +2 -0
- package/.github/workflows/vona-test-sqlite3.yml +2 -0
- package/.github/workflows/vona-tsc.yml +2 -0
- package/.github/workflows/zova-ui.yml +2 -0
- package/.gitignore +0 -4
- package/CHANGELOG.md +41 -0
- package/CLAUDE.md +2 -0
- package/README.md +15 -0
- package/cabloy-docs/.vitepress/config.mjs +43 -0
- package/cabloy-docs/ai/class-placement-rule.md +2 -0
- package/cabloy-docs/ai/cli-to-skill-map.md +7 -0
- package/cabloy-docs/ai/future-skill-roadmap.md +17 -2
- package/cabloy-docs/backend/bean-scene-authoring.md +350 -0
- package/cabloy-docs/backend/cli.md +26 -1
- package/cabloy-docs/backend/foundation.md +28 -3
- package/cabloy-docs/backend/introduction.md +8 -0
- package/cabloy-docs/backend/service-guide.md +2 -0
- package/cabloy-docs/backend/websocket-call-flow.md +435 -0
- package/cabloy-docs/backend/websocket-guide.md +455 -0
- package/cabloy-docs/backend/websocket-protocol-guide.md +381 -0
- package/cabloy-docs/backend/websocket-usage-guide.md +356 -0
- package/cabloy-docs/frontend/bean-scene-authoring.md +372 -0
- package/cabloy-docs/frontend/behavior-guide.md +449 -0
- package/cabloy-docs/frontend/cli.md +12 -0
- package/cabloy-docs/frontend/introduction.md +5 -0
- package/cabloy-docs/frontend/ioc-and-beans.md +10 -9
- package/cabloy-docs/frontend/router-tabs-admin-web-comparison.md +206 -0
- package/cabloy-docs/frontend/router-tabs-introduction.md +106 -0
- package/cabloy-docs/frontend/router-tabs-mechanism.md +469 -0
- package/cabloy-docs/frontend/router-tabs-overview.md +227 -0
- package/cabloy-docs/frontend/router-tabs-route-meta-cookbook.md +343 -0
- package/cabloy-docs/frontend/ssr-architecture-overview.md +211 -0
- package/cabloy-docs/frontend/ssr-build-deploy-guide.md +308 -0
- package/cabloy-docs/frontend/ssr-review-checklist.md +184 -0
- package/cabloy-docs/frontend/ssr-troubleshooting-guide.md +301 -0
- package/cabloy-docs/fullstack/framework-performance.md +3 -3
- package/cabloy-docs/fullstack/introduction.md +29 -0
- package/cabloy-docs/fullstack/quickstart.md +7 -1
- package/cabloy-docs/fullstack/tutorial-1-first-module.md +111 -0
- package/cabloy-docs/fullstack/tutorial-2-first-crud.md +122 -0
- package/cabloy-docs/fullstack/tutorial-3-frontend-metadata-sharing.md +131 -0
- package/cabloy-docs/fullstack/tutorial-4-custom-level-renderers.md +119 -0
- package/cabloy-docs/fullstack/tutorial-5-backend-contract-sharing.md +144 -0
- package/cabloy-docs/fullstack/tutorial-6-one-contract-four-uses.md +168 -0
- package/cabloy-docs/fullstack/tutorials-overview.md +179 -0
- package/cabloy-docs/index.md +4 -3
- package/cabloy-docs/reference/bean-scene-boilerplates.md +73 -0
- package/cabloy-docs/reference/cli-reference.md +2 -0
- package/package.json +6 -2
- package/scripts/init.ts +18 -2
- package/scripts/upgrade.ts +6 -0
- package/vona/packages-cli/cabloy-cli/package.json +2 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.create.module.ts +4 -0
- package/vona/packages-utils/zod-query/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +133 -1088
- package/vona/pnpm-workspace.yaml +0 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/assets/static/img/vona.svg +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +3 -3
- package/zova/packages-cli/cli-set-front/cli/templates/init/icon/boilerplate/icons/default/zova.svg +1 -1
- package/zova/packages-cli/cli-set-front/package.json +3 -3
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.create.module.ts +4 -0
- package/zova/packages-cli/cli-set-front/src/lib/command/create.bean.ts +5 -1
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/pnpm-lock.yaml +284 -1313
- package/zova/pnpm-workspace.yaml +0 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/cabloy.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/vona.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/zova.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/src/.metadata/icons/groups/social.svg +3 -3
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +9 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +66 -16
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +60 -18
- package/zova/src/suite-vendor/a-zova/modules/a-table/cli/tableActionRow/boilerplate/{{sceneName}}.{{beanName}}.tsx_ +6 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/cli/tableCell/boilerplate/{{sceneName}}.{{beanName}}.tsx_ +6 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-zod/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Web Socket Call Flow
|
|
2
|
+
|
|
3
|
+
This guide is a source-oriented execution trace for the built-in `a-socket` module.
|
|
4
|
+
|
|
5
|
+
Read this together with:
|
|
6
|
+
|
|
7
|
+
- [Web Socket Guide](/backend/websocket-guide)
|
|
8
|
+
- [Web Socket Usage Guide](/backend/websocket-usage-guide)
|
|
9
|
+
- [Web Socket Protocol Guide](/backend/websocket-protocol-guide)
|
|
10
|
+
|
|
11
|
+
Use the practical split:
|
|
12
|
+
|
|
13
|
+
- [Web Socket Guide](/backend/websocket-guide) for architecture
|
|
14
|
+
- [Web Socket Usage Guide](/backend/websocket-usage-guide) for server-side authoring patterns
|
|
15
|
+
- [Web Socket Protocol Guide](/backend/websocket-protocol-guide) for the client-visible wire format
|
|
16
|
+
- this page for source tracing and debugging
|
|
17
|
+
|
|
18
|
+
## Why this call-flow view matters
|
|
19
|
+
|
|
20
|
+
The main Web Socket guide explains the architecture and extension model.
|
|
21
|
+
|
|
22
|
+
This page answers a different question:
|
|
23
|
+
|
|
24
|
+
- what methods actually run, and in what order, when a socket starts, receives a packet, sends a message, or closes?
|
|
25
|
+
|
|
26
|
+
That matters when you are:
|
|
27
|
+
|
|
28
|
+
- debugging connection lifecycle behavior
|
|
29
|
+
- deciding whether an extension belongs in `socketNamespace`, `socketConnection`, or `socketPacket`
|
|
30
|
+
- tracing authentication, origin checks, instance initialization, or packet execution from source
|
|
31
|
+
- verifying how local delivery and cross-worker broadcast fit together
|
|
32
|
+
|
|
33
|
+
## Key source files
|
|
34
|
+
|
|
35
|
+
Use these files as the primary trace surface:
|
|
36
|
+
|
|
37
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/monkey.ts`
|
|
38
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/service/socket.ts`
|
|
39
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/service/socketEvent.ts`
|
|
40
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/bean.socket.ts`
|
|
41
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketConnection.*.ts`
|
|
42
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketPacket.*.ts`
|
|
43
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/types/socketEvent.ts`
|
|
44
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/lib/const.ts`
|
|
45
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/hmr.socketConnection.ts`
|
|
46
|
+
- `vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/hmr.socketPacket.ts`
|
|
47
|
+
|
|
48
|
+
## Startup call flow
|
|
49
|
+
|
|
50
|
+
At backend startup, the call path is:
|
|
51
|
+
|
|
52
|
+
1. `Monkey.appReady()`
|
|
53
|
+
2. `ServiceSocket.appReady()`
|
|
54
|
+
3. create `WebSocketServer`
|
|
55
|
+
4. register the `connection` listener
|
|
56
|
+
|
|
57
|
+
Representative shape:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
async appReady() {
|
|
61
|
+
if (!this.app.server) return;
|
|
62
|
+
this.app.wss = new WebSocketServer({ server: this.app.server });
|
|
63
|
+
this.app.wss.on('connection', (ws, req) => {
|
|
64
|
+
this._onConnection(ws, req);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A practical interpretation is:
|
|
70
|
+
|
|
71
|
+
- monkey lifecycle integration connects `a-socket` to ordinary backend startup
|
|
72
|
+
- `a-socket` does not create a separate standalone server
|
|
73
|
+
- the Web Socket server is attached to the existing backend HTTP server
|
|
74
|
+
|
|
75
|
+
## Connection setup call flow
|
|
76
|
+
|
|
77
|
+
When a client connects, the main path is:
|
|
78
|
+
|
|
79
|
+
1. `WebSocketServer` emits `connection`
|
|
80
|
+
2. `ServiceSocket._onConnection(ws, req)` starts
|
|
81
|
+
3. reject immediately if the app is already closing
|
|
82
|
+
4. parse the request URL
|
|
83
|
+
5. read query values for instance, locale, and timezone
|
|
84
|
+
6. create a request-scoped backend context through `app.bean.executor.newCtx(...)`
|
|
85
|
+
7. define `ctx.ws`
|
|
86
|
+
8. derive `ws.namespace`
|
|
87
|
+
9. assign `ws.id`
|
|
88
|
+
10. add the client to `bean.socket`
|
|
89
|
+
11. execute the connection onion chain with `{ method: 'enter', ws }`
|
|
90
|
+
12. install `onclose`, `onmessage`, and `onerror`
|
|
91
|
+
13. send `sysReady`
|
|
92
|
+
|
|
93
|
+
## Connection setup as a source trace
|
|
94
|
+
|
|
95
|
+
A compact trace looks like this:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
Monkey.appReady()
|
|
99
|
+
-> ServiceSocket.appReady()
|
|
100
|
+
-> new WebSocketServer({ server: app.server })
|
|
101
|
+
-> wss.on('connection', (ws, req) => _onConnection(ws, req))
|
|
102
|
+
|
|
103
|
+
connection
|
|
104
|
+
-> ServiceSocket._onConnection(ws, req)
|
|
105
|
+
-> URL.parse(req.url, ...)
|
|
106
|
+
-> app.bean.executor.newCtx(...)
|
|
107
|
+
-> define ctx.ws getter
|
|
108
|
+
-> ws.namespace = getNamespace()
|
|
109
|
+
-> ws.id = uuidv4()
|
|
110
|
+
-> bean.socket.addClient(ws)
|
|
111
|
+
-> _getComposeSocketConnections(ws.namespace)({ method: 'enter', ws })
|
|
112
|
+
-> install ws.onclose / ws.onmessage / ws.onerror
|
|
113
|
+
-> ws.sendEvent('sysReady')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This is the first source path to read when a connection is accepted but later behavior is not what you expect.
|
|
117
|
+
|
|
118
|
+
## Namespace resolution call flow
|
|
119
|
+
|
|
120
|
+
Namespace identity is derived inside `ServiceSocket.getNamespace()`.
|
|
121
|
+
|
|
122
|
+
The flow is:
|
|
123
|
+
|
|
124
|
+
1. read `this.ctx.path`
|
|
125
|
+
2. remove the configured global prefix
|
|
126
|
+
3. if the remaining path is empty, use `/`
|
|
127
|
+
4. cast the result to a namespace key
|
|
128
|
+
|
|
129
|
+
A useful interpretation is:
|
|
130
|
+
|
|
131
|
+
- transport path and logical namespace are intentionally coupled
|
|
132
|
+
- namespace selection happens before packet handling
|
|
133
|
+
- namespace-specific socket behavior is therefore determined at connection time
|
|
134
|
+
|
|
135
|
+
Representative mapping:
|
|
136
|
+
|
|
137
|
+
- `/ws` -> `/`
|
|
138
|
+
- `/ws/ssrhmr` -> `/ssrhmr`
|
|
139
|
+
|
|
140
|
+
## Connection onion composition flow
|
|
141
|
+
|
|
142
|
+
The connection chain is not hardcoded as a manual list in `service/socket.ts`.
|
|
143
|
+
|
|
144
|
+
Instead, the flow is:
|
|
145
|
+
|
|
146
|
+
1. `ServiceSocket._getComposeSocketConnections(namespace)`
|
|
147
|
+
2. fetch the namespace cache from `getCacheSocketConnections(app)`
|
|
148
|
+
3. if the namespace is not cached:
|
|
149
|
+
- ask `this.bean.onion.socketConnection.getOnionsEnabledWrapped(...)` for enabled onion slices
|
|
150
|
+
- wrap each slice through `_wrapOnionConnection(...)`
|
|
151
|
+
- compose the wrapped handlers through `compose(...)`
|
|
152
|
+
4. execute the composed chain
|
|
153
|
+
|
|
154
|
+
Representative shape:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const connections = this.bean.onion.socketConnection.getOnionsEnabledWrapped(item => {
|
|
158
|
+
return this._wrapOnionConnection(item);
|
|
159
|
+
});
|
|
160
|
+
cacheSocketConnections[namespace] = compose(connections);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This matters because the runtime chain is framework-driven:
|
|
164
|
+
|
|
165
|
+
- enabled onion metadata determines what participates
|
|
166
|
+
- wrappers resolve the real bean instance from the container
|
|
167
|
+
- the composed result is cached per namespace
|
|
168
|
+
|
|
169
|
+
## Built-in connection onion execution order
|
|
170
|
+
|
|
171
|
+
The default built-in connection path is:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
alive -> app -> instance -> cors -> event -> passport -> ready
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The runtime enters each bean through:
|
|
178
|
+
|
|
179
|
+
- `beanInstance.enter(ws, options, next)` on connection setup
|
|
180
|
+
- `beanInstance.exit(ws, options, next)` on connection teardown
|
|
181
|
+
|
|
182
|
+
A practical responsibility map is:
|
|
183
|
+
|
|
184
|
+
- `alive` -> heartbeat and stale-socket detection
|
|
185
|
+
- `app` -> app-ready and closing checks
|
|
186
|
+
- `instance` -> instance initialization
|
|
187
|
+
- `cors` -> origin validation
|
|
188
|
+
- `event` -> attach `ws.sendEvent(...)`
|
|
189
|
+
- `passport` -> token or anonymous sign-in
|
|
190
|
+
- `ready` -> terminal ordered stage
|
|
191
|
+
|
|
192
|
+
## Packet composition flow
|
|
193
|
+
|
|
194
|
+
Inbound messages follow the same overall pattern as connection onions.
|
|
195
|
+
|
|
196
|
+
The runtime path is:
|
|
197
|
+
|
|
198
|
+
1. `ws.onmessage`
|
|
199
|
+
2. `ServiceSocket._getComposeSocketPackets(ws.namespace)`
|
|
200
|
+
3. fetch the namespace cache from `getCacheSocketPackets(app)`
|
|
201
|
+
4. if missing, load enabled packet onions, wrap them, and compose them
|
|
202
|
+
5. execute the composed chain with `{ data: event.data, ws }`
|
|
203
|
+
|
|
204
|
+
The packet wrapper calls:
|
|
205
|
+
|
|
206
|
+
- `beanInstance.execute(data.data, data.ws, options, _patchPacketNext(...))`
|
|
207
|
+
|
|
208
|
+
That patching step lets packet handlers pass a transformed packet onward while the composed chain keeps the socket attached.
|
|
209
|
+
|
|
210
|
+
## Built-in packet execution order
|
|
211
|
+
|
|
212
|
+
The default built-in packet path is:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
event -> performAction
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Step 1: event decoding
|
|
219
|
+
|
|
220
|
+
`SocketPacketEvent.execute(...)` checks whether the payload is an event-formatted string.
|
|
221
|
+
|
|
222
|
+
If so, it:
|
|
223
|
+
|
|
224
|
+
1. strips the configured prefix `'_:'`
|
|
225
|
+
2. parses JSON
|
|
226
|
+
3. reverse-maps short codes such as `_a`, `_b`, `_c`
|
|
227
|
+
4. forwards `[eventName, data]`
|
|
228
|
+
|
|
229
|
+
Otherwise it forwards:
|
|
230
|
+
|
|
231
|
+
- `[undefined, rawData]`
|
|
232
|
+
|
|
233
|
+
This is the source transition from raw transport data into normalized packet data.
|
|
234
|
+
|
|
235
|
+
### Step 2: perform-action handling
|
|
236
|
+
|
|
237
|
+
`SocketPacketPerformAction.execute(...)` checks whether the normalized event name is:
|
|
238
|
+
|
|
239
|
+
- `sysPerformAction`
|
|
240
|
+
|
|
241
|
+
If not, it calls `next()`.
|
|
242
|
+
|
|
243
|
+
If yes, it:
|
|
244
|
+
|
|
245
|
+
1. extracts compact request fields from the packet
|
|
246
|
+
2. calls `this.$scope.executor.service.executor.performActionInner(...)`
|
|
247
|
+
3. sends `sysPerformActionBack` on success or failure
|
|
248
|
+
|
|
249
|
+
A compact trace looks like this:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
ws.onmessage
|
|
253
|
+
-> composed socketPacket chain
|
|
254
|
+
-> SocketPacketEvent.execute(raw)
|
|
255
|
+
-> normalized packet [eventName, data]
|
|
256
|
+
-> SocketPacketPerformAction.execute(packet)
|
|
257
|
+
-> performActionInner(method, path, { query, body, headers })
|
|
258
|
+
-> ws.sendEvent('sysPerformActionBack', resultOrError)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Outbound send call flow
|
|
262
|
+
|
|
263
|
+
The simplest server-to-client path is direct send by client id.
|
|
264
|
+
|
|
265
|
+
The runtime path is:
|
|
266
|
+
|
|
267
|
+
1. some backend code calls `scope.socket.service.socketEvent.send(...)`
|
|
268
|
+
2. `ServiceSocketEvent.send(...)` calls `sendWorker(...)` locally
|
|
269
|
+
3. `sendWorker(...)` looks up the socket in `bean.socket.clients`
|
|
270
|
+
4. `ws.sendEvent(...)` serializes and sends the packet
|
|
271
|
+
5. `ServiceSocketEvent.send(...)` also emits the `send` broadcast bean for cross-worker propagation
|
|
272
|
+
|
|
273
|
+
A compact trace looks like this:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
caller
|
|
277
|
+
-> ServiceSocketEvent.send(id, eventName, data, options)
|
|
278
|
+
-> sendWorker(id, ...)
|
|
279
|
+
-> bean.socket.clients[id]?.sendEvent(...)
|
|
280
|
+
-> ws.send('_:' + JSON.stringify([eventNameInner, data]))
|
|
281
|
+
-> scope.broadcast.send.emit(...)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
This is why even a targeted send is not only a local socket write. It also has a distributed propagation path.
|
|
285
|
+
|
|
286
|
+
## Outbound broadcast call flow
|
|
287
|
+
|
|
288
|
+
Namespace broadcast follows the same two-level model.
|
|
289
|
+
|
|
290
|
+
The runtime path is:
|
|
291
|
+
|
|
292
|
+
1. some backend code calls `broadcast(namespace, eventName, data, options)`
|
|
293
|
+
2. `ServiceSocketEvent.broadcast(...)` calls `broadcastWorker(...)` locally
|
|
294
|
+
3. `broadcastWorker(...)` reads client ids from `bean.socket.clientsNamespace[namespace]`
|
|
295
|
+
4. each local client sends through `ws.sendEvent(...)`
|
|
296
|
+
5. `ServiceSocketEvent.broadcast(...)` emits the `broadcast` bean for other workers
|
|
297
|
+
|
|
298
|
+
A compact trace looks like this:
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
caller
|
|
302
|
+
-> ServiceSocketEvent.broadcast(namespace, eventName, data, options)
|
|
303
|
+
-> broadcastWorker(namespace, ...)
|
|
304
|
+
-> ids = bean.socket.clientsNamespace[namespace]
|
|
305
|
+
-> for each id: bean.socket.clients[id]?.sendEvent(...)
|
|
306
|
+
-> scope.broadcast.broadcast.emit(...)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
A practical interpretation is:
|
|
310
|
+
|
|
311
|
+
- local clients receive the event immediately
|
|
312
|
+
- other workers replay the same delivery logic for their local namespace members
|
|
313
|
+
- `a-socket` reuses the distributed broadcast layer instead of inventing a separate cross-worker socket bus
|
|
314
|
+
|
|
315
|
+
## Namespace-bean call flow
|
|
316
|
+
|
|
317
|
+
Most application code should not call low-level socket registries directly.
|
|
318
|
+
|
|
319
|
+
The usual path is a namespace bean extending `BeanSocketNamespaceBase`.
|
|
320
|
+
|
|
321
|
+
That path is:
|
|
322
|
+
|
|
323
|
+
1. define a namespace bean with `@SocketNamespace(...)`
|
|
324
|
+
2. call `send(...)` or `broadcast(...)` on that bean
|
|
325
|
+
3. `BeanSocketNamespaceBase` forwards to `ServiceSocketEvent`
|
|
326
|
+
4. `ServiceSocketEvent` performs local delivery and broadcast propagation
|
|
327
|
+
|
|
328
|
+
Representative trace:
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
BeanSsrHmr.reload()
|
|
332
|
+
-> this.scope.socketNamespace.ssrHmr.broadcast('reload')
|
|
333
|
+
-> BeanSocketNamespaceBase.broadcast(...)
|
|
334
|
+
-> this.$scope.socket.service.socketEvent.broadcast(namespace, ...)
|
|
335
|
+
-> local namespace delivery
|
|
336
|
+
-> broadcast fan-out to other workers
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
This is the normal extension path for feature modules.
|
|
340
|
+
|
|
341
|
+
## Disconnect call flow
|
|
342
|
+
|
|
343
|
+
When a connection closes, the runtime path is:
|
|
344
|
+
|
|
345
|
+
1. `ws.onclose`
|
|
346
|
+
2. run the connection onion chain again with `{ method: 'exit', ws }`
|
|
347
|
+
3. remove the client from the registry in `bean.socket.removeClient(ws)`
|
|
348
|
+
4. resolve the connection promise
|
|
349
|
+
|
|
350
|
+
Representative trace:
|
|
351
|
+
|
|
352
|
+
```text
|
|
353
|
+
ws.onclose
|
|
354
|
+
-> _getComposeSocketConnections(ws.namespace)({ method: 'exit', ws })
|
|
355
|
+
-> bean.socket.removeClient(ws)
|
|
356
|
+
-> resolve()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This matters because cleanup is not only a transport event. It also reuses the onion lifecycle so custom connection beans can attach exit-time behavior.
|
|
360
|
+
|
|
361
|
+
## Shutdown call flow
|
|
362
|
+
|
|
363
|
+
At backend shutdown, the path is:
|
|
364
|
+
|
|
365
|
+
1. `Monkey.appClose()`
|
|
366
|
+
2. `ServiceSocket.appClose()`
|
|
367
|
+
3. `app.wss.close()`
|
|
368
|
+
4. `bean.socket.close()`
|
|
369
|
+
5. terminate all tracked clients
|
|
370
|
+
|
|
371
|
+
A compact trace looks like this:
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
Monkey.appClose()
|
|
375
|
+
-> ServiceSocket.appClose()
|
|
376
|
+
-> app.wss.close()
|
|
377
|
+
-> bean.socket.close()
|
|
378
|
+
-> for each client: terminate()
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
This keeps runtime shutdown and socket cleanup aligned.
|
|
382
|
+
|
|
383
|
+
## HMR and cache invalidation flow
|
|
384
|
+
|
|
385
|
+
`a-socket` caches composed connection and packet chains in `app.meta` through:
|
|
386
|
+
|
|
387
|
+
- `SymbolCacheSocketConnections`
|
|
388
|
+
- `SymbolCacheSocketPackets`
|
|
389
|
+
|
|
390
|
+
The related flow is:
|
|
391
|
+
|
|
392
|
+
1. first connection or packet execution composes and caches the chain
|
|
393
|
+
2. later traffic reuses the cached composed function
|
|
394
|
+
3. HMR reload triggers `HmrSocketConnection.reload(...)` or `HmrSocketPacket.reload(...)`
|
|
395
|
+
4. those HMR beans clear the corresponding cache from `app.meta`
|
|
396
|
+
5. the next execution rebuilds the composed chain from current metadata
|
|
397
|
+
|
|
398
|
+
A compact trace looks like this:
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
first use
|
|
402
|
+
-> _getComposeSocketConnections(namespace)
|
|
403
|
+
-> getCacheSocketConnections(app)
|
|
404
|
+
-> compose(...) and cache
|
|
405
|
+
|
|
406
|
+
HMR reload
|
|
407
|
+
-> HmrSocketConnection.reload(...)
|
|
408
|
+
-> clearAllCacheSocketConnections(app)
|
|
409
|
+
|
|
410
|
+
next use
|
|
411
|
+
-> rebuild composed chain
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
This is the important source path when onion changes appear stale during development.
|
|
415
|
+
|
|
416
|
+
## When to trace which path
|
|
417
|
+
|
|
418
|
+
Use these call paths depending on the problem you are investigating:
|
|
419
|
+
|
|
420
|
+
- startup problem -> `monkey.ts` and `ServiceSocket.appReady()`
|
|
421
|
+
- connection rejected too early -> `_onConnection(...)` plus `app`, `instance`, `cors`, and `passport`
|
|
422
|
+
- client never receives ready -> `event` connection onion plus `sysReady`
|
|
423
|
+
- inbound action fails -> `socketPacket.event.ts`, `socketPacket.performAction.ts`, and `performActionInner(...)`
|
|
424
|
+
- targeted delivery fails -> `ServiceSocketEvent.send(...)` and `bean.socket.clients`
|
|
425
|
+
- namespace broadcast fails -> `ServiceSocketEvent.broadcast(...)` and `clientsNamespace`
|
|
426
|
+
- development reload seems stale -> `lib/const.ts` and the HMR beans
|
|
427
|
+
|
|
428
|
+
## Related guides
|
|
429
|
+
|
|
430
|
+
If you need the broader context next, read:
|
|
431
|
+
|
|
432
|
+
- [Web Socket Guide](/backend/websocket-guide) for the architecture and extension model
|
|
433
|
+
- [Web Socket Usage Guide](/backend/websocket-usage-guide) for server-side authoring patterns
|
|
434
|
+
- [Web Socket Protocol Guide](/backend/websocket-protocol-guide) for the client-visible wire format
|
|
435
|
+
- [Broadcast Guide](/backend/broadcast-guide) and [Worker Guide](/backend/worker-guide) for cross-worker delivery context
|