@rlanz/socket 0.0.1-6 → 0.0.1-8
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 +186 -147
- package/build/{types-DiYaFvgi.d.ts → base_channel-DPOnqned.d.ts} +185 -171
- 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-K243S3QP.js +131 -0
- package/build/chunk-K243S3QP.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-B8pJ4cCA.d.ts +13 -0
- package/build/index-DjBYUhFR.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 +478 -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-BcC9ASCE.d.ts} +8 -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 +3 -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,59 +532,54 @@ 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
|
-
|
|
471
|
-
|
|
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.
|
|
540
|
+
|
|
541
|
+
### Middleware
|
|
472
542
|
|
|
473
|
-
|
|
543
|
+
Middlewares run before subscription. They may be functions, objects with a `handle` method, or
|
|
544
|
+
classes. Middleware classes are resolved through the AdonisJS container for every subscription, so
|
|
545
|
+
constructor injection works with `@inject()`. Classes in the same middleware chain share one
|
|
546
|
+
container resolver, matching AdonisJS HTTP middleware scoping.
|
|
474
547
|
|
|
475
548
|
```ts
|
|
476
|
-
// app/channels/chat_channel.ts
|
|
477
549
|
import { BaseChannel } from '@rlanz/socket'
|
|
478
|
-
import type {
|
|
550
|
+
import type { MiddlewareContext } from '@rlanz/socket/types'
|
|
479
551
|
|
|
480
552
|
type User = { id: string; name: string }
|
|
481
553
|
|
|
554
|
+
async function auth(ctx: MiddlewareContext<User>, next: () => Promise<void>) {
|
|
555
|
+
ctx.socket.getUserOrFail()
|
|
556
|
+
await next()
|
|
557
|
+
}
|
|
558
|
+
|
|
482
559
|
export default class ChatChannel extends BaseChannel<User> {
|
|
483
560
|
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
|
-
}
|
|
561
|
+
static middlewares = [auth]
|
|
496
562
|
}
|
|
497
563
|
```
|
|
498
564
|
|
|
499
|
-
### Middleware
|
|
500
|
-
|
|
501
|
-
Middlewares run before subscription. They may be functions or objects with a `handle` method.
|
|
502
|
-
|
|
503
565
|
```ts
|
|
504
|
-
import {
|
|
566
|
+
import { inject } from '@adonisjs/core'
|
|
505
567
|
import type { MiddlewareContext } from '@rlanz/socket/types'
|
|
568
|
+
import RoomService from '#services/room_service'
|
|
506
569
|
|
|
507
|
-
|
|
570
|
+
@inject()
|
|
571
|
+
class EnsureRoomAccess {
|
|
572
|
+
constructor(private rooms: RoomService) {}
|
|
508
573
|
|
|
509
|
-
async
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
574
|
+
async handle(ctx: MiddlewareContext<User>, next: () => Promise<void>) {
|
|
575
|
+
await this.rooms.authorize(ctx.socket.getUserOrFail(), ctx.params.roomId)
|
|
576
|
+
await next()
|
|
577
|
+
}
|
|
513
578
|
}
|
|
514
579
|
|
|
515
580
|
export default class ChatChannel extends BaseChannel<User> {
|
|
516
581
|
static pattern = 'chat/:roomId'
|
|
517
|
-
static middlewares = [
|
|
582
|
+
static middlewares = [EnsureRoomAccess]
|
|
518
583
|
}
|
|
519
584
|
```
|
|
520
585
|
|
|
@@ -532,8 +597,12 @@ import { BaseChannel } from '@rlanz/socket'
|
|
|
532
597
|
import type { AuthenticatedSocket, PresenceInfo, PresenceMember } from '@rlanz/socket/types'
|
|
533
598
|
|
|
534
599
|
type User = { id: string; name: string }
|
|
600
|
+
type ServerEvents = {
|
|
601
|
+
'room:member_joined': PresenceMember
|
|
602
|
+
'room:member_left': PresenceMember
|
|
603
|
+
}
|
|
535
604
|
|
|
536
|
-
export default class RoomChannel extends BaseChannel<User> {
|
|
605
|
+
export default class RoomChannel extends BaseChannel<User, ServerEvents> {
|
|
537
606
|
static pattern = 'rooms/:roomId'
|
|
538
607
|
static options = { presence: true }
|
|
539
608
|
|
|
@@ -582,21 +651,23 @@ an exception from a later hook can be retried safely.
|
|
|
582
651
|
|
|
583
652
|
## Channel Messages
|
|
584
653
|
|
|
585
|
-
Incoming client messages are handled through the
|
|
654
|
+
Incoming client messages are handled through the `@onMessage` decorator.
|
|
655
|
+
Unknown events receive a negative acknowledgement.
|
|
586
656
|
|
|
587
657
|
```ts
|
|
588
658
|
import { BaseChannel } from '@rlanz/socket'
|
|
659
|
+
import { onMessage } from '@rlanz/socket/decorators'
|
|
589
660
|
import type { AuthenticatedSocket } from '@rlanz/socket/types'
|
|
590
661
|
|
|
591
662
|
type User = { id: string; name: string }
|
|
663
|
+
type ServerEvents = {
|
|
664
|
+
'chat:message': { user: User | undefined; body: string }
|
|
665
|
+
}
|
|
592
666
|
|
|
593
|
-
export default class ChatChannel extends BaseChannel<User> {
|
|
667
|
+
export default class ChatChannel extends BaseChannel<User, ServerEvents> {
|
|
594
668
|
static pattern = 'chat/:roomId'
|
|
595
669
|
|
|
596
|
-
|
|
597
|
-
'chat:send': this.sendMessage,
|
|
598
|
-
}
|
|
599
|
-
|
|
670
|
+
@onMessage('chat:send')
|
|
600
671
|
async sendMessage(socket: AuthenticatedSocket<User>, data: { body: string }) {
|
|
601
672
|
this.broadcast('chat:message', {
|
|
602
673
|
user: socket.user,
|
|
@@ -605,10 +676,6 @@ export default class ChatChannel extends BaseChannel<User> {
|
|
|
605
676
|
|
|
606
677
|
return { delivered: true }
|
|
607
678
|
}
|
|
608
|
-
|
|
609
|
-
async onMessage(socket: AuthenticatedSocket<User>, event: string, data: unknown) {
|
|
610
|
-
console.log('Unhandled channel message', event, data)
|
|
611
|
-
}
|
|
612
679
|
}
|
|
613
680
|
```
|
|
614
681
|
|
|
@@ -642,8 +709,9 @@ Import the browser client from `@rlanz/socket/client`.
|
|
|
642
709
|
|
|
643
710
|
```ts
|
|
644
711
|
import { Socket } from '@rlanz/socket/client'
|
|
712
|
+
import type { AppSocket } from '#generated/socket'
|
|
645
713
|
|
|
646
|
-
const socket = new Socket({
|
|
714
|
+
const socket = new Socket<AppSocket>({
|
|
647
715
|
url: 'http://localhost:3333',
|
|
648
716
|
path: '/socket',
|
|
649
717
|
autoReconnect: true,
|
|
@@ -665,7 +733,7 @@ await socket.connect()
|
|
|
665
733
|
### Subscribe, Listen, Send
|
|
666
734
|
|
|
667
735
|
```ts
|
|
668
|
-
const channel = socket.channel('chat
|
|
736
|
+
const channel = socket.channel('chat/:roomId', { roomId: 'general' })
|
|
669
737
|
|
|
670
738
|
channel
|
|
671
739
|
.here((users) => console.log('present users', users))
|
|
@@ -691,7 +759,7 @@ console.log(ack)
|
|
|
691
759
|
channel.stopListening('chat:message')
|
|
692
760
|
channel.stopListeningForWhisper('typing')
|
|
693
761
|
await channel.unsubscribe()
|
|
694
|
-
await socket.leave(
|
|
762
|
+
await socket.leave(channel.name)
|
|
695
763
|
socket.disconnect()
|
|
696
764
|
```
|
|
697
765
|
|
|
@@ -701,7 +769,7 @@ The client reconnects automatically by default. `reconnectDelay` is the initial
|
|
|
701
769
|
milliseconds, and each failed retry doubles the delay until `reconnectMaxDelay` is reached.
|
|
702
770
|
Set `autoReconnect: false` to disable reconnect attempts.
|
|
703
771
|
|
|
704
|
-
### React
|
|
772
|
+
### React and Vue
|
|
705
773
|
|
|
706
774
|
Framework adapters keep the generated `AppSocket` type at one application-level factory. Channel
|
|
707
775
|
names, client methods, server events, and payloads then remain inferred without repeating the
|
|
@@ -725,8 +793,13 @@ export const { SocketProvider, useChannel, useChannelEvent, useSocketState } =
|
|
|
725
793
|
</SocketProvider>
|
|
726
794
|
|
|
727
795
|
function Chat() {
|
|
728
|
-
const channel = useChannel('chat
|
|
729
|
-
useChannelEvent(
|
|
796
|
+
const channel = useChannel('chat/:roomId', { roomId: 'general' })
|
|
797
|
+
useChannelEvent(
|
|
798
|
+
'chat/:roomId',
|
|
799
|
+
{ roomId: 'general' },
|
|
800
|
+
'chat:message',
|
|
801
|
+
(message) => console.log(message.id)
|
|
802
|
+
)
|
|
730
803
|
|
|
731
804
|
return <button onClick={() => channel?.send('chat:send', { text: 'Hello' })}>Send</button>
|
|
732
805
|
}
|
|
@@ -748,63 +821,31 @@ export const { provideSocket, useChannel, useChannelEvent, useSocketState } =
|
|
|
748
821
|
provideSocket(socket, { owned: true })
|
|
749
822
|
|
|
750
823
|
// In a descendant component setup
|
|
751
|
-
const channel = useChannel('chat
|
|
752
|
-
useChannelEvent('chat
|
|
824
|
+
const channel = useChannel('chat/:roomId', { roomId: 'general' })
|
|
825
|
+
useChannelEvent('chat/:roomId', { roomId: 'general' }, 'chat:message', (message) =>
|
|
826
|
+
console.log(message.id)
|
|
827
|
+
)
|
|
753
828
|
|
|
754
829
|
function send() {
|
|
755
830
|
channel.value?.send('chat:send', { text: 'Hello' })
|
|
756
831
|
}
|
|
757
832
|
```
|
|
758
833
|
|
|
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
834
|
Adapters acquire and share channel subscriptions, attach listeners before subscribing, and release
|
|
794
|
-
them when the component
|
|
795
|
-
|
|
796
|
-
|
|
835
|
+
them when the component or scope is disposed. React returns `null` until its passive effect acquires
|
|
836
|
+
the channel; Vue exposes a nullable shallow ref. Multiple consumers of the same channel share one
|
|
837
|
+
subscription, including across development
|
|
797
838
|
remounts, without removing listeners registered through the direct client API.
|
|
798
839
|
|
|
799
840
|
Sockets are borrowed by default: the adapter neither connects nor disconnects them. Pass `owned` to
|
|
800
|
-
`SocketProvider
|
|
801
|
-
|
|
802
|
-
|
|
841
|
+
`SocketProvider` or `{ owned: true }` to `provideSocket` only when that framework root exclusively
|
|
842
|
+
owns the socket lifecycle. Otherwise call `socket.connect()` and `socket.disconnect()` in
|
|
843
|
+
application-owned lifecycle code.
|
|
803
844
|
|
|
804
845
|
## Package Exports
|
|
805
846
|
|
|
806
847
|
```ts
|
|
807
|
-
import { BaseChannel,
|
|
848
|
+
import { BaseChannel, SocketResponseError, defineConfig } from '@rlanz/socket'
|
|
808
849
|
import type { AuthenticatedSocket, SocketConfig } from '@rlanz/socket/types'
|
|
809
850
|
import SocketProvider from '@rlanz/socket/provider'
|
|
810
851
|
import socket from '@rlanz/socket/services/main'
|
|
@@ -812,11 +853,10 @@ import { onMessage } from '@rlanz/socket/decorators'
|
|
|
812
853
|
import { SocketHealthCheck } from '@rlanz/socket/health_check'
|
|
813
854
|
import { SocketInstrumentation } from '@rlanz/socket/otel'
|
|
814
855
|
import { SocketFake } from '@rlanz/socket/testing'
|
|
815
|
-
import { Socket
|
|
856
|
+
import { Socket } from '@rlanz/socket/client'
|
|
816
857
|
import { generateSocketRegistry } from '@rlanz/socket/assembler_hook'
|
|
817
858
|
import { createSocketHooks } from '@rlanz/socket/client/react'
|
|
818
859
|
import { createSocketComposables } from '@rlanz/socket/client/vue'
|
|
819
|
-
import { createSocketContext } from '@rlanz/socket/client/svelte'
|
|
820
860
|
import type { SocketOptions, PresenceData } from '@rlanz/socket/client/types'
|
|
821
861
|
```
|
|
822
862
|
|
|
@@ -836,4 +876,3 @@ Available exports:
|
|
|
836
876
|
- `@rlanz/socket/client/types`
|
|
837
877
|
- `@rlanz/socket/client/react`
|
|
838
878
|
- `@rlanz/socket/client/vue`
|
|
839
|
-
- `@rlanz/socket/client/svelte`
|