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/LICENSE +21 -0
- package/README.md +1543 -0
- package/client.d.ts +356 -0
- package/client.js +571 -0
- package/files/cookies.js +25 -0
- package/files/env.js +41 -0
- package/files/handler.js +898 -0
- package/files/index.js +116 -0
- package/files/shims.js +21 -0
- package/files/utils.js +136 -0
- package/index.d.ts +396 -0
- package/index.js +224 -0
- package/package.json +81 -0
- package/vite.d.ts +48 -0
- package/vite.js +310 -0
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
|
+
}
|