@fluojs/websockets 1.0.7 → 2.0.0
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.ko.md +30 -10
- package/README.md +30 -10
- package/dist/bun/bun-service.d.ts +3 -1
- package/dist/bun/bun-service.d.ts.map +1 -1
- package/dist/bun/bun-service.js +75 -26
- package/dist/bun/bun-types.d.ts +15 -4
- package/dist/bun/bun-types.d.ts.map +1 -1
- package/dist/bun.d.ts +2 -1
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +2 -1
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts +4 -0
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts.map +1 -1
- package/dist/cloudflare-workers/cloudflare-workers-service.js +108 -33
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts +2 -2
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts.map +1 -1
- package/dist/cloudflare-workers.d.ts +2 -1
- package/dist/cloudflare-workers.d.ts.map +1 -1
- package/dist/cloudflare-workers.js +2 -1
- package/dist/decorators.d.ts +2 -2
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +4 -4
- package/dist/deno/deno-service.d.ts +4 -0
- package/dist/deno/deno-service.d.ts.map +1 -1
- package/dist/deno/deno-service.js +85 -16
- package/dist/deno/deno-types.d.ts +2 -2
- package/dist/deno/deno-types.d.ts.map +1 -1
- package/dist/deno.d.ts +2 -1
- package/dist/deno.d.ts.map +1 -1
- package/dist/deno.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/shared.d.ts +6 -3
- package/dist/internal/shared.d.ts.map +1 -1
- package/dist/internal/shared.js +26 -6
- package/dist/internal.d.ts +7 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +6 -0
- package/dist/module.d.ts +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +23 -3
- package/dist/node/node-module.d.ts.map +1 -1
- package/dist/node/node-module.js +6 -2
- package/dist/node/node-service-token.d.ts +20 -0
- package/dist/node/node-service-token.d.ts.map +1 -0
- package/dist/node/node-service-token.js +29 -0
- package/dist/node/node-service.d.ts +5 -3
- package/dist/node/node-service.d.ts.map +1 -1
- package/dist/node/node-service.js +26 -8
- package/dist/node/node-types.d.ts +2 -2
- package/dist/node/node-types.d.ts.map +1 -1
- package/dist/node/node.d.ts +1 -1
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +1 -1
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -1
- package/dist/service.d.ts +1 -8
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1 -8
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +21 -12
package/README.ko.md
CHANGED
|
@@ -21,7 +21,9 @@ fluo 런타임을 위한 데코레이터 기반 WebSocket 게이트웨이 작성
|
|
|
21
21
|
npm install @fluojs/websockets
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Root Node.js
|
|
24
|
+
Root Node.js module path는 `WebSocketModule.forRoot()`가 runtime에서 해석될 때만 패키지가 직접 소유한 `ws` dependency를 사용합니다. 애플리케이션 코드에서 `ws`를 직접 사용하지 않는 한 별도 설치가 필요하지 않습니다.
|
|
25
|
+
|
|
26
|
+
`@fluojs/websockets`은 `ws@^8.21.0`을 포함합니다. 업그레이드할 때 application lockfile을 갱신해 패치된 package-owned Node.js WebSocket runtime이 설치되도록 하세요. Bun, Deno, Cloudflare Workers subpath는 계속 각 runtime이 소유한 WebSocket 구현을 사용합니다.
|
|
25
27
|
|
|
26
28
|
## 사용 시점
|
|
27
29
|
|
|
@@ -102,16 +104,20 @@ WebSocketModule.forRoot({
|
|
|
102
104
|
});
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수, inbound payload 크기, pending message buffer, shutdown cleanup에 bounded default를 적용합니다. 기본값은 `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, Node backpressure `maxBufferedAmountBytes: 1 MiB`와 drop behavior입니다. 또한 server-backed Node listener는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat timer를 활성화합니다. Node shutdown은
|
|
107
|
+
옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수, inbound payload 크기, pending message buffer, shutdown cleanup에 bounded default를 적용합니다. 기본값은 `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, Node backpressure `maxBufferedAmountBytes: 1 MiB`와 drop behavior입니다. 또한 server-backed Node listener는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat timer를 활성화합니다. Node shutdown은 terminal admission gate를 설정하고 upgrade guard가 없는 경우까지 포함해 일치하는 upgrade를 `ws`에 넘기기 직전에 gate를 다시 확인합니다. 애플리케이션 shutdown 시 추적 중인 websocket 클라이언트를 닫고, `shutdown.timeoutMs` 범위 안에서 `@OnDisconnect()` cleanup이 마무리될 수 있도록 bounded 기회를 제공합니다. 해결되지 않은 cleanup은 shutdown을 무기한 막지 않고 해당 timeout 안에서 로그로 남습니다. 모든 runtime은 queue된 disconnect cleanup이 settle될 때까지 connection별 lifecycle state를 유지하므로, shutdown 직전에 client close가 queue한 `@OnDisconnect()`도 같은 bounded drain에 포함됩니다. 공식 fetch-style runtime module(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)은 `Request` typed upgrade guard를 노출하며, 애플리케이션 shutdown 중 동일한 bounded close와 disconnect cleanup 동작을 제공합니다. Bun, Deno, Cloudflare Workers는 shutdown 중에도 adapter-owned binding을 유지하므로 host가 request routing 소유권을 보존하고 새 upgrade에는 runtime의 terminal response를 반환합니다. Terminal socket error 또는 room broadcast send 실패가 발생하면 fetch-style socket을 code `1011`로 닫고 `@OnDisconnect()` cleanup을 완료합니다. Cloudflare Workers 애플리케이션 shutdown 중 새 upgrade 시도는 HTTP dispatch로 넘어가지 않고 JSON `503` shutdown response로 거절됩니다.
|
|
106
108
|
|
|
107
|
-
Root `@fluojs/websockets` / `@fluojs/websockets/node` guard는 Node의 `IncomingMessage`를 받습니다. Fetch-style subpath는 Web standard `Request`를 받으므로, 재사용 가능한 옵션 객체를 작성할 때는 subpath별 `WebSocketModuleOptions` 타입을 선택하세요.
|
|
109
|
+
Root `@fluojs/websockets` / `@fluojs/websockets/node` guard는 Node의 `IncomingMessage`를 받습니다. Fetch-style subpath는 Web standard `Request`를 받으므로, 재사용 가능한 옵션 객체를 작성할 때는 subpath별 `WebSocketModuleOptions` 타입을 선택하세요. Guard는 `true`, `undefined`, 또는 return 없음으로 upgrade를 허용하고, `false` 또는 `{ status, body? }` 형태의 `WebSocketUpgradeRejection`으로 거절하거나 `UnauthorizedException` 같은 `HttpException` 계열 오류를 throw할 수 있습니다. Throw된 HTTP exception은 socket이 accept되기 전에 동일한 pre-handshake rejection response로 변환됩니다.
|
|
108
110
|
|
|
109
111
|
### Room
|
|
110
|
-
`WebSocketRoomService`를 사용하면 gateway 또는 application service가 adapter 내부에 접근하지 않고도 가벼운 room membership state를 유지할 수 있습니다. Runtime lifecycle service는 `joinRoom(socketId, room)`, `leaveRoom(socketId, room)`, `broadcastToRoom(room, event, data)`, `getRooms(socketId)`를 구현합니다. `broadcastToRoom(...)`은 현재 room에 있는 열린 socket에 `{ event, data }` 형태의 JSON frame을
|
|
112
|
+
`WebSocketRoomService`를 사용하면 gateway 또는 application service가 adapter 내부에 접근하지 않고도 가벼운 room membership state를 유지할 수 있습니다. Runtime lifecycle service는 `joinRoom(socketId, room)`, `leaveRoom(socketId, room)`, `broadcastToRoom(room, event, data)`, `getRooms(socketId)`를 구현합니다. `joinRoom(...)`은 `socketId`가 선택한 runtime lifecycle service에 등록된 현재 열린 socket을 가리킬 때만 membership을 추가합니다. 알 수 없거나 이미 닫힌 socket identifier는 무시하며 room index를 생성하지 않습니다. `broadcastToRoom(...)`은 현재 room에 있는 열린 socket에 `{ event, data }` 형태의 JSON frame을 보냅니다. Node.js 기반 adapter는 전송 전에 설정된 `backpressure` policy를 적용하지만, fetch-style runtime(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)은 room broadcast에 backpressure policy를 적용하지 않습니다.
|
|
113
|
+
|
|
114
|
+
`WebSocketRoomService`는 runtime lifecycle service가 구현하는 type-only contract입니다. `@Inject(...)`로 lifecycle service token을 주입하고 constructor parameter를 `WebSocketRoomService`로 type 지정하세요. Root `@fluojs/websockets` entrypoint는 `WebSocketGatewayLifecycleService`를 DI token으로 노출하고, 명시적 `@fluojs/websockets/node` subpath는 `NodeWebSocketGatewayLifecycleService`를 노출합니다. 다른 runtime-specific subpath는 아래 runtime 표에 나열된 해당 `*WebSocketGatewayLifecycleService` token을 노출합니다.
|
|
111
115
|
|
|
112
116
|
```typescript
|
|
113
|
-
import {
|
|
117
|
+
import { Inject } from '@fluojs/core';
|
|
118
|
+
import { WebSocketGatewayLifecycleService, type WebSocketRoomService } from '@fluojs/websockets';
|
|
114
119
|
|
|
120
|
+
@Inject(WebSocketGatewayLifecycleService)
|
|
115
121
|
class OrderStatusPublisher {
|
|
116
122
|
constructor(private readonly rooms: WebSocketRoomService) {}
|
|
117
123
|
|
|
@@ -125,6 +131,19 @@ class OrderStatusPublisher {
|
|
|
125
131
|
|
|
126
132
|
Gateway `@OnMessage()` handler는 지원 런타임 전반에서 하나의 정규화된 payload contract를 받습니다. Text frame은 가능한 경우 JSON으로 파싱하고, 그렇지 않으면 string으로 전달합니다. Binary frame은 런타임이 Node `Buffer`/typed array, Bun `ArrayBuffer`/view, Deno `ArrayBuffer`/view/`Blob`, Cloudflare Workers `ArrayBuffer`/view/`Blob` 중 어떤 형태로 노출하더라도 UTF-8로 디코딩한 뒤 동일한 JSON/event dispatch 단계를 거칩니다. `limits.maxPayloadBytes` 검사는 모든 표현에 byte length를 사용하며, 허용된 socket에서 oversized payload가 들어오면 close code `1009`로 닫습니다.
|
|
127
133
|
|
|
134
|
+
Message handler는 `(payload, socket, request, socketId)`를 받습니다. 안정적인 `socketId`는 `WebSocketRoomService`가 받는 connection identity와 같으므로, handler가 adapter별 socket 내부를 조사하지 않고 현재 connection을 room에 추가할 수 있습니다.
|
|
135
|
+
|
|
136
|
+
기본적으로 handler return value는 Node, Bun, Deno, Cloudflare Workers 전반에서 완료될 때까지 await된 뒤 무시됩니다. `socket.send(JSON.stringify({ event: 'pong', data }))`처럼 runtime socket argument를 통해 명시적으로 reply를 보낼 수 있습니다. Return 기반 reply를 opt-in하려면 `replies: { mode: 'event-envelope' }`를 설정하세요. 그러면 올바른 `{ event: string, data?: unknown }` return이 handler settle 후 serialize되어 전송됩니다. 올바르지 않은 return은 계속 무시되며, `replies`를 생략하면 기존 기본 동작이 유지됩니다.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
WebSocketModule.forRoot({ replies: { mode: 'event-envelope' } });
|
|
140
|
+
|
|
141
|
+
@OnMessage('ping')
|
|
142
|
+
handlePing(payload, _socket, _request, socketId) {
|
|
143
|
+
return { event: 'pong', data: { payload, socketId } };
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
128
147
|
## 공개 API 개요
|
|
129
148
|
|
|
130
149
|
- `@WebSocketGateway(options)`: 클래스를 WebSocket 게이트웨이로 표시합니다.
|
|
@@ -132,18 +151,19 @@ Gateway `@OnMessage()` handler는 지원 런타임 전반에서 하나의 정규
|
|
|
132
151
|
- `@OnMessage(event?)`: 인바운드 메시지 핸들러를 위한 데코레이터입니다.
|
|
133
152
|
- `@OnDisconnect()`: 연결 해제 핸들러를 위한 데코레이터입니다.
|
|
134
153
|
- `WebSocketModule`: WebSocket 통합을 위한 루트 모듈입니다.
|
|
135
|
-
- `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, shutdown })`: pre-upgrade guard
|
|
136
|
-
- `WebSocketGatewayLifecycleService`: 기본 Node.js 기반 lifecycle service token
|
|
154
|
+
- `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, replies, shutdown })`: pre-upgrade guard, bounded runtime default, optional event-envelope reply를 구성합니다.
|
|
155
|
+
- `WebSocketGatewayLifecycleService`: 기본 Node.js 기반 lifecycle service token인 `NodeWebSocketGatewayLifecycleService`의 루트 alias입니다. 직접 instantiate하는 concrete class가 아니라 DI token placeholder이며, `WebSocketModule.forRoot(...)`가 lazy Node implementation을 연결한 뒤 application container에서 resolve해야 합니다.
|
|
137
156
|
- `WebSocketRoomService`: websocket room join, leave, broadcast, 조회를 위해 runtime lifecycle service가 구현하는 Room management contract입니다.
|
|
157
|
+
- Typed runtime seam: `WebSocketUpgradeContext`, `WebSocketUpgradeGuard`, `WebSocketUpgradeRejection`, `WebSocketGatewayDescriptor`, `WebSocketGatewayHandlerDescriptor`, 그리고 Node, Bun, Deno, Cloudflare Workers subpath의 runtime socket/binding type.
|
|
138
158
|
- Metadata helper와 symbol: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`.
|
|
139
159
|
|
|
140
160
|
## 런타임별 서브패스
|
|
141
161
|
|
|
142
|
-
기본 루트 Node.js alias 대신 런타임을 명시적으로 고정하고 싶다면 런타임별 서브패스를 사용하세요. 루트 `@fluojs/websockets` 진입점은 Node.js 기본 module
|
|
162
|
+
기본 루트 Node.js alias 대신 런타임을 명시적으로 고정하고 싶다면 런타임별 서브패스를 사용하세요. 루트 `@fluojs/websockets` 진입점은 Node.js 기본 module export name을 유지하고 `WebSocketGatewayLifecycleService`를 `NodeWebSocketGatewayLifecycleService`와 같은 DI token으로 alias하지만, 단순 root package import는 concrete Node implementation을 lazy runtime provider resolution 뒤에 둡니다. Fetch-style 애플리케이션은 선택한 런타임 서브패스에서 gateway decorator와 metadata helper를 import해 authoring code가 root Node.js-backed module boundary에 의존하지 않게 해야 합니다.
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
Published package와 기본 Node.js entrypoint의 package-owned 지원 계약은 manifest의 `engines.node >=24.0.0 <27` 선언입니다. Node 20 및 Node 22 host를 Node.js `>=24.0.0 <27`로 올리세요. Node 24 미만과 Node 27 이상은 지원하지 않습니다. Bun, Deno, Cloudflare Workers 지원은 아래 전용 fetch-style subpath를 통해 노출되며, 해당 subpath는 request/handler type을 web-standard로 유지하고 application code가 root Node.js lifecycle-service alias에 의존하지 않게 합니다.
|
|
145
165
|
|
|
146
|
-
각 서브패스는 해당 `*WebSocketModule.forRoot(...)` 진입점, 일치하는 런타임 lifecycle service export, 그리고 공유 gateway authoring primitive인 `WebSocketGateway`, `OnConnect`, `OnMessage`, `OnDisconnect`, `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`을 제공합니다.
|
|
166
|
+
각 서브패스는 해당 `*WebSocketModule.forRoot(...)` 진입점, 일치하는 런타임 lifecycle service export, 그리고 공유 gateway authoring primitive인 `WebSocketGateway`, `OnConnect`, `OnMessage`, `OnDisconnect`, `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`을 제공합니다. Bun 서브패스의 low-level binding은 `upgrade(...)`만 가진 `BunWebSocketUpgradeHost`를 받으며, adapter-owned listener shutdown과 raw HTTP fetch 제어는 `@fluojs/platform-bun`에 남습니다.
|
|
147
167
|
|
|
148
168
|
| 런타임 | 서브패스 | 모듈 | Lifecycle service |
|
|
149
169
|
| --- | --- | --- | --- |
|
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ Decorator-based WebSocket gateway authoring for the fluo runtime.
|
|
|
21
21
|
npm install @fluojs/websockets
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The root Node.js
|
|
24
|
+
The root Node.js module path uses the package-owned `ws` dependency only when `WebSocketModule.forRoot()` is resolved at runtime. Applications do not need to install `ws` separately unless they use it directly in their own code.
|
|
25
|
+
|
|
26
|
+
`@fluojs/websockets` includes `ws@^8.21.0`. Refresh the application lockfile when upgrading so the patched package-owned Node.js WebSocket runtime is installed. The Bun, Deno, and Cloudflare Workers subpaths continue to use their runtime-owned WebSocket implementations.
|
|
25
27
|
|
|
26
28
|
## When to Use
|
|
27
29
|
|
|
@@ -102,16 +104,20 @@ WebSocketModule.forRoot({
|
|
|
102
104
|
});
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
When omitted, `@fluojs/websockets` applies bounded defaults for concurrent connections, inbound payload size, pending message buffers, and shutdown cleanup. Default settings are `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, and Node backpressure `maxBufferedAmountBytes: 1 MiB` with drop behavior. Server-backed Node listeners enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. Node shutdown
|
|
107
|
+
When omitted, `@fluojs/websockets` applies bounded defaults for concurrent connections, inbound payload size, pending message buffers, and shutdown cleanup. Default settings are `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, and Node backpressure `maxBufferedAmountBytes: 1 MiB` with drop behavior. Server-backed Node listeners enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. Node shutdown establishes a terminal admission gate, rechecks it immediately before handing a matching upgrade to `ws` even when no upgrade guard is configured, and will close tracked websocket clients during application shutdown. It gives `@OnDisconnect()` cleanup a bounded chance to finish within `shutdown.timeoutMs`; unresolved cleanup is logged and bounded by that timeout instead of blocking shutdown indefinitely. All runtimes retain per-connection lifecycle state until queued disconnect cleanup settles, so a client close that queues `@OnDisconnect()` immediately before shutdown still enters the same bounded drain. The official fetch-style runtime modules (`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, and `@fluojs/websockets/cloudflare-workers`) expose `Request`-typed upgrade guards and provide the same bounded close and disconnect cleanup behavior during application shutdown. Bun, Deno, and Cloudflare Workers keep the adapter-owned binding installed during shutdown so the host retains request routing ownership while new upgrades receive the runtime's terminal response. Fetch-style sockets are closed with code `1011` and complete `@OnDisconnect()` cleanup after terminal socket errors or failed room-broadcast sends. During Cloudflare Workers application shutdown, new upgrade attempts receive a JSON `503` shutdown response instead of falling through to HTTP dispatch.
|
|
106
108
|
|
|
107
|
-
The root `@fluojs/websockets` / `@fluojs/websockets/node` guard receives Node's `IncomingMessage`. Fetch-style subpaths receive a Web-standard `Request`, so choose the subpath-specific `WebSocketModuleOptions` type when authoring reusable option objects.
|
|
109
|
+
The root `@fluojs/websockets` / `@fluojs/websockets/node` guard receives Node's `IncomingMessage`. Fetch-style subpaths receive a Web-standard `Request`, so choose the subpath-specific `WebSocketModuleOptions` type when authoring reusable option objects. Guards may allow an upgrade with `true`, `undefined`, or no return value; reject with `false` or a `{ status, body? }` `WebSocketUpgradeRejection`; or throw an `HttpException`-like error such as `UnauthorizedException`. Thrown HTTP exceptions are converted to the same pre-handshake rejection response before any socket is accepted.
|
|
108
110
|
|
|
109
111
|
### Rooms
|
|
110
|
-
`WebSocketRoomService` lets gateway or application services keep lightweight room membership state without reaching into adapter internals. Runtime lifecycle services implement `joinRoom(socketId, room)`, `leaveRoom(socketId, room)`, `broadcastToRoom(room, event, data)`, and `getRooms(socketId)`. `broadcastToRoom(...)` sends a JSON frame shaped as `{ event, data }` to currently open sockets in the room
|
|
112
|
+
`WebSocketRoomService` lets gateway or application services keep lightweight room membership state without reaching into adapter internals. Runtime lifecycle services implement `joinRoom(socketId, room)`, `leaveRoom(socketId, room)`, `broadcastToRoom(room, event, data)`, and `getRooms(socketId)`. `joinRoom(...)` adds membership only when `socketId` identifies a currently open socket registered by the selected runtime lifecycle service; unknown or already closed socket identifiers are ignored and do not create room indexes. `broadcastToRoom(...)` sends a JSON frame shaped as `{ event, data }` to currently open sockets in the room. The Node.js-backed adapter applies the configured `backpressure` policy before sending; the fetch-style runtimes (`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, and `@fluojs/websockets/cloudflare-workers`) do not apply a backpressure policy to room broadcasts.
|
|
113
|
+
|
|
114
|
+
`WebSocketRoomService` is a type-only contract implemented by the runtime lifecycle service. Inject the lifecycle service token with `@Inject(...)` and type the constructor parameter as `WebSocketRoomService`. The root `@fluojs/websockets` entrypoint exposes `WebSocketGatewayLifecycleService` as the DI token, while the explicit `@fluojs/websockets/node` subpath exposes `NodeWebSocketGatewayLifecycleService`; the other runtime-specific subpaths expose the matching `*WebSocketGatewayLifecycleService` token listed in the runtime table below.
|
|
111
115
|
|
|
112
116
|
```typescript
|
|
113
|
-
import {
|
|
117
|
+
import { Inject } from '@fluojs/core';
|
|
118
|
+
import { WebSocketGatewayLifecycleService, type WebSocketRoomService } from '@fluojs/websockets';
|
|
114
119
|
|
|
120
|
+
@Inject(WebSocketGatewayLifecycleService)
|
|
115
121
|
class OrderStatusPublisher {
|
|
116
122
|
constructor(private readonly rooms: WebSocketRoomService) {}
|
|
117
123
|
|
|
@@ -125,6 +131,19 @@ class OrderStatusPublisher {
|
|
|
125
131
|
|
|
126
132
|
Gateway `@OnMessage()` handlers receive one normalized payload contract across supported runtimes. Text frames are parsed as JSON when possible and otherwise delivered as strings. Binary frames are decoded as UTF-8 before the same JSON/event dispatch step, whether the runtime surfaces them as Node `Buffer`/typed arrays, Bun `ArrayBuffer`/views, Deno `ArrayBuffer`/views/`Blob`, or Cloudflare Workers `ArrayBuffer`/views/`Blob`. The `limits.maxPayloadBytes` check uses byte length for every representation and closes oversized accepted sockets with close code `1009`.
|
|
127
133
|
|
|
134
|
+
Message handlers receive `(payload, socket, request, socketId)`. The stable `socketId` is the same connection identity accepted by `WebSocketRoomService`, so a handler can join its current connection to a room without adapter-specific socket inspection.
|
|
135
|
+
|
|
136
|
+
Handler return values are awaited for completion and then ignored across Node, Bun, Deno, and Cloudflare Workers by default. Send replies explicitly through the runtime socket argument, for example `socket.send(JSON.stringify({ event: 'pong', data }))`. To opt into return-based replies, configure `replies: { mode: 'event-envelope' }`; then a valid `{ event: string, data?: unknown }` return is serialized and sent after the handler settles. Invalid returns remain ignored, and omitting `replies` preserves the default behavior.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
WebSocketModule.forRoot({ replies: { mode: 'event-envelope' } });
|
|
140
|
+
|
|
141
|
+
@OnMessage('ping')
|
|
142
|
+
handlePing(payload, _socket, _request, socketId) {
|
|
143
|
+
return { event: 'pong', data: { payload, socketId } };
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
128
147
|
## Public API Overview
|
|
129
148
|
|
|
130
149
|
- `@WebSocketGateway(options)`: Marks a class as a WebSocket gateway.
|
|
@@ -132,18 +151,19 @@ Gateway `@OnMessage()` handlers receive one normalized payload contract across s
|
|
|
132
151
|
- `@OnMessage(event?)`: Decorator for inbound message handlers.
|
|
133
152
|
- `@OnDisconnect()`: Decorator for disconnection handlers.
|
|
134
153
|
- `WebSocketModule`: Root module for WebSocket integration.
|
|
135
|
-
- `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, shutdown })`: Configures pre-upgrade guards
|
|
136
|
-
- `WebSocketGatewayLifecycleService`: Root alias for the default Node.js-backed lifecycle service token.
|
|
154
|
+
- `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, replies, shutdown })`: Configures pre-upgrade guards, bounded runtime defaults, and optional event-envelope replies.
|
|
155
|
+
- `WebSocketGatewayLifecycleService`: Root alias for `NodeWebSocketGatewayLifecycleService`, the default Node.js-backed lifecycle service token. It is a DI token placeholder, not a concrete class to instantiate directly; resolve it from the application container after `WebSocketModule.forRoot(...)` wires the lazy Node implementation.
|
|
137
156
|
- `WebSocketRoomService`: Room management contract implemented by runtime lifecycle services for joining, leaving, broadcasting to, and inspecting websocket rooms.
|
|
157
|
+
- Typed runtime seams: `WebSocketUpgradeContext`, `WebSocketUpgradeGuard`, `WebSocketUpgradeRejection`, `WebSocketGatewayDescriptor`, and `WebSocketGatewayHandlerDescriptor`, plus runtime socket/binding types from the Node, Bun, Deno, and Cloudflare Workers subpaths.
|
|
138
158
|
- Metadata helpers and symbols: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`.
|
|
139
159
|
|
|
140
160
|
## Runtime-Specific Subpaths
|
|
141
161
|
|
|
142
|
-
Use the runtime subpaths when you want an explicit runtime binding instead of the default root Node.js alias. The root `@fluojs/websockets` entrypoint preserves the Node.js default module and
|
|
162
|
+
Use the runtime subpaths when you want an explicit runtime binding instead of the default root Node.js alias. The root `@fluojs/websockets` entrypoint preserves the Node.js default module export name and aliases `WebSocketGatewayLifecycleService` to the same DI token as `NodeWebSocketGatewayLifecycleService`, but plain root-package imports keep the concrete Node implementation behind lazy runtime provider resolution. Fetch-style applications should still import gateway decorators and metadata helpers from their selected runtime subpath so authoring code does not rely on the root Node.js-backed module boundary.
|
|
143
163
|
|
|
144
|
-
The package manifest declares `engines.node >=
|
|
164
|
+
The package manifest declares `engines.node >=24.0.0 <27` for the published package and default Node.js entrypoint, under its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js `>=24.0.0 <27`; Node versions below 24 and Node 27+ are unsupported. Bun, Deno, and Cloudflare Workers support is exposed through the dedicated fetch-style subpaths listed below; those subpaths keep request/handler types web-standard and avoid the root Node.js lifecycle-service alias in application code.
|
|
145
165
|
|
|
146
|
-
Each subpath exposes its `*WebSocketModule.forRoot(...)` entrypoint, the matching runtime lifecycle service export, and the shared gateway authoring primitives: `WebSocketGateway`, `OnConnect`, `OnMessage`, `OnDisconnect`, `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, and `webSocketHandlerMetadataSymbol`.
|
|
166
|
+
Each subpath exposes its `*WebSocketModule.forRoot(...)` entrypoint, the matching runtime lifecycle service export, and the shared gateway authoring primitives: `WebSocketGateway`, `OnConnect`, `OnMessage`, `OnDisconnect`, `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, and `webSocketHandlerMetadataSymbol`. The Bun subpath's low-level binding receives a `BunWebSocketUpgradeHost` with only `upgrade(...)`; adapter-owned listener shutdown and raw HTTP fetch control stay with `@fluojs/platform-bun`.
|
|
147
167
|
|
|
148
168
|
| Runtime | Subpath | Module | Lifecycle service |
|
|
149
169
|
| --- | --- | --- | --- |
|
|
@@ -48,7 +48,6 @@ export declare class BunWebSocketGatewayLifecycleService implements OnApplicatio
|
|
|
48
48
|
private runConnectHandlers;
|
|
49
49
|
private finalizeConnectionBinding;
|
|
50
50
|
private replayBufferedConnectionEvents;
|
|
51
|
-
private resolveBackpressureLimit;
|
|
52
51
|
private resolveUpgradeRejection;
|
|
53
52
|
private closeOversizedPayload;
|
|
54
53
|
private resolveMaxConnectionCount;
|
|
@@ -63,11 +62,14 @@ export declare class BunWebSocketGatewayLifecycleService implements OnApplicatio
|
|
|
63
62
|
private trackPendingUpgradeOperation;
|
|
64
63
|
private awaitPendingUpgradeOperations;
|
|
65
64
|
private closeActiveSockets;
|
|
65
|
+
private scheduleSocketStateCleanup;
|
|
66
66
|
private awaitHandlerQueueDrain;
|
|
67
67
|
joinRoom(socketId: string, room: string): void;
|
|
68
68
|
leaveRoom(socketId: string, room: string): void;
|
|
69
69
|
broadcastToRoom(room: string, event: string, data: unknown): void;
|
|
70
70
|
getRooms(socketId: string): ReadonlySet<string>;
|
|
71
|
+
private closeSocketAfterTerminalFailure;
|
|
72
|
+
private unregisterSocketWithDeferredStateCleanup;
|
|
71
73
|
private unregisterSocket;
|
|
72
74
|
}
|
|
73
75
|
//# sourceMappingURL=bun-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun-service.d.ts","sourceRoot":"","sources":["../../src/bun/bun-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAezI,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bun-service.d.ts","sourceRoot":"","sources":["../../src/bun/bun-service.ts"],"names":[],"mappings":"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"}
|
package/dist/bun/bun-service.js
CHANGED
|
@@ -90,14 +90,12 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
90
90
|
return {
|
|
91
91
|
fetch: (request, server) => this.trackPendingUpgradeOperation(this.handleUpgradeRequest(request, server, descriptorsByPath)),
|
|
92
92
|
websocket: {
|
|
93
|
-
backpressureLimit: this.resolveBackpressureLimit(),
|
|
94
93
|
close: (socket, code, reason) => {
|
|
95
|
-
this.
|
|
94
|
+
this.unregisterSocketWithDeferredStateCleanup(socket.data.state);
|
|
96
95
|
this.handleSocketClose(socket, code, reason);
|
|
97
96
|
},
|
|
98
|
-
closeOnBackpressureLimit: this.moduleOptions.backpressure?.policy === 'close',
|
|
99
97
|
error: (socket, error) => {
|
|
100
|
-
this.
|
|
98
|
+
this.closeSocketAfterTerminalFailure(socket.data.state, socket, 'Socket error');
|
|
101
99
|
this.logger.error('WebSocket gateway socket emitted an error.', error, LIFECYCLE_LOG_CONTEXT);
|
|
102
100
|
},
|
|
103
101
|
idleTimeout: this.resolveIdleTimeoutSeconds(),
|
|
@@ -107,7 +105,7 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
107
105
|
},
|
|
108
106
|
open: socket => {
|
|
109
107
|
void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(socket)).catch(error => {
|
|
110
|
-
this.
|
|
108
|
+
this.unregisterSocketWithDeferredStateCleanup(socket.data.state);
|
|
111
109
|
this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT);
|
|
112
110
|
socket.close(1011, 'Internal server error');
|
|
113
111
|
});
|
|
@@ -213,11 +211,13 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
213
211
|
bufferedDisconnect: undefined,
|
|
214
212
|
bufferedMessages: [],
|
|
215
213
|
bufferedMessagesStartIndex: 0,
|
|
214
|
+
cleanupScheduled: false,
|
|
216
215
|
connectLifecycleSettled: false,
|
|
217
216
|
connectLifecyclePromise: connectLifecycle.promise,
|
|
218
217
|
descriptors,
|
|
219
218
|
disconnectLifecycleSettled: false,
|
|
220
219
|
disconnectLifecyclePromise: disconnectLifecycle.promise,
|
|
220
|
+
disconnectDispatchScheduled: false,
|
|
221
221
|
enqueuedMessageCount: 0,
|
|
222
222
|
handlerQueue: Promise.resolve(),
|
|
223
223
|
handlersReady: false,
|
|
@@ -231,7 +231,8 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
231
231
|
resolveConnectLifecycle: connectLifecycle.resolve,
|
|
232
232
|
resolveDisconnectLifecycle: disconnectLifecycle.resolve,
|
|
233
233
|
resolved: [],
|
|
234
|
-
socketId: crypto.randomUUID()
|
|
234
|
+
socketId: crypto.randomUUID(),
|
|
235
|
+
terminalFailureHandled: false
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
238
|
settleOpenRegistration(state) {
|
|
@@ -334,13 +335,16 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
334
335
|
this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest pending message due to buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
|
|
335
336
|
}
|
|
336
337
|
enqueueMessageDispatch(state, socket, message) {
|
|
338
|
+
if (state.terminalFailureHandled) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
337
341
|
const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
|
|
338
342
|
const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
|
|
339
343
|
if (this.getQueuedMessageCount(state) >= limit) {
|
|
340
344
|
if (policy === 'close') {
|
|
341
345
|
socket.close(1013, 'Ready-state message queue limit exceeded');
|
|
342
346
|
this.clearQueuedMessages(state);
|
|
343
|
-
this.
|
|
347
|
+
this.unregisterSocketWithDeferredStateCleanup(state);
|
|
344
348
|
this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
|
|
345
349
|
return;
|
|
346
350
|
}
|
|
@@ -374,11 +378,25 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
374
378
|
if (nextMessage === undefined) {
|
|
375
379
|
continue;
|
|
376
380
|
}
|
|
377
|
-
await dispatchGatewayMessage(state.resolved, socket, state.request, nextMessage, this.
|
|
381
|
+
const outcome = await dispatchGatewayMessage(state.resolved, socket, state.request, nextMessage, state.socketId, this.moduleOptions.replies?.mode, message => {
|
|
382
|
+
if (socket.send(message) !== 0) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
|
|
386
|
+
return false;
|
|
387
|
+
}, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
388
|
+
if (outcome === 'terminal') {
|
|
389
|
+
this.clearQueuedMessages(state);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
378
392
|
}
|
|
379
393
|
this.clearQueuedMessages(state);
|
|
380
394
|
}
|
|
381
395
|
enqueueDisconnectDispatch(state, socket, disconnectEvent) {
|
|
396
|
+
if (state.disconnectDispatchScheduled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
state.disconnectDispatchScheduled = true;
|
|
382
400
|
state.handlerQueue = state.handlerQueue.then(async () => {
|
|
383
401
|
await dispatchGatewayDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
384
402
|
}).catch(error => {
|
|
@@ -426,13 +444,6 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
426
444
|
}
|
|
427
445
|
this.clearBufferedMessages(state);
|
|
428
446
|
}
|
|
429
|
-
resolveBackpressureLimit() {
|
|
430
|
-
const configured = this.moduleOptions.backpressure?.maxBufferedAmountBytes;
|
|
431
|
-
if (!isFinitePositiveInteger(configured)) {
|
|
432
|
-
return 1_048_576;
|
|
433
|
-
}
|
|
434
|
-
return configured;
|
|
435
|
-
}
|
|
436
447
|
async resolveUpgradeRejection(request, path) {
|
|
437
448
|
if (this.isShuttingDown) {
|
|
438
449
|
return {
|
|
@@ -543,10 +554,6 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
543
554
|
}
|
|
544
555
|
async runShutdownLifecycle() {
|
|
545
556
|
this.isShuttingDown = true;
|
|
546
|
-
if (hasBunWebSocketBindingHost(this.adapter)) {
|
|
547
|
-
const bunAdapter = this.adapter;
|
|
548
|
-
bunAdapter.configureWebSocketBinding(undefined);
|
|
549
|
-
}
|
|
550
557
|
const shutdownTimeoutMs = this.resolveShutdownTimeoutMs();
|
|
551
558
|
await this.awaitPendingUpgradeOperations(shutdownTimeoutMs);
|
|
552
559
|
await this.closeActiveSockets(shutdownTimeoutMs);
|
|
@@ -600,10 +607,7 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
600
607
|
}
|
|
601
608
|
async closeActiveSockets(timeoutMs) {
|
|
602
609
|
const activeSockets = [...this.socketRegistry.entries()];
|
|
603
|
-
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined);
|
|
610
|
+
const activeStates = [...this.socketStates.values()];
|
|
607
611
|
for (const [, socket] of activeSockets) {
|
|
608
612
|
if (socket.readyState === 1) {
|
|
609
613
|
socket.close(1001, 'Server shutting down');
|
|
@@ -611,6 +615,15 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
611
615
|
}
|
|
612
616
|
await this.awaitHandlerQueueDrain(activeStates, timeoutMs);
|
|
613
617
|
}
|
|
618
|
+
scheduleSocketStateCleanup(state) {
|
|
619
|
+
if (state.cleanupScheduled) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
state.cleanupScheduled = true;
|
|
623
|
+
void Promise.all([state.connectLifecyclePromise, state.disconnectLifecyclePromise]).finally(() => {
|
|
624
|
+
this.socketStates.delete(state.socketId);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
614
627
|
async awaitHandlerQueueDrain(states, timeoutMs) {
|
|
615
628
|
if (states.length === 0) {
|
|
616
629
|
return;
|
|
@@ -647,6 +660,10 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
647
660
|
});
|
|
648
661
|
}
|
|
649
662
|
joinRoom(socketId, room) {
|
|
663
|
+
const socket = this.socketRegistry.get(socketId);
|
|
664
|
+
if (!socket || socket.readyState !== 1) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
650
667
|
let rooms = this.socketRooms.get(socketId);
|
|
651
668
|
if (!rooms) {
|
|
652
669
|
rooms = new Set();
|
|
@@ -688,7 +705,13 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
688
705
|
}
|
|
689
706
|
const result = socket.send(message);
|
|
690
707
|
if (result === 0) {
|
|
691
|
-
this.
|
|
708
|
+
const state = this.socketStates.get(socketId);
|
|
709
|
+
if (state) {
|
|
710
|
+
this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
|
|
711
|
+
} else {
|
|
712
|
+
socket.close(1011, 'Send failed');
|
|
713
|
+
this.unregisterSocket(socketId);
|
|
714
|
+
}
|
|
692
715
|
this.logger.warn(`WebSocket connection ${socketId} dropped a room broadcast because the socket was unavailable.`, LIFECYCLE_LOG_CONTEXT);
|
|
693
716
|
}
|
|
694
717
|
}
|
|
@@ -700,9 +723,35 @@ class BunWebSocketGatewayLifecycleService {
|
|
|
700
723
|
}
|
|
701
724
|
return new Set(rooms);
|
|
702
725
|
}
|
|
703
|
-
|
|
726
|
+
closeSocketAfterTerminalFailure(state, socket, reason) {
|
|
727
|
+
if (state.terminalFailureHandled) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
state.terminalFailureHandled = true;
|
|
731
|
+
this.clearQueuedMessages(state);
|
|
732
|
+
this.unregisterSocketWithDeferredStateCleanup(state);
|
|
733
|
+
socket.close(1011, reason);
|
|
734
|
+
const disconnectEvent = {
|
|
735
|
+
code: 1011,
|
|
736
|
+
reason
|
|
737
|
+
};
|
|
738
|
+
if (!state.handlersReady) {
|
|
739
|
+
state.bufferedDisconnect ??= disconnectEvent;
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.enqueueDisconnectDispatch(state, socket, disconnectEvent);
|
|
743
|
+
}
|
|
744
|
+
unregisterSocketWithDeferredStateCleanup(state) {
|
|
745
|
+
this.unregisterSocket(state.socketId, {
|
|
746
|
+
deleteState: false
|
|
747
|
+
});
|
|
748
|
+
this.scheduleSocketStateCleanup(state);
|
|
749
|
+
}
|
|
750
|
+
unregisterSocket(socketId, options = {}) {
|
|
704
751
|
this.socketRegistry.delete(socketId);
|
|
705
|
-
|
|
752
|
+
if (options.deleteState !== false) {
|
|
753
|
+
this.socketStates.delete(socketId);
|
|
754
|
+
}
|
|
706
755
|
const rooms = this.socketRooms.get(socketId);
|
|
707
756
|
if (rooms) {
|
|
708
757
|
for (const room of rooms) {
|
package/dist/bun/bun-types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WebSocketModuleOptions as SharedWebSocketModuleOptions,
|
|
1
|
+
import type { WebSocketModuleOptions as SharedWebSocketModuleOptions, WebSocketUpgradeGuard as SharedWebSocketUpgradeGuard, WebSocketUpgradeContext, WebSocketUpgradeRejection } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Defines the bun web socket message type.
|
|
4
4
|
*/
|
|
@@ -20,7 +20,9 @@ export interface BunServerWebSocket<TData = unknown> {
|
|
|
20
20
|
unsubscribe(topic: string): void;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Describes the
|
|
23
|
+
* Describes the legacy complete Bun server handle used by host doubles.
|
|
24
|
+
*
|
|
25
|
+
* @deprecated Websocket bindings receive {@link BunWebSocketUpgradeHost}. Keep listener lifecycle and raw fetch ownership in `@fluojs/platform-bun`.
|
|
24
26
|
*/
|
|
25
27
|
export interface BunServerLike {
|
|
26
28
|
fetch?(request: Request): Response | Promise<Response> | undefined | Promise<Response | undefined>;
|
|
@@ -33,6 +35,15 @@ export interface BunServerLike {
|
|
|
33
35
|
}): boolean;
|
|
34
36
|
url?: URL;
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Describes the upgrade-only Bun host exposed to websocket bindings.
|
|
40
|
+
*/
|
|
41
|
+
export interface BunWebSocketUpgradeHost {
|
|
42
|
+
upgrade<TData = unknown>(request: Request, options?: {
|
|
43
|
+
data?: TData;
|
|
44
|
+
headers?: HeadersInit;
|
|
45
|
+
}): boolean;
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Describes the bun web socket handler contract.
|
|
38
49
|
*/
|
|
@@ -58,7 +69,7 @@ export interface BunWebSocketHandler<TData = unknown> {
|
|
|
58
69
|
* Describes the bun web socket binding contract.
|
|
59
70
|
*/
|
|
60
71
|
export interface BunWebSocketBinding<TData = unknown> {
|
|
61
|
-
fetch(request: Request, server:
|
|
72
|
+
fetch(request: Request, server: BunWebSocketUpgradeHost): Response | Promise<Response> | undefined | Promise<Response | undefined>;
|
|
62
73
|
websocket: BunWebSocketHandler<TData>;
|
|
63
74
|
}
|
|
64
75
|
/**
|
|
@@ -70,7 +81,7 @@ export interface BunWebSocketBindingHost {
|
|
|
70
81
|
/**
|
|
71
82
|
* Defines the typed on message handler type.
|
|
72
83
|
*/
|
|
73
|
-
export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = (
|
|
84
|
+
export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = import('../types.js').TypedOnMessageHandler<TEvents, K, BunServerWebSocket, Request>;
|
|
74
85
|
/**
|
|
75
86
|
* Describes the web socket gateway context contract.
|
|
76
87
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun-types.d.ts","sourceRoot":"","sources":["../../src/bun/bun-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,IAAI,4BAA4B,EACtD,
|
|
1
|
+
{"version":3,"file":"bun-types.d.ts","sourceRoot":"","sources":["../../src/bun/bun-types.ts"],"names":[],"mappings":"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"}
|
package/dist/bun.d.ts
CHANGED
package/dist/bun.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../src/bun.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,
|
|
1
|
+
{"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../src/bun.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/bun.js
CHANGED
|
@@ -61,11 +61,15 @@ export declare class CloudflareWorkersWebSocketGatewayLifecycleService implement
|
|
|
61
61
|
private trackPendingUpgradeOperation;
|
|
62
62
|
private awaitPendingUpgradeOperations;
|
|
63
63
|
private closeActiveSockets;
|
|
64
|
+
private scheduleSocketStateCleanup;
|
|
64
65
|
private awaitHandlerQueueDrain;
|
|
65
66
|
joinRoom(socketId: string, room: string): void;
|
|
66
67
|
leaveRoom(socketId: string, room: string): void;
|
|
67
68
|
broadcastToRoom(room: string, event: string, data: unknown): void;
|
|
68
69
|
getRooms(socketId: string): ReadonlySet<string>;
|
|
70
|
+
private closeSocketAfterTerminalFailure;
|
|
71
|
+
private unregisterSocketWithDeferredStateCleanup;
|
|
72
|
+
private unregisterTrackedSocketWithDeferredStateCleanup;
|
|
69
73
|
private unregisterSocket;
|
|
70
74
|
}
|
|
71
75
|
//# sourceMappingURL=cloudflare-workers-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare-workers-service.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAgBzI,OAAO,KAAK,EAA8B,oBAAoB,EAA6B,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"cloudflare-workers-service.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-service.ts"],"names":[],"mappings":"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"}
|