svelte-adapter-uws 0.1.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/index.js ADDED
@@ -0,0 +1,224 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { rollup } from 'rollup';
5
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
6
+ import commonjs from '@rollup/plugin-commonjs';
7
+ import json from '@rollup/plugin-json';
8
+
9
+ const files = fileURLToPath(new URL('./files', import.meta.url).href);
10
+
11
+ // Empty default WebSocket handler - subscribe/unsubscribe is handled
12
+ // by handler.js for ALL messages regardless of user handler.
13
+ const DEFAULT_WS_HANDLER = '// Built-in: subscribe/unsubscribe handled by the runtime\n';
14
+
15
+ /** @type {import('./index.js').default} */
16
+ export default function (opts = {}) {
17
+ const { out = 'build', precompress = true, envPrefix = '', healthCheckPath = '/healthz' } = opts;
18
+
19
+ // Normalize websocket config: true → {}, false/undefined → null
20
+ const websocket =
21
+ opts.websocket === true
22
+ ? {}
23
+ : opts.websocket || null;
24
+
25
+ return {
26
+ name: 'adapter-uws',
27
+
28
+ async adapt(builder) {
29
+ // Verify uWebSockets.js is installed - it's a native addon from GitHub,
30
+ // so install failures are common and produce confusing runtime errors
31
+ try {
32
+ await import('uWebSockets.js');
33
+ } catch {
34
+ throw new Error(
35
+ 'Could not load uWebSockets.js. Make sure it is installed:\n' +
36
+ ' npm install uNetworking/uWebSockets.js#v20.60.0\n\n' +
37
+ 'It is a native addon installed from GitHub (not npm) and may fail ' +
38
+ 'on some platforms. Check the uWebSockets.js README for details.'
39
+ );
40
+ }
41
+
42
+ const tmp = builder.getBuildDirectory('adapter-uws');
43
+
44
+ builder.rimraf(out);
45
+ builder.rimraf(tmp);
46
+ builder.mkdirp(tmp);
47
+
48
+ builder.log.minor('Copying assets');
49
+ builder.writeClient(`${out}/client${builder.config.kit.paths.base}`);
50
+ builder.writePrerendered(`${out}/prerendered${builder.config.kit.paths.base}`);
51
+
52
+ if (precompress) {
53
+ builder.log.minor('Compressing assets');
54
+ await Promise.all([
55
+ builder.compress(`${out}/client`),
56
+ builder.compress(`${out}/prerendered`)
57
+ ]);
58
+ }
59
+
60
+ builder.log.minor('Building server');
61
+
62
+ builder.writeServer(tmp);
63
+
64
+ writeFileSync(
65
+ `${tmp}/manifest.js`,
66
+ [
67
+ `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
68
+ `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`,
69
+ `export const base = ${JSON.stringify(builder.config.kit.paths.base)};`
70
+ ].join('\n\n')
71
+ );
72
+
73
+ // Write the WebSocket handler module
74
+ if (websocket) {
75
+ // Resolve the handler: explicit path > auto-discovered > built-in default
76
+ let handlerFile = websocket.handler;
77
+
78
+ if (!handlerFile) {
79
+ // Auto-discover src/hooks.ws.{js,ts,mjs}
80
+ const candidates = ['src/hooks.ws.js', 'src/hooks.ws.ts', 'src/hooks.ws.mjs'];
81
+ for (const candidate of candidates) {
82
+ if (existsSync(candidate)) {
83
+ handlerFile = candidate;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+
89
+ if (handlerFile) {
90
+ const handlerPath = path.resolve(handlerFile).replace(/\\/g, '/');
91
+ writeFileSync(
92
+ `${tmp}/ws-handler.js`,
93
+ `export * from '${handlerPath}';\n`
94
+ );
95
+ builder.log.minor(`WebSocket handler: ${handlerFile}`);
96
+ } else {
97
+ // No handler found - use built-in default (subscribe/unsubscribe only)
98
+ writeFileSync(`${tmp}/ws-handler.js`, DEFAULT_WS_HANDLER);
99
+ builder.log.minor('WebSocket enabled (built-in handler)');
100
+ }
101
+ } else {
102
+ // No WebSocket - empty module
103
+ writeFileSync(`${tmp}/ws-handler.js`, '// No WebSocket handler configured\n');
104
+ }
105
+
106
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
107
+
108
+ /** @type {Record<string, string>} */
109
+ const input = {
110
+ index: `${tmp}/index.js`,
111
+ manifest: `${tmp}/manifest.js`,
112
+ 'ws-handler': `${tmp}/ws-handler.js`
113
+ };
114
+
115
+ if (builder.hasServerInstrumentationFile?.()) {
116
+ input['instrumentation.server'] = `${tmp}/instrumentation.server.js`;
117
+ }
118
+
119
+ // Bundle the Vite output so that deployments only need
120
+ // their production dependencies. Anything in devDependencies
121
+ // will get included in the bundled code.
122
+ const bundle = await rollup({
123
+ input,
124
+ external: [
125
+ // dependencies could have deep exports, so we need a regex
126
+ ...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`)),
127
+ // uWebSockets.js must stay external - it's a native addon
128
+ /^uWebSockets\.js$/
129
+ ],
130
+ plugins: [
131
+ nodeResolve({
132
+ preferBuiltins: true,
133
+ exportConditions: ['node']
134
+ }),
135
+ commonjs({ strictRequires: true }),
136
+ json()
137
+ ]
138
+ });
139
+
140
+ await bundle.write({
141
+ dir: `${out}/server`,
142
+ format: 'esm',
143
+ sourcemap: true,
144
+ chunkFileNames: 'chunks/[name]-[hash].js'
145
+ });
146
+
147
+ // WebSocket config - serialized as globals for the runtime template
148
+ const wsPath = websocket?.path ?? '/ws';
149
+ if (wsPath[0] !== '/') {
150
+ throw new Error(
151
+ `websocket.path must start with '/' - got '${wsPath}'. ` +
152
+ `Use '/${wsPath}' instead.`
153
+ );
154
+ }
155
+ const wsOpts = {
156
+ maxPayloadLength: websocket?.maxPayloadLength ?? 16 * 1024,
157
+ idleTimeout: websocket?.idleTimeout ?? 120,
158
+ maxBackpressure: websocket?.maxBackpressure ?? 1024 * 1024,
159
+ sendPingsAutomatically: websocket?.sendPingsAutomatically ?? true,
160
+ compression: websocket?.compression ?? false,
161
+ allowedOrigins: websocket?.allowedOrigins ?? 'same-origin'
162
+ };
163
+
164
+ builder.copy(files, out, {
165
+ replace: {
166
+ ENV: './env.js',
167
+ HANDLER: './handler.js',
168
+ MANIFEST: './server/manifest.js',
169
+ SERVER: './server/index.js',
170
+ SHIMS: './shims.js',
171
+ WS_HANDLER: './server/ws-handler.js',
172
+ ENV_PREFIX: JSON.stringify(envPrefix),
173
+ PRECOMPRESS: JSON.stringify(precompress),
174
+ WS_ENABLED: JSON.stringify(!!websocket),
175
+ WS_PATH: JSON.stringify(wsPath),
176
+ WS_OPTIONS: JSON.stringify(wsOpts),
177
+ HEALTH_CHECK_PATH: JSON.stringify(healthCheckPath)
178
+ }
179
+ });
180
+
181
+ if (builder.hasServerInstrumentationFile?.()) {
182
+ builder.instrument?.({
183
+ entrypoint: `${out}/index.js`,
184
+ instrumentation: `${out}/server/instrumentation.server.js`,
185
+ module: {
186
+ exports: ['host', 'port']
187
+ }
188
+ });
189
+ }
190
+ },
191
+
192
+ supports: {
193
+ read: () => true,
194
+ instrumentation: () => true
195
+ },
196
+
197
+ emulate() {
198
+ return {
199
+ platform() {
200
+ // Vite plugin sets this when installed
201
+ if (globalThis.__uws_dev_platform) {
202
+ return globalThis.__uws_dev_platform;
203
+ }
204
+
205
+ // No Vite plugin - if WebSocket isn't configured, that's fine
206
+ if (!websocket) return undefined;
207
+
208
+ // WebSocket IS configured but plugin is missing - return a
209
+ // helpful proxy that throws only when actually used
210
+ const msg =
211
+ 'WebSocket platform not available in dev. Add the Vite plugin to your vite.config.js:\n\n' +
212
+ " import uwsDev from 'svelte-adapter-uws/vite';\n" +
213
+ ' export default { plugins: [sveltekit(), uwsDev()] };';
214
+ return new Proxy(/** @type {any} */ ({}), {
215
+ get(_, prop) {
216
+ if (typeof prop === 'symbol' || prop === 'then') return undefined;
217
+ throw new Error(msg);
218
+ }
219
+ });
220
+ }
221
+ };
222
+ }
223
+ };
224
+ }
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "svelte-adapter-uws",
3
+ "version": "0.1.0",
4
+ "description": "SvelteKit adapter for uWebSockets.js - high-performance C++ HTTP server with built-in WebSocket support",
5
+ "author": "Kevin Radziszewski",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lanteanio/svelte-adapter-uws.git"
10
+ },
11
+ "homepage": "https://github.com/lanteanio/svelte-adapter-uws#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/lanteanio/svelte-adapter-uws/issues"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./index.d.ts",
19
+ "default": "./index.js"
20
+ },
21
+ "./client": {
22
+ "types": "./client.d.ts",
23
+ "default": "./client.js"
24
+ },
25
+ "./vite": {
26
+ "types": "./vite.d.ts",
27
+ "default": "./vite.js"
28
+ }
29
+ },
30
+ "types": "./index.d.ts",
31
+ "files": [
32
+ "index.js",
33
+ "index.d.ts",
34
+ "client.js",
35
+ "client.d.ts",
36
+ "vite.js",
37
+ "vite.d.ts",
38
+ "files",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "scripts": {
43
+ "test": "vitest run",
44
+ "test:watch": "vitest"
45
+ },
46
+ "engines": {
47
+ "node": ">=20.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@sveltejs/kit": "^2.0.0",
51
+ "svelte": "^4.0.0 || ^5.0.0",
52
+ "uWebSockets.js": "uNetworking/uWebSockets.js#v20.60.0",
53
+ "ws": "^8.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "uWebSockets.js": {
57
+ "optional": true
58
+ },
59
+ "ws": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "dependencies": {
64
+ "@rollup/plugin-commonjs": "^28.0.0",
65
+ "@rollup/plugin-json": "^6.0.0",
66
+ "@rollup/plugin-node-resolve": "^16.0.0",
67
+ "rollup": "^4.0.0"
68
+ },
69
+ "keywords": [
70
+ "svelte",
71
+ "sveltekit",
72
+ "adapter",
73
+ "uwebsockets",
74
+ "uws",
75
+ "performance",
76
+ "websocket"
77
+ ],
78
+ "devDependencies": {
79
+ "vitest": "^4.0.18"
80
+ }
81
+ }
package/vite.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ export interface UWSDevOptions {
4
+ /**
5
+ * WebSocket endpoint path. Must match the adapter config.
6
+ * @default '/ws'
7
+ */
8
+ path?: string;
9
+
10
+ /**
11
+ * Path to a custom WebSocket handler module (same as adapter's `websocket.handler`).
12
+ * Auto-discovers `src/hooks.ws.{js,ts,mjs}` if not specified.
13
+ */
14
+ handler?: string;
15
+ }
16
+
17
+ /**
18
+ * Vite plugin for dev mode WebSocket support.
19
+ *
20
+ * Add this to your `vite.config.js` so the client store and
21
+ * `event.platform` work during development:
22
+ *
23
+ * ```js
24
+ * import { sveltekit } from '@sveltejs/kit/vite';
25
+ * import uwsDev from 'svelte-adapter-uws/vite';
26
+ *
27
+ * export default {
28
+ * plugins: [sveltekit(), uwsDev()]
29
+ * };
30
+ * ```
31
+ *
32
+ * That's it - `event.platform` works identically in dev and production:
33
+ *
34
+ * ```js
35
+ * export async function POST({ platform }) {
36
+ * platform.publish('todos', 'created', todo);
37
+ * }
38
+ * ```
39
+ *
40
+ * The adapter's `emulate()` hook provides `event.platform` in dev
41
+ * using the platform object created by this Vite plugin.
42
+ */
43
+ export default function uwsDev(options?: UWSDevOptions): Plugin;
44
+
45
+ declare global {
46
+ /** Dev-mode platform object - set by the Vite plugin. Same API as production `event.platform`. */
47
+ var __uws_dev_platform: import('./index.js').Platform | undefined;
48
+ }
package/vite.js ADDED
@@ -0,0 +1,310 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import path from 'node:path';
3
+ import { parseCookies } from './files/cookies.js';
4
+
5
+ /**
6
+ * Vite plugin that provides WebSocket support during development.
7
+ *
8
+ * Uses the same subscribe/unsubscribe/publish protocol as the production
9
+ * uWS handler, so the client store works identically in dev and prod.
10
+ *
11
+ * @param {{ path?: string, handler?: string }} [options]
12
+ * @returns {import('vite').Plugin}
13
+ */
14
+ export default function uwsDev(options = {}) {
15
+ const wsPath = options.path || '/ws';
16
+
17
+ /** @type {WebSocketServer} */
18
+ let wss;
19
+
20
+ /** @type {Map<import('ws').WebSocket, Set<string>>} */
21
+ const subscriptions = new Map();
22
+
23
+ /** @type {Set<import('ws').WebSocket>} */
24
+ const connections = new Set();
25
+
26
+ /** @type {Map<import('ws').WebSocket, object>} */
27
+ const wsWrappers = new Map();
28
+
29
+ /** @type {{ upgrade?: Function, open?: Function, message?: Function, close?: Function, drain?: Function }} */
30
+ let userHandlers = {};
31
+
32
+ /**
33
+ * Wrap a ws WebSocket to mimic the uWS WebSocket API.
34
+ * @param {import('ws').WebSocket} rawWs
35
+ * @param {unknown} userData
36
+ */
37
+ function wrapWebSocket(rawWs, userData) {
38
+ const topics = subscriptions.get(rawWs) || new Set();
39
+ return {
40
+ send(message, isBinary = false, _compress = false) {
41
+ if (rawWs.readyState !== 1) return 0;
42
+ rawWs.send(typeof message === 'string' ? message : Buffer.from(message));
43
+ return 1;
44
+ },
45
+ close() { rawWs.close(); },
46
+ end(code, message) { rawWs.close(code, message?.toString()); },
47
+ subscribe(topic) { topics.add(topic); return true; },
48
+ unsubscribe(topic) { topics.delete(topic); return true; },
49
+ publish(topic, message, isBinary = false, _compress = false) {
50
+ const msg = typeof message === 'string' ? message : Buffer.from(message);
51
+ for (const [ws, wsTopics] of subscriptions) {
52
+ if (ws !== rawWs && wsTopics.has(topic) && ws.readyState === 1) {
53
+ ws.send(msg);
54
+ }
55
+ }
56
+ return true;
57
+ },
58
+ isSubscribed(topic) { return topics.has(topic); },
59
+ getTopics() { return [...topics]; },
60
+ getUserData() { return userData; },
61
+ getBufferedAmount() { return rawWs.bufferedAmount || 0; },
62
+ getRemoteAddress() {
63
+ return new TextEncoder().encode(rawWs._socket?.remoteAddress || '127.0.0.1').buffer;
64
+ },
65
+ getRemoteAddressAsText() {
66
+ return new TextEncoder().encode(rawWs._socket?.remoteAddress || '127.0.0.1').buffer;
67
+ },
68
+ cork(fn) { fn(); }
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Publish to all subscribers of a topic.
74
+ * @param {string} topic
75
+ * @param {string} event
76
+ * @param {unknown} [data]
77
+ * @returns {boolean}
78
+ */
79
+ function publish(topic, event, data) {
80
+ const envelope = JSON.stringify({ topic, event, data });
81
+ let sent = false;
82
+ for (const [ws, topics] of subscriptions) {
83
+ if (topics.has(topic) && ws.readyState === 1) {
84
+ ws.send(envelope);
85
+ sent = true;
86
+ }
87
+ }
88
+ return sent;
89
+ }
90
+
91
+ /**
92
+ * Send to a single connection.
93
+ * @param {object} ws - Wrapped WebSocket
94
+ * @param {string} topic
95
+ * @param {string} event
96
+ * @param {unknown} [data]
97
+ * @returns {number}
98
+ */
99
+ function send(ws, topic, event, data) {
100
+ return ws.send(JSON.stringify({ topic, event, data }), false, false) ?? 1;
101
+ }
102
+
103
+ /**
104
+ * Send to connections matching a filter (by userData).
105
+ * @param {(userData: any) => boolean} filter
106
+ * @param {string} topic
107
+ * @param {string} event
108
+ * @param {unknown} [data]
109
+ * @returns {number}
110
+ */
111
+ function sendTo(filter, topic, event, data) {
112
+ const envelope = JSON.stringify({ topic, event, data });
113
+ let count = 0;
114
+ for (const [, wrapped] of wsWrappers) {
115
+ if (filter(wrapped.getUserData())) {
116
+ wrapped.send(envelope);
117
+ count++;
118
+ }
119
+ }
120
+ return count;
121
+ }
122
+
123
+ // Dev-mode platform - same API shape as production
124
+ const platform = {
125
+ publish,
126
+ send,
127
+ sendTo,
128
+ get connections() { return connections.size; },
129
+ subscribers(topic) {
130
+ let count = 0;
131
+ for (const [, topics] of subscriptions) {
132
+ if (topics.has(topic)) count++;
133
+ }
134
+ return count;
135
+ },
136
+ topic(name) {
137
+ return {
138
+ publish: (/** @type {string} */ event, /** @type {unknown} */ data) => publish(name, event, data),
139
+ created: (/** @type {unknown} */ data) => publish(name, 'created', data),
140
+ updated: (/** @type {unknown} */ data) => publish(name, 'updated', data),
141
+ deleted: (/** @type {unknown} */ data) => publish(name, 'deleted', data),
142
+ set: (/** @type {number} */ value) => publish(name, 'set', value),
143
+ increment: (/** @type {number} */ amount = 1) => publish(name, 'increment', amount),
144
+ decrement: (/** @type {number} */ amount = 1) => publish(name, 'decrement', amount)
145
+ };
146
+ }
147
+ };
148
+
149
+ // Expose platform globally so hooks/load functions can access it in dev
150
+ globalThis.__uws_dev_platform = platform;
151
+
152
+ /** @type {Promise<void>} */
153
+ let handlerReady;
154
+
155
+ return {
156
+ name: 'svelte-adapter-uws',
157
+ configureServer(server) {
158
+ wss = new WebSocketServer({ noServer: true });
159
+ const root = server.config.root;
160
+
161
+ // Load user's WebSocket handler - all exports, not just message
162
+ const handlerPath = options.handler
163
+ ? path.resolve(root, options.handler)
164
+ : null;
165
+
166
+ if (handlerPath) {
167
+ handlerReady = import(handlerPath).then((mod) => {
168
+ userHandlers = {
169
+ upgrade: mod.upgrade,
170
+ open: mod.open,
171
+ message: mod.message,
172
+ close: mod.close,
173
+ drain: mod.drain,
174
+ subscribe: mod.subscribe
175
+ };
176
+ }).catch((err) => {
177
+ console.error(`[adapter-uws] Failed to load WebSocket handler '${options.handler}':`, err);
178
+ });
179
+ } else {
180
+ // Auto-discover src/hooks.ws.{js,ts,mjs}
181
+ const candidates = ['src/hooks.ws.js', 'src/hooks.ws.ts', 'src/hooks.ws.mjs'];
182
+ handlerReady = (async () => {
183
+ for (const candidate of candidates) {
184
+ try {
185
+ const mod = await import(path.resolve(root, candidate));
186
+ userHandlers = {
187
+ upgrade: mod.upgrade,
188
+ open: mod.open,
189
+ message: mod.message,
190
+ close: mod.close,
191
+ drain: mod.drain,
192
+ subscribe: mod.subscribe
193
+ };
194
+ break;
195
+ } catch (err) {
196
+ // File genuinely doesn't exist - try next candidate
197
+ if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'ENOENT') continue;
198
+ // File exists but has errors - report and stop searching
199
+ console.error(`[adapter-uws] Error loading '${candidate}':`, err.message);
200
+ break;
201
+ }
202
+ }
203
+ })();
204
+ }
205
+
206
+ server.httpServer?.on('upgrade', async (req, socket, head) => {
207
+ const { pathname } = new URL(req.url || '', 'http://localhost');
208
+ if (pathname !== wsPath) return;
209
+
210
+ // If user has an upgrade handler, run it for auth
211
+ let userData = {};
212
+ await handlerReady;
213
+
214
+ if (userHandlers.upgrade) {
215
+ /** @type {Record<string, string>} */
216
+ const headers = {};
217
+ for (const [key, value] of Object.entries(req.headers)) {
218
+ if (typeof value === 'string') headers[key] = value;
219
+ else if (Array.isArray(value)) headers[key] = value.join(', ');
220
+ }
221
+
222
+ try {
223
+ const result = await Promise.resolve(
224
+ userHandlers.upgrade({
225
+ headers,
226
+ cookies: parseCookies(headers['cookie']),
227
+ url: pathname,
228
+ remoteAddress: req.socket?.remoteAddress || ''
229
+ })
230
+ );
231
+ if (result === false) {
232
+ socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\n\r\nUnauthorized');
233
+ socket.destroy();
234
+ return;
235
+ }
236
+ userData = result || {};
237
+ } catch (err) {
238
+ console.error('[adapter-uws] WebSocket upgrade error:', err);
239
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\nInternal Server Error');
240
+ socket.destroy();
241
+ return;
242
+ }
243
+ }
244
+
245
+ wss.handleUpgrade(req, socket, head, (ws) => {
246
+ /** @type {any} */ (ws).__userData = userData;
247
+ wss.emit('connection', ws, req);
248
+ });
249
+ });
250
+
251
+ wss.on('connection', (ws) => {
252
+ connections.add(ws);
253
+ subscriptions.set(ws, new Set());
254
+
255
+ const userData = /** @type {any} */ (ws).__userData || {};
256
+ const wrapped = wrapWebSocket(ws, userData);
257
+ wsWrappers.set(ws, wrapped);
258
+
259
+ // Call user open handler
260
+ userHandlers.open?.(wrapped);
261
+
262
+ ws.on('message', async (raw, isBinary) => {
263
+ // Convert to ArrayBuffer (matching uWS interface)
264
+ const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(/** @type {any} */ (raw));
265
+ const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
266
+
267
+ // Handle subscribe/unsubscribe from client store
268
+ if (!isBinary && buf.byteLength < 512) {
269
+ try {
270
+ const msg = JSON.parse(buf.toString());
271
+ if (msg.type === 'subscribe' && typeof msg.topic === 'string') {
272
+ if (userHandlers.subscribe && userHandlers.subscribe(wrapped, msg.topic) === false) {
273
+ return;
274
+ }
275
+ subscriptions.get(ws)?.add(msg.topic);
276
+ return;
277
+ }
278
+ if (msg.type === 'unsubscribe' && typeof msg.topic === 'string') {
279
+ subscriptions.get(ws)?.delete(msg.topic);
280
+ return;
281
+ }
282
+ } catch {
283
+ // Not JSON - fall through to user handler
284
+ }
285
+ }
286
+
287
+ // Delegate to user handler
288
+ await handlerReady;
289
+ if (userHandlers.message) {
290
+ userHandlers.message(wrapped, arrayBuffer, !!isBinary);
291
+ }
292
+ });
293
+
294
+ ws.on('close', (code, reason) => {
295
+ const reasonBuf = reason || Buffer.alloc(0);
296
+ const reasonAB = reasonBuf.buffer.slice(reasonBuf.byteOffset, reasonBuf.byteOffset + reasonBuf.byteLength);
297
+ userHandlers.close?.(wrapped, code, reasonAB);
298
+ connections.delete(ws);
299
+ subscriptions.delete(ws);
300
+ wsWrappers.delete(ws);
301
+ });
302
+ });
303
+
304
+ console.log(`[adapter-uws] Dev WebSocket endpoint at ${wsPath}`);
305
+ if (wsPath !== '/ws') {
306
+ console.log(`[adapter-uws] Client must match: connect({ path: '${wsPath}' })`);
307
+ }
308
+ }
309
+ };
310
+ }