@fluojs/websockets 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +133 -0
- package/README.md +133 -0
- package/dist/bun/bun-module.d.ts +15 -0
- package/dist/bun/bun-module.d.ts.map +1 -0
- package/dist/bun/bun-module.js +27 -0
- package/dist/bun/bun-service.d.ts +63 -0
- package/dist/bun/bun-service.d.ts.map +1 -0
- package/dist/bun/bun-service.js +558 -0
- package/dist/bun/bun-types.d.ts +58 -0
- package/dist/bun/bun-types.d.ts.map +1 -0
- package/dist/bun/bun-types.js +1 -0
- package/dist/bun/bun.d.ts +4 -0
- package/dist/bun/bun.d.ts.map +1 -0
- package/dist/bun/bun.js +3 -0
- package/dist/bun.d.ts +2 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +1 -0
- package/dist/cloudflare-workers/cloudflare-workers-module.d.ts +15 -0
- package/dist/cloudflare-workers/cloudflare-workers-module.d.ts.map +1 -0
- package/dist/cloudflare-workers/cloudflare-workers-module.js +27 -0
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts +61 -0
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts.map +1 -0
- package/dist/cloudflare-workers/cloudflare-workers-service.js +538 -0
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts +30 -0
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts.map +1 -0
- package/dist/cloudflare-workers/cloudflare-workers-types.js +1 -0
- package/dist/cloudflare-workers/cloudflare-workers.d.ts +4 -0
- package/dist/cloudflare-workers/cloudflare-workers.d.ts.map +1 -0
- package/dist/cloudflare-workers/cloudflare-workers.js +3 -0
- package/dist/cloudflare-workers.d.ts +2 -0
- package/dist/cloudflare-workers.d.ts.map +1 -0
- package/dist/cloudflare-workers.js +1 -0
- package/dist/decorators.d.ts +56 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +115 -0
- package/dist/deno/deno-module.d.ts +15 -0
- package/dist/deno/deno-module.d.ts.map +1 -0
- package/dist/deno/deno-module.js +27 -0
- package/dist/deno/deno-service.d.ts +61 -0
- package/dist/deno/deno-service.d.ts.map +1 -0
- package/dist/deno/deno-service.js +533 -0
- package/dist/deno/deno-types.d.ts +25 -0
- package/dist/deno/deno-types.d.ts.map +1 -0
- package/dist/deno/deno-types.js +1 -0
- package/dist/deno/deno.d.ts +4 -0
- package/dist/deno/deno.d.ts.map +1 -0
- package/dist/deno/deno.js +3 -0
- package/dist/deno.d.ts +2 -0
- package/dist/deno.d.ts.map +1 -0
- package/dist/deno.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/shared.d.ts +28 -0
- package/dist/internal/shared.d.ts.map +1 -0
- package/dist/internal/shared.js +188 -0
- package/dist/metadata.d.ts +13 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +81 -0
- package/dist/module.d.ts +26 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +26 -0
- package/dist/node/node-module.d.ts +15 -0
- package/dist/node/node-module.d.ts.map +1 -0
- package/dist/node/node-module.js +27 -0
- package/dist/node/node-service.d.ts +129 -0
- package/dist/node/node-service.d.ts.map +1 -0
- package/dist/node/node-service.js +892 -0
- package/dist/node/node-types.d.ts +81 -0
- package/dist/node/node-types.d.ts.map +1 -0
- package/dist/node/node-types.js +1 -0
- package/dist/node/node.d.ts +4 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +3 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +1 -0
- package/dist/options-token.internal.d.ts +7 -0
- package/dist/options-token.internal.d.ts.map +1 -0
- package/dist/options-token.internal.js +4 -0
- package/dist/service.d.ts +9 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +8 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineModule } from '@fluojs/runtime';
|
|
2
|
+
import { WEBSOCKET_OPTIONS_INTERNAL } from '../options-token.internal.js';
|
|
3
|
+
import { CloudflareWorkersWebSocketGatewayLifecycleService } from './cloudflare-workers-service.js';
|
|
4
|
+
function createCloudflareWorkersWebSocketProviders(options = {}) {
|
|
5
|
+
return [{
|
|
6
|
+
provide: WEBSOCKET_OPTIONS_INTERNAL,
|
|
7
|
+
useValue: options
|
|
8
|
+
}, CloudflareWorkersWebSocketGatewayLifecycleService];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Explicit Cloudflare Workers websocket module entrypoint.
|
|
13
|
+
*/
|
|
14
|
+
export class CloudflareWorkersWebSocketModule {
|
|
15
|
+
/**
|
|
16
|
+
* Registers the Cloudflare Workers websocket lifecycle service for request-upgrade gateway hosting.
|
|
17
|
+
*
|
|
18
|
+
* @param options Websocket gateway runtime options for guards, limits, heartbeat, and shutdown behavior.
|
|
19
|
+
* @returns A runtime module definition scoped to the Cloudflare Workers websocket adapter.
|
|
20
|
+
*/
|
|
21
|
+
static forRoot(options = {}) {
|
|
22
|
+
class CloudflareWorkersWebSocketRuntimeModule extends CloudflareWorkersWebSocketModule {}
|
|
23
|
+
return defineModule(CloudflareWorkersWebSocketRuntimeModule, {
|
|
24
|
+
providers: createCloudflareWorkersWebSocketProviders(options)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Container } from '@fluojs/di';
|
|
2
|
+
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy } from '@fluojs/runtime';
|
|
3
|
+
import type { HttpApplicationAdapter } from '@fluojs/http';
|
|
4
|
+
import type { WebSocketRoomService } from '../types.js';
|
|
5
|
+
import type { WebSocketModuleOptions } from './cloudflare-workers-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Boots Cloudflare Workers websocket gateways and manages their room lifecycle state.
|
|
8
|
+
*/
|
|
9
|
+
export declare class CloudflareWorkersWebSocketGatewayLifecycleService implements OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy, WebSocketRoomService {
|
|
10
|
+
private readonly runtimeContainer;
|
|
11
|
+
private readonly compiledModules;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
private readonly adapter;
|
|
14
|
+
private readonly moduleOptions;
|
|
15
|
+
private pendingUpgradeReservations;
|
|
16
|
+
private readonly roomSockets;
|
|
17
|
+
private shutdownPromise;
|
|
18
|
+
private readonly socketRegistry;
|
|
19
|
+
private readonly socketRooms;
|
|
20
|
+
constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, adapter: HttpApplicationAdapter, moduleOptions: WebSocketModuleOptions);
|
|
21
|
+
onApplicationBootstrap(): Promise<void>;
|
|
22
|
+
private assertNoServerBackedGatewayOptIn;
|
|
23
|
+
onApplicationShutdown(): Promise<void>;
|
|
24
|
+
onModuleDestroy(): Promise<void>;
|
|
25
|
+
private createBinding;
|
|
26
|
+
private groupDescriptorsByPath;
|
|
27
|
+
private bindConnectionHandlers;
|
|
28
|
+
private createConnectionHandlerState;
|
|
29
|
+
private attachConnectionListeners;
|
|
30
|
+
private getBufferedMessageCount;
|
|
31
|
+
private getQueuedMessageCount;
|
|
32
|
+
private maybeCompactBufferedMessages;
|
|
33
|
+
private clearBufferedMessages;
|
|
34
|
+
private maybeCompactQueuedMessages;
|
|
35
|
+
private clearQueuedMessages;
|
|
36
|
+
private bufferIncomingMessage;
|
|
37
|
+
private enqueueMessageDispatch;
|
|
38
|
+
private drainMessageQueue;
|
|
39
|
+
private normalizeMessage;
|
|
40
|
+
private enqueueDisconnectDispatch;
|
|
41
|
+
private resolveConnectionGateways;
|
|
42
|
+
private runConnectHandlers;
|
|
43
|
+
private finalizeConnectionBinding;
|
|
44
|
+
private replayBufferedConnectionEvents;
|
|
45
|
+
private findSocketId;
|
|
46
|
+
private resolveUpgradeRejection;
|
|
47
|
+
private closeOversizedPayload;
|
|
48
|
+
private resolveMaxConnectionCount;
|
|
49
|
+
private resolveReservedConnectionCount;
|
|
50
|
+
private tryReserveUpgradeSlot;
|
|
51
|
+
private releaseUpgradeReservation;
|
|
52
|
+
private resolveMaxPayloadBytes;
|
|
53
|
+
private shutdown;
|
|
54
|
+
private runShutdownLifecycle;
|
|
55
|
+
joinRoom(socketId: string, room: string): void;
|
|
56
|
+
leaveRoom(socketId: string, room: string): void;
|
|
57
|
+
broadcastToRoom(room: string, event: string, data: unknown): void;
|
|
58
|
+
getRooms(socketId: string): ReadonlySet<string>;
|
|
59
|
+
private unregisterSocket;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=cloudflare-workers-service.d.ts.map
|
|
@@ -0,0 +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,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEzI,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAa3D,OAAO,KAAK,EAA8B,oBAAoB,EAA6B,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,EAKV,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AA0GvC;;GAEG;AACH,qBACa,iDACX,YAAW,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,oBAAoB;IAS7F,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAXhC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgD;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAG3C,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,sBAAsB;IAGlD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB7C,OAAO,CAAC,gCAAgC;IAclC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,OAAO,CAAC,aAAa;IAqDrB,OAAO,CAAC,sBAAsB;YAmBhB,sBAAsB;IAgBpC,OAAO,CAAC,4BAA4B;IAqBpC,OAAO,CAAC,yBAAyB;IAyCjC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,sBAAsB;YAyDhB,iBAAiB;YAqBjB,gBAAgB;IAU9B,OAAO,CAAC,yBAAyB;YAsBnB,yBAAyB;YAczB,kBAAkB;YAgBlB,yBAAyB;YAUzB,8BAA8B;IAqB5C,OAAO,CAAC,YAAY;YAUN,uBAAuB;IAmDrC,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,sBAAsB;YAUhB,QAAQ;YAUR,oBAAoB;IAWlC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAmB9C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAc/C,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IA4BjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAU/C,OAAO,CAAC,gBAAgB;CAoBzB"}
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
let _initClass;
|
|
2
|
+
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
3
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
4
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
|
+
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
6
|
+
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
|
+
import { Inject } from '@fluojs/core';
|
|
8
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
|
+
import { dispatchGatewayDisconnect, dispatchGatewayMessage, discoverGatewayDescriptors, isFinitePositiveInteger, normalizeGatewayPath, resolveGatewayInstance, runGatewayHandlers } from '../internal/shared.js';
|
|
10
|
+
import { WEBSOCKET_OPTIONS_INTERNAL } from '../options-token.internal.js';
|
|
11
|
+
const DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET = 256;
|
|
12
|
+
const DEFAULT_MAX_WEBSOCKET_CONNECTIONS = 1_000;
|
|
13
|
+
const DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = 1_048_576;
|
|
14
|
+
const LIFECYCLE_LOG_CONTEXT = 'WebSocketGatewayLifecycleService';
|
|
15
|
+
const WEBSOCKET_OPEN_READY_STATE = 1;
|
|
16
|
+
function hasCloudflareWorkerWebSocketBindingHost(adapter) {
|
|
17
|
+
return 'configureWebSocketBinding' in adapter && typeof adapter.configureWebSocketBinding === 'function';
|
|
18
|
+
}
|
|
19
|
+
function resolveSupportedFetchStyleRealtimeCapability(adapter) {
|
|
20
|
+
if (typeof adapter.getRealtimeCapability !== 'function') {
|
|
21
|
+
throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires an HTTP adapter with getRealtimeCapability(). Use @fluojs/platform-cloudflare-workers together with @fluojs/websockets/cloudflare-workers.');
|
|
22
|
+
}
|
|
23
|
+
const capability = adapter.getRealtimeCapability();
|
|
24
|
+
if (capability.kind !== 'fetch-style' || capability.contract !== 'raw-websocket-expansion') {
|
|
25
|
+
throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires a fetch-style raw-websocket-expansion realtime capability from the selected HTTP adapter.');
|
|
26
|
+
}
|
|
27
|
+
if (capability.support !== 'supported') {
|
|
28
|
+
throw new Error(`Cloudflare Workers WebSocket gateway bootstrap requires supported fetch-style websocket hosting. ${capability.reason}`);
|
|
29
|
+
}
|
|
30
|
+
return capability;
|
|
31
|
+
}
|
|
32
|
+
function isWebSocketUpgradeRequest(request) {
|
|
33
|
+
return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
|
34
|
+
}
|
|
35
|
+
function isHttpExceptionLike(error) {
|
|
36
|
+
return typeof error === 'object' && error !== null && 'message' in error && 'status' in error;
|
|
37
|
+
}
|
|
38
|
+
function resolveMessageByteLength(message) {
|
|
39
|
+
if (typeof message === 'string') {
|
|
40
|
+
return Buffer.byteLength(message);
|
|
41
|
+
}
|
|
42
|
+
if (message instanceof Blob) {
|
|
43
|
+
return message.size;
|
|
44
|
+
}
|
|
45
|
+
return message.byteLength;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Boots Cloudflare Workers websocket gateways and manages their room lifecycle state.
|
|
50
|
+
*/
|
|
51
|
+
let _CloudflareWorkersWeb;
|
|
52
|
+
class CloudflareWorkersWebSocketGatewayLifecycleService {
|
|
53
|
+
static {
|
|
54
|
+
[_CloudflareWorkersWeb, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, HTTP_APPLICATION_ADAPTER, WEBSOCKET_OPTIONS_INTERNAL)], []).c;
|
|
55
|
+
}
|
|
56
|
+
pendingUpgradeReservations = 0;
|
|
57
|
+
roomSockets = new Map();
|
|
58
|
+
shutdownPromise;
|
|
59
|
+
socketRegistry = new Map();
|
|
60
|
+
socketRooms = new Map();
|
|
61
|
+
constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) {
|
|
62
|
+
this.runtimeContainer = runtimeContainer;
|
|
63
|
+
this.compiledModules = compiledModules;
|
|
64
|
+
this.logger = logger;
|
|
65
|
+
this.adapter = adapter;
|
|
66
|
+
this.moduleOptions = moduleOptions;
|
|
67
|
+
}
|
|
68
|
+
async onApplicationBootstrap() {
|
|
69
|
+
const descriptors = discoverGatewayDescriptors(this.compiledModules, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
70
|
+
if (descriptors.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.assertNoServerBackedGatewayOptIn(descriptors);
|
|
74
|
+
resolveSupportedFetchStyleRealtimeCapability(this.adapter);
|
|
75
|
+
if (!hasCloudflareWorkerWebSocketBindingHost(this.adapter)) {
|
|
76
|
+
throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires the selected adapter to expose Cloudflare websocket binding configuration. Use @fluojs/platform-cloudflare-workers with @fluojs/websockets/cloudflare-workers.');
|
|
77
|
+
}
|
|
78
|
+
this.adapter.configureWebSocketBinding(this.createBinding(descriptors));
|
|
79
|
+
}
|
|
80
|
+
assertNoServerBackedGatewayOptIn(descriptors) {
|
|
81
|
+
const descriptor = descriptors.find(entry => entry.serverBacked !== undefined);
|
|
82
|
+
if (!descriptor) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`@WebSocketGateway({ serverBacked }) is not supported on @fluojs/websockets/cloudflare-workers. Gateway path ${descriptor.path} must use the default fetch-style request-upgrade host instead.`);
|
|
86
|
+
}
|
|
87
|
+
async onApplicationShutdown() {
|
|
88
|
+
await this.shutdown();
|
|
89
|
+
}
|
|
90
|
+
async onModuleDestroy() {
|
|
91
|
+
await this.shutdown();
|
|
92
|
+
}
|
|
93
|
+
createBinding(descriptors) {
|
|
94
|
+
const descriptorsByPath = this.groupDescriptorsByPath(descriptors);
|
|
95
|
+
return {
|
|
96
|
+
fetch: async (request, host) => {
|
|
97
|
+
if (!isWebSocketUpgradeRequest(request)) {
|
|
98
|
+
return new Response(null, {
|
|
99
|
+
status: 426
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
let targetPath;
|
|
103
|
+
try {
|
|
104
|
+
targetPath = normalizeGatewayPath(new URL(request.url).pathname);
|
|
105
|
+
} catch {
|
|
106
|
+
return new Response(null, {
|
|
107
|
+
status: 400
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const matchedDescriptors = descriptorsByPath.get(targetPath);
|
|
111
|
+
if (!matchedDescriptors) {
|
|
112
|
+
return new Response(null, {
|
|
113
|
+
status: 404
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const rejection = await this.resolveUpgradeRejection(request, targetPath);
|
|
117
|
+
if (rejection) {
|
|
118
|
+
return new Response(rejection.body ?? null, {
|
|
119
|
+
headers: rejection.headers,
|
|
120
|
+
status: rejection.status
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
let response;
|
|
124
|
+
let serverSocket;
|
|
125
|
+
try {
|
|
126
|
+
({
|
|
127
|
+
response,
|
|
128
|
+
serverSocket
|
|
129
|
+
} = host.upgrade(request));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.releaseUpgradeReservation();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
serverSocket.accept();
|
|
135
|
+
void this.bindConnectionHandlers(serverSocket, request, matchedDescriptors).catch(error => {
|
|
136
|
+
this.unregisterSocket(this.findSocketId(serverSocket));
|
|
137
|
+
this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT);
|
|
138
|
+
serverSocket.close(1011, 'Internal server error');
|
|
139
|
+
});
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
groupDescriptorsByPath(descriptors) {
|
|
145
|
+
const descriptorsByPath = new Map();
|
|
146
|
+
for (const descriptor of descriptors) {
|
|
147
|
+
const current = descriptorsByPath.get(descriptor.path);
|
|
148
|
+
if (current) {
|
|
149
|
+
current.push(descriptor);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
descriptorsByPath.set(descriptor.path, [descriptor]);
|
|
153
|
+
}
|
|
154
|
+
return descriptorsByPath;
|
|
155
|
+
}
|
|
156
|
+
async bindConnectionHandlers(socket, request, descriptors) {
|
|
157
|
+
const state = this.createConnectionHandlerState(request, descriptors);
|
|
158
|
+
this.releaseUpgradeReservation();
|
|
159
|
+
this.socketRegistry.set(state.socketId, socket);
|
|
160
|
+
this.attachConnectionListeners(state, socket, request);
|
|
161
|
+
await this.resolveConnectionGateways(state);
|
|
162
|
+
await this.runConnectHandlers(state, socket);
|
|
163
|
+
await this.finalizeConnectionBinding(state, socket, request);
|
|
164
|
+
}
|
|
165
|
+
createConnectionHandlerState(request, descriptors) {
|
|
166
|
+
return {
|
|
167
|
+
bufferedDisconnect: undefined,
|
|
168
|
+
bufferedMessages: [],
|
|
169
|
+
bufferedMessagesStartIndex: 0,
|
|
170
|
+
descriptors,
|
|
171
|
+
enqueuedMessageCount: 0,
|
|
172
|
+
handlerQueue: Promise.resolve(),
|
|
173
|
+
handlersReady: false,
|
|
174
|
+
processingMessageQueue: false,
|
|
175
|
+
queuedMessages: [],
|
|
176
|
+
queuedMessagesStartIndex: 0,
|
|
177
|
+
request,
|
|
178
|
+
resolved: [],
|
|
179
|
+
socketId: crypto.randomUUID()
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
attachConnectionListeners(state, socket, request) {
|
|
183
|
+
socket.addEventListener('message', event => {
|
|
184
|
+
if (this.closeOversizedPayload(state.socketId, socket, event.data)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!state.handlersReady) {
|
|
188
|
+
this.bufferIncomingMessage(state, socket, event.data);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.enqueueMessageDispatch(state, socket, request, event.data);
|
|
192
|
+
});
|
|
193
|
+
socket.addEventListener('error', event => {
|
|
194
|
+
this.unregisterSocket(state.socketId);
|
|
195
|
+
this.logger.error('WebSocket gateway socket emitted an error.', event, LIFECYCLE_LOG_CONTEXT);
|
|
196
|
+
});
|
|
197
|
+
socket.addEventListener('close', event => {
|
|
198
|
+
this.unregisterSocket(state.socketId);
|
|
199
|
+
const closeEvent = event;
|
|
200
|
+
const disconnectEvent = {
|
|
201
|
+
code: closeEvent.code,
|
|
202
|
+
reason: closeEvent.reason
|
|
203
|
+
};
|
|
204
|
+
if (!state.handlersReady) {
|
|
205
|
+
state.bufferedDisconnect = disconnectEvent;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this.enqueueDisconnectDispatch(state, socket, disconnectEvent);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
getBufferedMessageCount(state) {
|
|
212
|
+
return state.bufferedMessages.length - state.bufferedMessagesStartIndex;
|
|
213
|
+
}
|
|
214
|
+
getQueuedMessageCount(state) {
|
|
215
|
+
return state.queuedMessages.length - state.queuedMessagesStartIndex;
|
|
216
|
+
}
|
|
217
|
+
maybeCompactBufferedMessages(state) {
|
|
218
|
+
if (state.bufferedMessagesStartIndex === 0 || state.bufferedMessagesStartIndex < state.bufferedMessages.length / 2) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
state.bufferedMessages = state.bufferedMessages.slice(state.bufferedMessagesStartIndex);
|
|
222
|
+
state.bufferedMessagesStartIndex = 0;
|
|
223
|
+
}
|
|
224
|
+
clearBufferedMessages(state) {
|
|
225
|
+
state.bufferedMessages = [];
|
|
226
|
+
state.bufferedMessagesStartIndex = 0;
|
|
227
|
+
}
|
|
228
|
+
maybeCompactQueuedMessages(state) {
|
|
229
|
+
if (state.queuedMessagesStartIndex === 0 || state.queuedMessagesStartIndex < state.queuedMessages.length / 2) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
state.queuedMessages = state.queuedMessages.slice(state.queuedMessagesStartIndex);
|
|
233
|
+
state.queuedMessagesStartIndex = 0;
|
|
234
|
+
}
|
|
235
|
+
clearQueuedMessages(state) {
|
|
236
|
+
state.queuedMessages = [];
|
|
237
|
+
state.queuedMessagesStartIndex = 0;
|
|
238
|
+
state.enqueuedMessageCount = 0;
|
|
239
|
+
}
|
|
240
|
+
bufferIncomingMessage(state, socket, message) {
|
|
241
|
+
const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
|
|
242
|
+
const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
|
|
243
|
+
if (this.getBufferedMessageCount(state) < limit) {
|
|
244
|
+
state.bufferedMessages.push(message);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (policy === 'close') {
|
|
248
|
+
socket.close(1013, 'Pending message buffer limit exceeded');
|
|
249
|
+
this.clearBufferedMessages(state);
|
|
250
|
+
this.logger.warn(`WebSocket connection ${state.socketId} exceeded pending message buffer limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (policy === 'drop-newest') {
|
|
254
|
+
this.logger.warn(`WebSocket connection ${state.socketId} dropped an incoming message due to pending buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
state.bufferedMessagesStartIndex += 1;
|
|
258
|
+
this.maybeCompactBufferedMessages(state);
|
|
259
|
+
state.bufferedMessages.push(message);
|
|
260
|
+
this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest pending message due to buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
|
|
261
|
+
}
|
|
262
|
+
enqueueMessageDispatch(state, socket, request, message) {
|
|
263
|
+
const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
|
|
264
|
+
const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
|
|
265
|
+
if (this.getQueuedMessageCount(state) >= limit) {
|
|
266
|
+
if (policy === 'close') {
|
|
267
|
+
socket.close(1013, 'Ready-state message queue limit exceeded');
|
|
268
|
+
this.clearQueuedMessages(state);
|
|
269
|
+
this.unregisterSocket(state.socketId);
|
|
270
|
+
this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (policy === 'drop-oldest') {
|
|
274
|
+
state.queuedMessagesStartIndex += 1;
|
|
275
|
+
this.maybeCompactQueuedMessages(state);
|
|
276
|
+
this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest ready-state message because queue limit (${String(limit)}) was reached.`, LIFECYCLE_LOG_CONTEXT);
|
|
277
|
+
} else {
|
|
278
|
+
this.logger.warn(`WebSocket connection ${state.socketId} dropped a ready-state message because queue limit (${String(limit)}) was reached.`, LIFECYCLE_LOG_CONTEXT);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
state.queuedMessages.push(message);
|
|
283
|
+
state.enqueuedMessageCount = this.getQueuedMessageCount(state);
|
|
284
|
+
if (state.processingMessageQueue) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
state.processingMessageQueue = true;
|
|
288
|
+
state.handlerQueue = this.drainMessageQueue(state, socket, request).finally(() => {
|
|
289
|
+
state.processingMessageQueue = false;
|
|
290
|
+
state.enqueuedMessageCount = this.getQueuedMessageCount(state);
|
|
291
|
+
}).catch(error => {
|
|
292
|
+
this.logger.error('WebSocket gateway message dispatch failed.', error, LIFECYCLE_LOG_CONTEXT);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async drainMessageQueue(state, socket, request) {
|
|
296
|
+
while (state.queuedMessagesStartIndex < state.queuedMessages.length) {
|
|
297
|
+
const nextMessage = state.queuedMessages[state.queuedMessagesStartIndex];
|
|
298
|
+
state.queuedMessagesStartIndex += 1;
|
|
299
|
+
state.enqueuedMessageCount = this.getQueuedMessageCount(state);
|
|
300
|
+
if (nextMessage === undefined) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const normalizedMessage = await this.normalizeMessage(nextMessage);
|
|
304
|
+
await dispatchGatewayMessage(state.resolved, socket, request, normalizedMessage, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
305
|
+
}
|
|
306
|
+
this.clearQueuedMessages(state);
|
|
307
|
+
}
|
|
308
|
+
async normalizeMessage(message) {
|
|
309
|
+
if (message instanceof Blob) {
|
|
310
|
+
return await message.arrayBuffer();
|
|
311
|
+
}
|
|
312
|
+
return message;
|
|
313
|
+
}
|
|
314
|
+
enqueueDisconnectDispatch(state, socket, disconnectEvent) {
|
|
315
|
+
state.handlerQueue = state.handlerQueue.then(async () => {
|
|
316
|
+
await dispatchGatewayDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
317
|
+
}).catch(error => {
|
|
318
|
+
this.logger.error('WebSocket gateway disconnect dispatch failed.', error, LIFECYCLE_LOG_CONTEXT);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async resolveConnectionGateways(state) {
|
|
322
|
+
const resolved = [];
|
|
323
|
+
for (const descriptor of state.descriptors) {
|
|
324
|
+
const instance = await resolveGatewayInstance(this.runtimeContainer, descriptor, this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
325
|
+
if (instance !== undefined) {
|
|
326
|
+
resolved.push({
|
|
327
|
+
descriptor,
|
|
328
|
+
instance
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
state.resolved = resolved;
|
|
333
|
+
}
|
|
334
|
+
async runConnectHandlers(state, socket) {
|
|
335
|
+
for (const {
|
|
336
|
+
descriptor,
|
|
337
|
+
instance
|
|
338
|
+
} of state.resolved) {
|
|
339
|
+
await runGatewayHandlers(instance, descriptor, 'connect', [socket, state.request, state.socketId], this.logger, LIFECYCLE_LOG_CONTEXT);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async finalizeConnectionBinding(state, socket, request) {
|
|
343
|
+
state.handlersReady = true;
|
|
344
|
+
await this.replayBufferedConnectionEvents(state, socket, request);
|
|
345
|
+
await state.handlerQueue;
|
|
346
|
+
}
|
|
347
|
+
async replayBufferedConnectionEvents(state, socket, request) {
|
|
348
|
+
for (let index = state.bufferedMessagesStartIndex; index < state.bufferedMessages.length; index += 1) {
|
|
349
|
+
const message = state.bufferedMessages[index];
|
|
350
|
+
if (message !== undefined) {
|
|
351
|
+
this.enqueueMessageDispatch(state, socket, request, message);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (state.bufferedDisconnect) {
|
|
355
|
+
this.enqueueDisconnectDispatch(state, socket, state.bufferedDisconnect);
|
|
356
|
+
state.bufferedDisconnect = undefined;
|
|
357
|
+
}
|
|
358
|
+
this.clearBufferedMessages(state);
|
|
359
|
+
}
|
|
360
|
+
findSocketId(target) {
|
|
361
|
+
for (const [socketId, socket] of this.socketRegistry) {
|
|
362
|
+
if (socket === target) {
|
|
363
|
+
return socketId;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
async resolveUpgradeRejection(request, path) {
|
|
369
|
+
if (!this.tryReserveUpgradeSlot()) {
|
|
370
|
+
return {
|
|
371
|
+
body: 'WebSocket connection limit exceeded.',
|
|
372
|
+
status: 429
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const guard = this.moduleOptions.upgrade?.guard;
|
|
376
|
+
if (!guard) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const result = await guard(request, {
|
|
381
|
+
activeConnectionCount: this.resolveReservedConnectionCount() - 1,
|
|
382
|
+
path
|
|
383
|
+
});
|
|
384
|
+
if (result === false) {
|
|
385
|
+
this.releaseUpgradeReservation();
|
|
386
|
+
return {
|
|
387
|
+
body: 'WebSocket upgrade rejected.',
|
|
388
|
+
status: 403
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (typeof result === 'object' && result !== null && 'status' in result) {
|
|
392
|
+
this.releaseUpgradeReservation();
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
this.releaseUpgradeReservation();
|
|
398
|
+
if (isHttpExceptionLike(error)) {
|
|
399
|
+
return {
|
|
400
|
+
body: error.message,
|
|
401
|
+
status: error.status
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
closeOversizedPayload(socketId, socket, message) {
|
|
408
|
+
const maxPayloadBytes = this.resolveMaxPayloadBytes();
|
|
409
|
+
if (resolveMessageByteLength(message) <= maxPayloadBytes) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
socket.close(1009, 'Payload too large');
|
|
413
|
+
this.logger.warn(`WebSocket connection ${socketId} exceeded payload limit (${String(maxPayloadBytes)} bytes). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
resolveMaxConnectionCount() {
|
|
417
|
+
const configured = this.moduleOptions.limits?.maxConnections;
|
|
418
|
+
if (!isFinitePositiveInteger(configured)) {
|
|
419
|
+
return DEFAULT_MAX_WEBSOCKET_CONNECTIONS;
|
|
420
|
+
}
|
|
421
|
+
return configured;
|
|
422
|
+
}
|
|
423
|
+
resolveReservedConnectionCount() {
|
|
424
|
+
return this.socketRegistry.size + this.pendingUpgradeReservations;
|
|
425
|
+
}
|
|
426
|
+
tryReserveUpgradeSlot() {
|
|
427
|
+
if (this.resolveReservedConnectionCount() >= this.resolveMaxConnectionCount()) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
this.pendingUpgradeReservations += 1;
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
releaseUpgradeReservation() {
|
|
434
|
+
if (this.pendingUpgradeReservations > 0) {
|
|
435
|
+
this.pendingUpgradeReservations -= 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
resolveMaxPayloadBytes() {
|
|
439
|
+
const configured = this.moduleOptions.limits?.maxPayloadBytes;
|
|
440
|
+
if (!isFinitePositiveInteger(configured)) {
|
|
441
|
+
return DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES;
|
|
442
|
+
}
|
|
443
|
+
return configured;
|
|
444
|
+
}
|
|
445
|
+
async shutdown() {
|
|
446
|
+
if (this.shutdownPromise) {
|
|
447
|
+
await this.shutdownPromise;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.shutdownPromise = this.runShutdownLifecycle();
|
|
451
|
+
await this.shutdownPromise;
|
|
452
|
+
}
|
|
453
|
+
async runShutdownLifecycle() {
|
|
454
|
+
if (hasCloudflareWorkerWebSocketBindingHost(this.adapter)) {
|
|
455
|
+
this.adapter.configureWebSocketBinding(undefined);
|
|
456
|
+
}
|
|
457
|
+
this.pendingUpgradeReservations = 0;
|
|
458
|
+
this.socketRegistry.clear();
|
|
459
|
+
this.socketRooms.clear();
|
|
460
|
+
this.roomSockets.clear();
|
|
461
|
+
}
|
|
462
|
+
joinRoom(socketId, room) {
|
|
463
|
+
let rooms = this.socketRooms.get(socketId);
|
|
464
|
+
if (!rooms) {
|
|
465
|
+
rooms = new Set();
|
|
466
|
+
this.socketRooms.set(socketId, rooms);
|
|
467
|
+
}
|
|
468
|
+
rooms.add(room);
|
|
469
|
+
let sockets = this.roomSockets.get(room);
|
|
470
|
+
if (!sockets) {
|
|
471
|
+
sockets = new Set();
|
|
472
|
+
this.roomSockets.set(room, sockets);
|
|
473
|
+
}
|
|
474
|
+
sockets.add(socketId);
|
|
475
|
+
}
|
|
476
|
+
leaveRoom(socketId, room) {
|
|
477
|
+
const rooms = this.socketRooms.get(socketId);
|
|
478
|
+
rooms?.delete(room);
|
|
479
|
+
if (rooms && rooms.size === 0) {
|
|
480
|
+
this.socketRooms.delete(socketId);
|
|
481
|
+
}
|
|
482
|
+
const sockets = this.roomSockets.get(room);
|
|
483
|
+
sockets?.delete(socketId);
|
|
484
|
+
if (sockets && sockets.size === 0) {
|
|
485
|
+
this.roomSockets.delete(room);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
broadcastToRoom(room, event, data) {
|
|
489
|
+
const socketIds = this.roomSockets.get(room);
|
|
490
|
+
if (!socketIds) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const message = JSON.stringify({
|
|
494
|
+
data,
|
|
495
|
+
event
|
|
496
|
+
});
|
|
497
|
+
for (const socketId of socketIds) {
|
|
498
|
+
const socket = this.socketRegistry.get(socketId);
|
|
499
|
+
if (!socket || socket.readyState !== WEBSOCKET_OPEN_READY_STATE) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
socket.send(message);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
this.unregisterSocket(socketId);
|
|
506
|
+
this.logger.warn(`WebSocket connection ${socketId} failed to send a room broadcast and was removed. ${error instanceof Error ? error.message : 'Unknown error.'}`, LIFECYCLE_LOG_CONTEXT);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
getRooms(socketId) {
|
|
511
|
+
const rooms = this.socketRooms.get(socketId);
|
|
512
|
+
if (!rooms) {
|
|
513
|
+
return new Set();
|
|
514
|
+
}
|
|
515
|
+
return new Set(rooms);
|
|
516
|
+
}
|
|
517
|
+
unregisterSocket(socketId) {
|
|
518
|
+
if (!socketId) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
this.socketRegistry.delete(socketId);
|
|
522
|
+
const rooms = this.socketRooms.get(socketId);
|
|
523
|
+
if (rooms) {
|
|
524
|
+
for (const room of rooms) {
|
|
525
|
+
const sockets = this.roomSockets.get(room);
|
|
526
|
+
sockets?.delete(socketId);
|
|
527
|
+
if (sockets && sockets.size === 0) {
|
|
528
|
+
this.roomSockets.delete(room);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
this.socketRooms.delete(socketId);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
static {
|
|
535
|
+
_initClass();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
export { _CloudflareWorkersWeb as CloudflareWorkersWebSocketGatewayLifecycleService };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WebSocketModuleOptions as SharedWebSocketModuleOptions } from '../types.js';
|
|
2
|
+
export type CloudflareWorkerWebSocketMessage = ArrayBuffer | ArrayBufferView | Blob | string;
|
|
3
|
+
export interface CloudflareWorkerWebSocket extends Pick<WebSocket, 'addEventListener' | 'close' | 'removeEventListener' | 'send'> {
|
|
4
|
+
readonly readyState: number;
|
|
5
|
+
accept(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface CloudflareWorkerWebSocketPair {
|
|
8
|
+
0: CloudflareWorkerWebSocket;
|
|
9
|
+
1: CloudflareWorkerWebSocket;
|
|
10
|
+
}
|
|
11
|
+
export interface CloudflareWorkerWebSocketUpgradeResult {
|
|
12
|
+
response: Response;
|
|
13
|
+
serverSocket: CloudflareWorkerWebSocket;
|
|
14
|
+
}
|
|
15
|
+
export interface CloudflareWorkerWebSocketUpgradeHost {
|
|
16
|
+
upgrade(request: Request): CloudflareWorkerWebSocketUpgradeResult;
|
|
17
|
+
}
|
|
18
|
+
export interface CloudflareWorkerWebSocketBinding {
|
|
19
|
+
fetch(request: Request, host: CloudflareWorkerWebSocketUpgradeHost): Response | Promise<Response>;
|
|
20
|
+
}
|
|
21
|
+
export interface CloudflareWorkerWebSocketBindingHost {
|
|
22
|
+
configureWebSocketBinding(binding: CloudflareWorkerWebSocketBinding | undefined): void;
|
|
23
|
+
}
|
|
24
|
+
export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = (payload: TEvents[K], socket: CloudflareWorkerWebSocket, request: Request) => void | Promise<void>;
|
|
25
|
+
export interface WebSocketGatewayContext {
|
|
26
|
+
request: Request;
|
|
27
|
+
socket: CloudflareWorkerWebSocket;
|
|
28
|
+
}
|
|
29
|
+
export type WebSocketModuleOptions = SharedWebSocketModuleOptions;
|
|
30
|
+
//# sourceMappingURL=cloudflare-workers-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-workers-types.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,IAAI,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAE1F,MAAM,MAAM,gCAAgC,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7F,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,GAAG,qBAAqB,GAAG,MAAM,CAAC;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,6BAA6B;IAC5C,CAAC,EAAE,yBAAyB,CAAC;IAC7B,CAAC,EAAE,yBAAyB,CAAC;CAC9B;AAED,MAAM,WAAW,sCAAsC;IACrD,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,yBAAyB,CAAC;CACzC;AAED,MAAM,WAAW,oCAAoC;IACnD,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,sCAAsC,CAAC;CACnE;AAED,MAAM,WAAW,gCAAgC;IAC/C,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,oCAAoC,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnG;AAED,MAAM,WAAW,oCAAoC;IACnD,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,SAAS,GAAG,IAAI,CAAC;CACxF;AAED,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,IAAI,CACpG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,OAAO,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,yBAAyB,CAAC;CACnC;AAED,MAAM,MAAM,sBAAsB,GAAG,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|