@rlanz/socket 0.0.1-6 → 0.0.1-7
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/README.md +164 -149
- package/build/{types-DiYaFvgi.d.ts → base_channel-Dr40l7CW.d.ts} +174 -169
- package/build/chunk-2X6WGCH5.js +120 -0
- package/build/chunk-2X6WGCH5.js.map +1 -0
- package/build/chunk-6PJ4ALJD.js +115 -0
- package/build/chunk-6PJ4ALJD.js.map +1 -0
- package/build/chunk-E6ILCRL3.js +182 -0
- package/build/chunk-E6ILCRL3.js.map +1 -0
- package/build/{chunk-SHH6U4CI.js → chunk-G75J233Z.js} +1 -1
- package/build/chunk-G75J233Z.js.map +1 -0
- package/build/{chunk-B4Y3TNDI.js → chunk-IUQ3GYZZ.js} +5 -24
- package/build/chunk-IUQ3GYZZ.js.map +1 -0
- package/build/chunk-QNOOQXYY.js +899 -0
- package/build/chunk-QNOOQXYY.js.map +1 -0
- package/build/{chunk-FVPY6HZW.js → chunk-YVC7IMDN.js} +21 -5
- package/build/chunk-YVC7IMDN.js.map +1 -0
- package/build/framework-B-4kGmAw.d.ts +13 -0
- package/build/index-Fg-tkHhP.d.ts +150 -0
- package/build/index.d.ts +23 -12
- package/build/index.js +12 -14
- package/build/index.js.map +1 -1
- package/build/providers/socket_provider.d.ts +2 -2
- package/build/providers/socket_provider.js +468 -215
- package/build/providers/socket_provider.js.map +1 -1
- package/build/services/socket.d.ts +2 -2
- package/build/{socket_service-D3jKrleE.d.ts → socket_service-BYEB8LIN.d.ts} +7 -7
- package/build/src/assembler_hook.js +33 -59
- package/build/src/assembler_hook.js.map +1 -1
- package/build/src/client/index.d.ts +3 -147
- package/build/src/client/index.js +4 -837
- package/build/src/client/index.js.map +1 -1
- package/build/src/client/react.d.ts +13 -5
- package/build/src/client/react.js +27 -11
- package/build/src/client/react.js.map +1 -1
- package/build/src/client/types.d.ts +30 -36
- package/build/src/client/vue.d.ts +17 -6
- package/build/src/client/vue.js +31 -13
- package/build/src/client/vue.js.map +1 -1
- package/build/src/decorators.d.ts +3 -2
- package/build/src/decorators.js +1 -1
- package/build/src/health_check.d.ts +2 -2
- package/build/src/health_check.js +14 -3
- package/build/src/health_check.js.map +1 -1
- package/build/src/otel.js +0 -1
- package/build/src/otel.js.map +1 -1
- package/build/src/testing.d.ts +3 -12
- package/build/src/testing.js +1 -1
- package/build/src/types/tracing_channels.d.ts +1 -2
- package/build/src/types.d.ts +1 -1
- package/package.json +2 -9
- package/build/chunk-B4Y3TNDI.js.map +0 -1
- package/build/chunk-FVPY6HZW.js.map +0 -1
- package/build/chunk-HK7Z65DA.js +0 -19
- package/build/chunk-HK7Z65DA.js.map +0 -1
- package/build/chunk-SFAY2ZA4.js +0 -28
- package/build/chunk-SFAY2ZA4.js.map +0 -1
- package/build/chunk-SHH6U4CI.js.map +0 -1
- package/build/chunk-YAX5EHHB.js +0 -453
- package/build/chunk-YAX5EHHB.js.map +0 -1
- package/build/framework-DuW6zpPk.d.ts +0 -14
- package/build/src/client/svelte.d.ts +0 -23
- package/build/src/client/svelte.js +0 -82
- package/build/src/client/svelte.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,12 +8,24 @@ WebSocket integration for AdonisJS, powered by [`ws`](https://github.com/websock
|
|
|
8
8
|
yarn add @rlanz/socket
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Configure the package in your AdonisJS application:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
node ace configure @rlanz/socket
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This registers the provider for the `web` environment only, so Ace commands do not boot the
|
|
18
|
+
WebSocket integration, and installs the Assembler hook:
|
|
12
19
|
|
|
13
20
|
```ts
|
|
14
21
|
// adonisrc.ts
|
|
15
22
|
export default defineConfig({
|
|
16
|
-
providers: [
|
|
23
|
+
providers: [
|
|
24
|
+
{
|
|
25
|
+
file: () => import('@rlanz/socket/provider'),
|
|
26
|
+
environment: ['web'],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
17
29
|
hooks: {
|
|
18
30
|
init: [() => import('@rlanz/socket/assembler_hook')],
|
|
19
31
|
},
|
|
@@ -21,10 +33,11 @@ export default defineConfig({
|
|
|
21
33
|
```
|
|
22
34
|
|
|
23
35
|
The Assembler hook generates both the application registry used for end-to-end client types and the
|
|
24
|
-
server manifest consumed by the provider
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`#generated
|
|
36
|
+
server manifest consumed by the provider from the same channel file list. The client registry is a
|
|
37
|
+
typed subset: channels whose inheritance, pattern, or handlers cannot be represented statically are
|
|
38
|
+
still registered at runtime but are omitted from generated client types with a diagnostic. Keep the
|
|
39
|
+
standard AdonisJS package import mapping `#generated/*` pointed at `./.adonisjs/server/*.js`; the
|
|
40
|
+
provider loads `#generated/socket_channels` through that convention.
|
|
28
41
|
|
|
29
42
|
## Configuration
|
|
30
43
|
|
|
@@ -37,7 +50,9 @@ import { defineConfig } from '@rlanz/socket'
|
|
|
37
50
|
export default defineConfig({
|
|
38
51
|
websocket: {
|
|
39
52
|
path: '/socket',
|
|
40
|
-
|
|
53
|
+
origin: (origin, ctx) => {
|
|
54
|
+
return ['https://app.example.com', 'https://admin.example.com'].includes(origin)
|
|
55
|
+
},
|
|
41
56
|
pingInterval: '25s',
|
|
42
57
|
pingTimeout: '5s',
|
|
43
58
|
maxBufferedAmount: 16 * 1024 * 1024,
|
|
@@ -49,9 +64,12 @@ export default defineConfig({
|
|
|
49
64
|
```
|
|
50
65
|
|
|
51
66
|
- `websocket.path` defaults to `/socket`.
|
|
52
|
-
- Browser upgrades are restricted to the request's own origin by default.
|
|
53
|
-
|
|
54
|
-
an `
|
|
67
|
+
- Browser upgrades are restricted to the request's own origin by default. `websocket.origin`
|
|
68
|
+
accepts the same values as AdonisJS CORS `origin`: a boolean, a string (including `'*'` or a
|
|
69
|
+
comma-separated list), an array of strings, or `(origin, httpContext) => value`. Matching is
|
|
70
|
+
case-sensitive and, like `@adonisjs/cors`, comma-separated entries are not whitespace-trimmed. A
|
|
71
|
+
configured policy replaces the same-origin default. Clients without an `Origin` header remain
|
|
72
|
+
supported.
|
|
55
73
|
- `websocket.middleware` runs AdonisJS HTTP middleware for the WebSocket upgrade request.
|
|
56
74
|
- `websocket.pingInterval` sends a WebSocket ping every configured duration.
|
|
57
75
|
- `websocket.pingTimeout` closes connections that do not answer in time.
|
|
@@ -79,7 +97,7 @@ Fetch CORS response checks to WebSockets. The origin check therefore runs before
|
|
|
79
97
|
and authentication, which protects cookie- or session-authenticated sockets from cross-site
|
|
80
98
|
WebSocket hijacking. A reverse proxy must preserve the public `Host` and overwrite
|
|
81
99
|
`X-Forwarded-Proto` with the trusted public protocol. If it rewrites either value, add the public
|
|
82
|
-
origin explicitly
|
|
100
|
+
origin explicitly with `websocket.origin` instead of relying on implicit same-origin
|
|
83
101
|
detection.
|
|
84
102
|
|
|
85
103
|
### Authentication
|
|
@@ -101,7 +119,7 @@ import { authenticateWithAdonisAuth, defineConfig } from '@rlanz/socket'
|
|
|
101
119
|
|
|
102
120
|
type User = { id: string | number; name: string }
|
|
103
121
|
|
|
104
|
-
export default defineConfig
|
|
122
|
+
export default defineConfig({
|
|
105
123
|
websocket: {
|
|
106
124
|
middleware: [
|
|
107
125
|
() => import('@adonisjs/session/session_middleware'),
|
|
@@ -114,8 +132,10 @@ export default defineConfig<User>({
|
|
|
114
132
|
```
|
|
115
133
|
|
|
116
134
|
Return `false`, `null`, or throw to reject the upgrade with `401 Unauthorized`. The returned user is
|
|
117
|
-
available as `socket.user`.
|
|
118
|
-
|
|
135
|
+
available as `socket.user`.
|
|
136
|
+
|
|
137
|
+
Every connected socket retains the initial Adonis HTTP context at `socket.raw.httpContext`; use
|
|
138
|
+
`socket.raw.data` for mutable socket-lifetime state.
|
|
119
139
|
|
|
120
140
|
The retained HTTP context describes the initial upgrade handshake. After the `101` response, it is
|
|
121
141
|
not an active HTTP request: do not write through `httpContext.response` or expect it to remain
|
|
@@ -136,6 +156,12 @@ async authenticate({ httpContext }) {
|
|
|
136
156
|
|
|
137
157
|
Configure an [`@boringnode/bus`](https://github.com/boringnode/bus) transport to synchronize broadcasts across multiple application instances.
|
|
138
158
|
|
|
159
|
+
Install the peer dependency required by the selected transport. For Redis:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
yarn add ioredis
|
|
163
|
+
```
|
|
164
|
+
|
|
139
165
|
```ts
|
|
140
166
|
// config/socket.ts
|
|
141
167
|
import { redis } from '@boringnode/bus/transports/redis'
|
|
@@ -149,14 +175,22 @@ export default defineConfig({
|
|
|
149
175
|
}),
|
|
150
176
|
channel: 'socket::broadcast',
|
|
151
177
|
presenceTimeout: '100ms',
|
|
178
|
+
retryQueue: {
|
|
179
|
+
maxSize: 1000,
|
|
180
|
+
},
|
|
152
181
|
},
|
|
153
182
|
})
|
|
154
183
|
```
|
|
155
184
|
|
|
156
|
-
When configured, `socket.to(channel).emit(...)`, `socket.to(channel).except(socketId).emit(...)`,
|
|
185
|
+
When configured, `socket.to(channel).emit(...)`, `socket.to(channel).except(socketId).emit(...)`, and `socket.broadcast(...)` are delivered locally and published to the bus so other instances can deliver them to their own connected sockets.
|
|
157
186
|
|
|
158
187
|
The default bus channel is `socket::broadcast`. Presence channels also use the bus to build distributed snapshots during subscribe, duplicate subscribe, join, and leave updates. `presenceTimeout` controls how long an instance waits for other instances to answer a presence snapshot request; it defaults to `100ms`.
|
|
159
188
|
|
|
189
|
+
Failed publications enter an in-memory retry queue. It is enabled by default, deduplicates identical
|
|
190
|
+
messages, and retains at most `1000` entries; when full, the oldest entry is discarded. Configure
|
|
191
|
+
`transport.retryQueue` to change the limit, retry interval, deduplication, or to disable retries.
|
|
192
|
+
Setting `maxSize: null` explicitly opts into an unbounded queue.
|
|
193
|
+
|
|
160
194
|
### Health Checks
|
|
161
195
|
|
|
162
196
|
Register `SocketHealthCheck` inside your AdonisJS readiness checks to report whether the WebSocket service is ready to accept traffic.
|
|
@@ -203,22 +237,19 @@ test.group('Notifications', (group) => {
|
|
|
203
237
|
|
|
204
238
|
The fake supports explicit resource management as well, so `using fake = socket.fake()` automatically restores the real socket service when the test scope exits.
|
|
205
239
|
|
|
206
|
-
|
|
240
|
+
Five assertion helpers cover global broadcasts, channel events, counts, and negative assertions.
|
|
207
241
|
|
|
208
242
|
```ts
|
|
209
243
|
fake.assertBroadcasted('maintenance', { data: { active: true } })
|
|
210
|
-
fake.assertNotBroadcasted('
|
|
244
|
+
fake.assertNotBroadcasted('deploy:started')
|
|
211
245
|
|
|
212
246
|
fake.assertEmittedTo('chat/general', 'chat:message', {
|
|
213
247
|
data: (data) => data.text === 'Hello',
|
|
214
248
|
})
|
|
215
249
|
fake.assertNotEmittedTo('chat/general', 'chat:typing')
|
|
216
250
|
|
|
217
|
-
fake.
|
|
218
|
-
fake.
|
|
219
|
-
|
|
220
|
-
fake.assertEmittedCount(2, { target: 'channel', channel: 'chat/general' })
|
|
221
|
-
fake.assertNothingEmitted()
|
|
251
|
+
fake.assertCount(2, { target: 'channel', channel: 'chat/general' })
|
|
252
|
+
fake.assertCount(0)
|
|
222
253
|
```
|
|
223
254
|
|
|
224
255
|
### OpenTelemetry
|
|
@@ -258,7 +289,7 @@ At the reverse proxy or load balancer:
|
|
|
258
289
|
`X-Forwarded-Proto` value, so client-supplied versions of those headers must not reach the app.
|
|
259
290
|
|
|
260
291
|
Browser origins are not authorized by AdonisJS CORS middleware. Configure exact public origins in
|
|
261
|
-
[`websocket.
|
|
292
|
+
[`websocket.origin`](#configuration) when the browser origin differs from the public socket
|
|
262
293
|
origin or when the proxy rewrites `Host`. Clients without an `Origin` header are accepted, so use
|
|
263
294
|
upgrade authentication for non-browser clients rather than treating the origin check as
|
|
264
295
|
authentication.
|
|
@@ -267,7 +298,7 @@ authentication.
|
|
|
267
298
|
|
|
268
299
|
A load balancer may route each new upgrade to any ready instance and must keep that upgraded TCP
|
|
269
300
|
connection attached to that instance. The package neither implements nor guarantees sticky
|
|
270
|
-
sessions. Without a [transport](#horizontal-sync), subscriptions,
|
|
301
|
+
sessions. Without a [transport](#horizontal-sync), subscriptions, broadcasts, and
|
|
271
302
|
presence are local to one instance; affinity is not a replacement for configuring the transport.
|
|
272
303
|
All instances in one logical deployment must use a compatible shared transport and the same bus
|
|
273
304
|
channel.
|
|
@@ -282,10 +313,11 @@ large fleet could reconnect simultaneously.
|
|
|
282
313
|
### Transport, presence, and delivery semantics
|
|
283
314
|
|
|
284
315
|
Local broadcasts are sent to currently connected local recipients and are also published to the
|
|
285
|
-
configured `@boringnode/bus` transport. Publication is fire-and-forget from the socket API.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
316
|
+
configured `@boringnode/bus` transport. Publication is fire-and-forget from the socket API. Failed
|
|
317
|
+
publications may be retried from the bounded, in-memory retry queue after the transport reconnects,
|
|
318
|
+
so stale events remain possible. The package does not persist bus messages, wait for remote
|
|
319
|
+
delivery, acknowledge recipients, or recover events after a process restart; the guarantees of a
|
|
320
|
+
particular transport do not turn socket delivery into a durable application queue.
|
|
289
321
|
|
|
290
322
|
Distributed presence is also best-effort and non-durable. A snapshot contains the replies received
|
|
291
323
|
before `transport.presenceTimeout`; partitions, slow instances, concurrent changes, and restarts can
|
|
@@ -311,7 +343,7 @@ Review the limits under [Configuration](#configuration) against real payloads an
|
|
|
311
343
|
|
|
312
344
|
- enable heartbeat explicitly; it is disabled when neither `pingInterval` nor `pingTimeout` is set;
|
|
313
345
|
- tune inbound/outbound payload sizes, the per-socket fixed-window message rate, serialized message
|
|
314
|
-
queue depth, subscriptions per socket,
|
|
346
|
+
queue depth, subscriptions per socket, channel-name length, and the transport retry queue;
|
|
315
347
|
- treat rate and queue limits as per-connection safeguards, not perimeter abuse prevention; enforce
|
|
316
348
|
aggregate connection and request limits at the proxy or application boundary; and
|
|
317
349
|
- tune `maxBufferedAmount` for available memory. Slow consumers and oversized outbound frames are
|
|
@@ -341,7 +373,7 @@ counters count immediate sends to local `ws` objects, not application-level rece
|
|
|
341
373
|
|
|
342
374
|
- [ ] Proxy upgrades on the configured path with HTTP/1.1 `Upgrade`/`Connection` headers.
|
|
343
375
|
- [ ] Terminate TLS safely; preserve a validated public `Host` and overwrite `X-Forwarded-Proto`.
|
|
344
|
-
- [ ] Configure exact `
|
|
376
|
+
- [ ] Configure an exact `origin` policy and upgrade authentication; protect the upstream from direct
|
|
345
377
|
access.
|
|
346
378
|
- [ ] Set proxy idle timeouts and explicitly configure/test heartbeat behavior.
|
|
347
379
|
- [ ] Configure a shared transport and bus channel for every instance that must exchange broadcasts
|
|
@@ -359,11 +391,13 @@ counters count immediate sends to local `ws` objects, not application-level rece
|
|
|
359
391
|
|
|
360
392
|
Channels are discovered from `app/channels/**/*_channel.{ts,js}`. Export a default class extending
|
|
361
393
|
`BaseChannel`. Client-to-server payloads and acknowledgements are inferred from public handler
|
|
362
|
-
methods. The optional second generic declares server-to-client events
|
|
394
|
+
methods. The optional second generic declares server-to-client events. When omitted, the channel
|
|
395
|
+
cannot broadcast server events.
|
|
363
396
|
|
|
364
397
|
```ts
|
|
365
398
|
// app/channels/chat_channel.ts
|
|
366
399
|
import { BaseChannel } from '@rlanz/socket'
|
|
400
|
+
import { onMessage } from '@rlanz/socket/decorators'
|
|
367
401
|
import type { AuthenticatedSocket } from '@rlanz/socket/types'
|
|
368
402
|
|
|
369
403
|
type User = { id: string; name: string }
|
|
@@ -376,11 +410,7 @@ type ServerEvents = {
|
|
|
376
410
|
export default class ChatChannel extends BaseChannel<User, ServerEvents> {
|
|
377
411
|
static pattern = 'chat/:roomId'
|
|
378
412
|
|
|
379
|
-
|
|
380
|
-
'chat:send': this.sendMessage,
|
|
381
|
-
'chat:ping': this.ping,
|
|
382
|
-
}
|
|
383
|
-
|
|
413
|
+
@onMessage('chat:send')
|
|
384
414
|
async sendMessage(
|
|
385
415
|
socket: AuthenticatedSocket<User>,
|
|
386
416
|
payload: { text: string }
|
|
@@ -390,22 +420,35 @@ export default class ChatChannel extends BaseChannel<User, ServerEvents> {
|
|
|
390
420
|
return message
|
|
391
421
|
}
|
|
392
422
|
|
|
423
|
+
@onMessage('chat:ping')
|
|
393
424
|
ping(_socket: AuthenticatedSocket<User>, _payload: undefined): void {}
|
|
394
425
|
}
|
|
395
426
|
```
|
|
396
427
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
client event map is required. `ServerEvents` remains explicit because generation does not scan
|
|
400
|
-
`broadcast()` calls; it checks `broadcast()` and `broadcastExcept()`. Omitting it preserves the broad
|
|
401
|
-
legacy broadcast API:
|
|
428
|
+
Channel instances are resolved through the AdonisJS container for every new subscription, so
|
|
429
|
+
constructor injection works as it does for other AdonisJS classes:
|
|
402
430
|
|
|
403
431
|
```ts
|
|
404
|
-
|
|
405
|
-
|
|
432
|
+
import { inject } from '@adonisjs/core'
|
|
433
|
+
import { BaseChannel } from '@rlanz/socket'
|
|
434
|
+
import MessageService from '#services/message_service'
|
|
435
|
+
|
|
436
|
+
@inject()
|
|
437
|
+
export default class ChatChannel extends BaseChannel {
|
|
438
|
+
static pattern = 'chat/:roomId'
|
|
439
|
+
|
|
440
|
+
constructor(private messages: MessageService) {
|
|
441
|
+
super()
|
|
442
|
+
}
|
|
406
443
|
}
|
|
407
444
|
```
|
|
408
445
|
|
|
446
|
+
The event name comes from `@onMessage('event')`, the handler's second parameter becomes the client
|
|
447
|
+
payload, and `Awaited<ReturnType<handler>>` becomes its acknowledgement. No duplicate client event
|
|
448
|
+
map is required. `ServerEvents` remains explicit because generation does not scan
|
|
449
|
+
`broadcast()` calls; it checks `broadcast()` and `broadcastExcept()`. Omitting it makes both methods
|
|
450
|
+
reject every event at compile time.
|
|
451
|
+
|
|
409
452
|
The Assembler hook generates an explicit `AppSocket` registry. Pass it once to the browser client:
|
|
410
453
|
|
|
411
454
|
```ts
|
|
@@ -413,7 +456,7 @@ import { Socket } from '@rlanz/socket/client'
|
|
|
413
456
|
import type { AppSocket } from '#generated/socket'
|
|
414
457
|
|
|
415
458
|
const socket = new Socket<AppSocket>()
|
|
416
|
-
const channel = socket.channel('chat
|
|
459
|
+
const channel = socket.channel('chat/:roomId', { roomId: 'general' })
|
|
417
460
|
|
|
418
461
|
channel.listen('chat:message', (message) => console.log(message.id))
|
|
419
462
|
channel.send('chat:ping')
|
|
@@ -422,15 +465,37 @@ const message = await channel.sendWithAck('chat:send', { text: 'Hello' })
|
|
|
422
465
|
console.log(message.id)
|
|
423
466
|
```
|
|
424
467
|
|
|
468
|
+
The generated registry is a map keyed by each declared channel pattern. The client selects that
|
|
469
|
+
pattern explicitly and constructs the concrete name from typed parameters, so its types never need
|
|
470
|
+
to reproduce runtime route ordering or specificity. Every registry entry carries its generated
|
|
471
|
+
parameter shape; static channels use `undefined`, while dynamic channels expose their required,
|
|
472
|
+
optional, or wildcard parameters.
|
|
473
|
+
|
|
474
|
+
Client code can also extract event payloads without constructing a channel first:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
import type { ChannelBroadcastPayload, ChannelClientEventPayload } from '@rlanz/socket/client'
|
|
478
|
+
import type { AppSocket } from '#generated/socket'
|
|
479
|
+
|
|
480
|
+
type IncomingMessage = ChannelBroadcastPayload<AppSocket, 'chat/:roomId', 'chat:message'>
|
|
481
|
+
type OutgoingMessage = ChannelClientEventPayload<AppSocket, 'chat/:roomId', 'chat:send'>
|
|
482
|
+
```
|
|
483
|
+
|
|
425
484
|
The scanner recognizes default-exported classes that directly extend `BaseChannel`, direct
|
|
426
|
-
string-literal `static pattern` values, and
|
|
427
|
-
|
|
485
|
+
string-literal `static pattern` values, and `@onMessage('event')` imported from
|
|
486
|
+
`@rlanz/socket/decorators`. Handler methods must be
|
|
428
487
|
named, public instance methods so the generated registry can reference their parameter and return
|
|
429
|
-
types. Abstract channels,
|
|
430
|
-
|
|
488
|
+
types. Abstract channels, private/protected/static handlers, rest parameters, and required parameters
|
|
489
|
+
after the payload fail generation with a diagnostic. A handler may accept no
|
|
431
490
|
parameters, only the socket, a required payload, or an optional payload. Intermediate channel
|
|
432
|
-
inheritance is deliberately omitted from generated contracts
|
|
433
|
-
|
|
491
|
+
inheritance is deliberately omitted from generated contracts; runtime channel inheritance remains
|
|
492
|
+
valid.
|
|
493
|
+
|
|
494
|
+
Generated patterns support literal segments, required parameters, one final optional parameter, and
|
|
495
|
+
one final wildcard. Dynamic patterns always receive a parameter object; pass `{}` to omit a final
|
|
496
|
+
optional parameter. Wildcards use the explicit `wildcard` parameter and may contain slashes. Static
|
|
497
|
+
channels need no parameter object. An untyped `new Socket()` continues to accept raw concrete channel
|
|
498
|
+
names.
|
|
434
499
|
|
|
435
500
|
The default Assembler hook discovers `app/channels/**/*_channel.{ts,js}` and generates both
|
|
436
501
|
`.adonisjs/client/socket.ts` and `.adonisjs/server/socket_channels.ts`. Configure the frontend
|
|
@@ -454,7 +519,12 @@ export default generateSocketRegistry({
|
|
|
454
519
|
```ts
|
|
455
520
|
// adonisrc.ts
|
|
456
521
|
export default defineConfig({
|
|
457
|
-
providers: [
|
|
522
|
+
providers: [
|
|
523
|
+
{
|
|
524
|
+
file: () => import('@rlanz/socket/provider'),
|
|
525
|
+
environment: ['web'],
|
|
526
|
+
},
|
|
527
|
+
],
|
|
458
528
|
hooks: {
|
|
459
529
|
init: [() => import('./hooks/socket.js')],
|
|
460
530
|
},
|
|
@@ -462,39 +532,11 @@ export default defineConfig({
|
|
|
462
532
|
```
|
|
463
533
|
|
|
464
534
|
Generated matching supports literals, required parameters, a final optional parameter, and a final
|
|
465
|
-
wildcard. Dynamic or unsupported patterns are omitted
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
always come from the generated server manifest; the former `channels.patterns` runtime setting is no
|
|
471
|
-
longer supported and is rejected to prevent a second discovery source.
|
|
472
|
-
|
|
473
|
-
Legacy untyped channels remain supported without the generated generic:
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
// app/channels/chat_channel.ts
|
|
477
|
-
import { BaseChannel } from '@rlanz/socket'
|
|
478
|
-
import type { AuthenticatedSocket } from '@rlanz/socket/types'
|
|
479
|
-
|
|
480
|
-
type User = { id: string; name: string }
|
|
481
|
-
|
|
482
|
-
export default class ChatChannel extends BaseChannel<User> {
|
|
483
|
-
static pattern = 'chat/:roomId'
|
|
484
|
-
|
|
485
|
-
async onJoin(socket: AuthenticatedSocket<User>, roomId: string) {
|
|
486
|
-
this.broadcast('chat:system', {
|
|
487
|
-
message: `${socket.user?.name ?? 'Someone'} joined ${roomId}`,
|
|
488
|
-
})
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async onLeave(socket: AuthenticatedSocket<User>, roomId: string) {
|
|
492
|
-
this.broadcastExcept(socket.id, 'chat:system', {
|
|
493
|
-
message: `${socket.user?.name ?? 'Someone'} left ${roomId}`,
|
|
494
|
-
})
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
```
|
|
535
|
+
wildcard. Dynamic or unsupported patterns are omitted. Plain dynamic channel names, ambiguous
|
|
536
|
+
matches, and unmatched literals are rejected at compile time; the browser client must receive the
|
|
537
|
+
generated registry as `Socket<AppSocket>`. Generation provides compile-time types only and does not
|
|
538
|
+
validate runtime payloads. Whispers intentionally remain caller-generic because they are
|
|
539
|
+
peer-to-peer. Runtime channel files always come from the generated server manifest.
|
|
498
540
|
|
|
499
541
|
### Middleware
|
|
500
542
|
|
|
@@ -507,8 +549,7 @@ import type { MiddlewareContext } from '@rlanz/socket/types'
|
|
|
507
549
|
type User = { id: string; name: string }
|
|
508
550
|
|
|
509
551
|
async function auth(ctx: MiddlewareContext<User>, next: () => Promise<void>) {
|
|
510
|
-
|
|
511
|
-
await ctx.socket.joinUserRoom(user.id)
|
|
552
|
+
ctx.socket.getUserOrFail()
|
|
512
553
|
await next()
|
|
513
554
|
}
|
|
514
555
|
|
|
@@ -532,8 +573,12 @@ import { BaseChannel } from '@rlanz/socket'
|
|
|
532
573
|
import type { AuthenticatedSocket, PresenceInfo, PresenceMember } from '@rlanz/socket/types'
|
|
533
574
|
|
|
534
575
|
type User = { id: string; name: string }
|
|
576
|
+
type ServerEvents = {
|
|
577
|
+
'room:member_joined': PresenceMember
|
|
578
|
+
'room:member_left': PresenceMember
|
|
579
|
+
}
|
|
535
580
|
|
|
536
|
-
export default class RoomChannel extends BaseChannel<User> {
|
|
581
|
+
export default class RoomChannel extends BaseChannel<User, ServerEvents> {
|
|
537
582
|
static pattern = 'rooms/:roomId'
|
|
538
583
|
static options = { presence: true }
|
|
539
584
|
|
|
@@ -582,21 +627,23 @@ an exception from a later hook can be retried safely.
|
|
|
582
627
|
|
|
583
628
|
## Channel Messages
|
|
584
629
|
|
|
585
|
-
Incoming client messages are handled through the
|
|
630
|
+
Incoming client messages are handled through the `@onMessage` decorator.
|
|
631
|
+
Unknown events receive a negative acknowledgement.
|
|
586
632
|
|
|
587
633
|
```ts
|
|
588
634
|
import { BaseChannel } from '@rlanz/socket'
|
|
635
|
+
import { onMessage } from '@rlanz/socket/decorators'
|
|
589
636
|
import type { AuthenticatedSocket } from '@rlanz/socket/types'
|
|
590
637
|
|
|
591
638
|
type User = { id: string; name: string }
|
|
639
|
+
type ServerEvents = {
|
|
640
|
+
'chat:message': { user: User | undefined; body: string }
|
|
641
|
+
}
|
|
592
642
|
|
|
593
|
-
export default class ChatChannel extends BaseChannel<User> {
|
|
643
|
+
export default class ChatChannel extends BaseChannel<User, ServerEvents> {
|
|
594
644
|
static pattern = 'chat/:roomId'
|
|
595
645
|
|
|
596
|
-
|
|
597
|
-
'chat:send': this.sendMessage,
|
|
598
|
-
}
|
|
599
|
-
|
|
646
|
+
@onMessage('chat:send')
|
|
600
647
|
async sendMessage(socket: AuthenticatedSocket<User>, data: { body: string }) {
|
|
601
648
|
this.broadcast('chat:message', {
|
|
602
649
|
user: socket.user,
|
|
@@ -605,10 +652,6 @@ export default class ChatChannel extends BaseChannel<User> {
|
|
|
605
652
|
|
|
606
653
|
return { delivered: true }
|
|
607
654
|
}
|
|
608
|
-
|
|
609
|
-
async onMessage(socket: AuthenticatedSocket<User>, event: string, data: unknown) {
|
|
610
|
-
console.log('Unhandled channel message', event, data)
|
|
611
|
-
}
|
|
612
655
|
}
|
|
613
656
|
```
|
|
614
657
|
|
|
@@ -642,8 +685,9 @@ Import the browser client from `@rlanz/socket/client`.
|
|
|
642
685
|
|
|
643
686
|
```ts
|
|
644
687
|
import { Socket } from '@rlanz/socket/client'
|
|
688
|
+
import type { AppSocket } from '#generated/socket'
|
|
645
689
|
|
|
646
|
-
const socket = new Socket({
|
|
690
|
+
const socket = new Socket<AppSocket>({
|
|
647
691
|
url: 'http://localhost:3333',
|
|
648
692
|
path: '/socket',
|
|
649
693
|
autoReconnect: true,
|
|
@@ -665,7 +709,7 @@ await socket.connect()
|
|
|
665
709
|
### Subscribe, Listen, Send
|
|
666
710
|
|
|
667
711
|
```ts
|
|
668
|
-
const channel = socket.channel('chat
|
|
712
|
+
const channel = socket.channel('chat/:roomId', { roomId: 'general' })
|
|
669
713
|
|
|
670
714
|
channel
|
|
671
715
|
.here((users) => console.log('present users', users))
|
|
@@ -691,7 +735,7 @@ console.log(ack)
|
|
|
691
735
|
channel.stopListening('chat:message')
|
|
692
736
|
channel.stopListeningForWhisper('typing')
|
|
693
737
|
await channel.unsubscribe()
|
|
694
|
-
await socket.leave(
|
|
738
|
+
await socket.leave(channel.name)
|
|
695
739
|
socket.disconnect()
|
|
696
740
|
```
|
|
697
741
|
|
|
@@ -701,7 +745,7 @@ The client reconnects automatically by default. `reconnectDelay` is the initial
|
|
|
701
745
|
milliseconds, and each failed retry doubles the delay until `reconnectMaxDelay` is reached.
|
|
702
746
|
Set `autoReconnect: false` to disable reconnect attempts.
|
|
703
747
|
|
|
704
|
-
### React
|
|
748
|
+
### React and Vue
|
|
705
749
|
|
|
706
750
|
Framework adapters keep the generated `AppSocket` type at one application-level factory. Channel
|
|
707
751
|
names, client methods, server events, and payloads then remain inferred without repeating the
|
|
@@ -725,8 +769,13 @@ export const { SocketProvider, useChannel, useChannelEvent, useSocketState } =
|
|
|
725
769
|
</SocketProvider>
|
|
726
770
|
|
|
727
771
|
function Chat() {
|
|
728
|
-
const channel = useChannel('chat
|
|
729
|
-
useChannelEvent(
|
|
772
|
+
const channel = useChannel('chat/:roomId', { roomId: 'general' })
|
|
773
|
+
useChannelEvent(
|
|
774
|
+
'chat/:roomId',
|
|
775
|
+
{ roomId: 'general' },
|
|
776
|
+
'chat:message',
|
|
777
|
+
(message) => console.log(message.id)
|
|
778
|
+
)
|
|
730
779
|
|
|
731
780
|
return <button onClick={() => channel?.send('chat:send', { text: 'Hello' })}>Send</button>
|
|
732
781
|
}
|
|
@@ -748,63 +797,31 @@ export const { provideSocket, useChannel, useChannelEvent, useSocketState } =
|
|
|
748
797
|
provideSocket(socket, { owned: true })
|
|
749
798
|
|
|
750
799
|
// In a descendant component setup
|
|
751
|
-
const channel = useChannel('chat
|
|
752
|
-
useChannelEvent('chat
|
|
800
|
+
const channel = useChannel('chat/:roomId', { roomId: 'general' })
|
|
801
|
+
useChannelEvent('chat/:roomId', { roomId: 'general' }, 'chat:message', (message) =>
|
|
802
|
+
console.log(message.id)
|
|
803
|
+
)
|
|
753
804
|
|
|
754
805
|
function send() {
|
|
755
806
|
channel.value?.send('chat:send', { text: 'Hello' })
|
|
756
807
|
}
|
|
757
808
|
```
|
|
758
809
|
|
|
759
|
-
Svelte:
|
|
760
|
-
|
|
761
|
-
```ts
|
|
762
|
-
// src/socket.ts
|
|
763
|
-
import { Socket } from '@rlanz/socket/client'
|
|
764
|
-
import { createSocketContext } from '@rlanz/socket/client/svelte'
|
|
765
|
-
import type { AppSocket } from '#generated/socket'
|
|
766
|
-
|
|
767
|
-
export const socket = new Socket<AppSocket>()
|
|
768
|
-
export const { setSocket, useSocketState, channel, onChannelEvent } =
|
|
769
|
-
createSocketContext<AppSocket>()
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
```svelte
|
|
773
|
-
<!-- In the root component -->
|
|
774
|
-
<script lang="ts">
|
|
775
|
-
import { setSocket, socket } from './socket'
|
|
776
|
-
|
|
777
|
-
setSocket(socket, { owned: true })
|
|
778
|
-
</script>
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
```svelte
|
|
782
|
-
<!-- In a descendant component -->
|
|
783
|
-
<script lang="ts">
|
|
784
|
-
import { channel, onChannelEvent } from './socket'
|
|
785
|
-
|
|
786
|
-
const chat = channel('chat/general')
|
|
787
|
-
onChannelEvent('chat/general', 'chat:message', (message) => console.log(message.id))
|
|
788
|
-
</script>
|
|
789
|
-
|
|
790
|
-
<button on:click={() => $chat?.send('chat:send', { text: 'Hello' })}>Send</button>
|
|
791
|
-
```
|
|
792
|
-
|
|
793
810
|
Adapters acquire and share channel subscriptions, attach listeners before subscribing, and release
|
|
794
|
-
them when the component
|
|
795
|
-
|
|
796
|
-
|
|
811
|
+
them when the component or scope is disposed. React returns `null` until its passive effect acquires
|
|
812
|
+
the channel; Vue exposes a nullable shallow ref. Multiple consumers of the same channel share one
|
|
813
|
+
subscription, including across development
|
|
797
814
|
remounts, without removing listeners registered through the direct client API.
|
|
798
815
|
|
|
799
816
|
Sockets are borrowed by default: the adapter neither connects nor disconnects them. Pass `owned` to
|
|
800
|
-
`SocketProvider
|
|
801
|
-
|
|
802
|
-
|
|
817
|
+
`SocketProvider` or `{ owned: true }` to `provideSocket` only when that framework root exclusively
|
|
818
|
+
owns the socket lifecycle. Otherwise call `socket.connect()` and `socket.disconnect()` in
|
|
819
|
+
application-owned lifecycle code.
|
|
803
820
|
|
|
804
821
|
## Package Exports
|
|
805
822
|
|
|
806
823
|
```ts
|
|
807
|
-
import { BaseChannel,
|
|
824
|
+
import { BaseChannel, SocketResponseError, defineConfig } from '@rlanz/socket'
|
|
808
825
|
import type { AuthenticatedSocket, SocketConfig } from '@rlanz/socket/types'
|
|
809
826
|
import SocketProvider from '@rlanz/socket/provider'
|
|
810
827
|
import socket from '@rlanz/socket/services/main'
|
|
@@ -812,11 +829,10 @@ import { onMessage } from '@rlanz/socket/decorators'
|
|
|
812
829
|
import { SocketHealthCheck } from '@rlanz/socket/health_check'
|
|
813
830
|
import { SocketInstrumentation } from '@rlanz/socket/otel'
|
|
814
831
|
import { SocketFake } from '@rlanz/socket/testing'
|
|
815
|
-
import { Socket
|
|
832
|
+
import { Socket } from '@rlanz/socket/client'
|
|
816
833
|
import { generateSocketRegistry } from '@rlanz/socket/assembler_hook'
|
|
817
834
|
import { createSocketHooks } from '@rlanz/socket/client/react'
|
|
818
835
|
import { createSocketComposables } from '@rlanz/socket/client/vue'
|
|
819
|
-
import { createSocketContext } from '@rlanz/socket/client/svelte'
|
|
820
836
|
import type { SocketOptions, PresenceData } from '@rlanz/socket/client/types'
|
|
821
837
|
```
|
|
822
838
|
|
|
@@ -836,4 +852,3 @@ Available exports:
|
|
|
836
852
|
- `@rlanz/socket/client/types`
|
|
837
853
|
- `@rlanz/socket/client/react`
|
|
838
854
|
- `@rlanz/socket/client/vue`
|
|
839
|
-
- `@rlanz/socket/client/svelte`
|