@pikku/bun-server 0.12.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/CHANGELOG.md +29 -0
- package/dist/bun-event-hub-service.d.ts +14 -0
- package/dist/bun-event-hub-service.js +30 -0
- package/dist/bun-event-hub-service.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/pikku-bun-server.d.ts +24 -0
- package/dist/pikku-bun-server.js +132 -0
- package/dist/pikku-bun-server.js.map +1 -0
- package/package.json +28 -0
- package/run-tests.sh +54 -0
- package/src/bun-event-hub-service.test.ts +73 -0
- package/src/bun-event-hub-service.ts +54 -0
- package/src/index.ts +6 -0
- package/src/pikku-bun-server.test.ts +62 -0
- package/src/pikku-bun-server.ts +190 -0
- package/tsconfig.json +18 -0
- package/tsconfig.tsbuildinfo +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @pikku/bun-server
|
|
2
|
+
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d5c3c85: feat: bun first-class support — new `@pikku/bun-server` runtime and `@pikku/kysely-bun-sqlite` dialect, bun template, CI matrix with `package-manager: [yarn, bun]`, and bun verifier.
|
|
8
|
+
- e443e94: feat(deploy): standalone provider can target the bun runtime
|
|
9
|
+
|
|
10
|
+
`pikku deploy plan|apply --provider standalone --runtime bun` now generates a
|
|
11
|
+
`@pikku/bun-server` entry (native `Bun.serve` WebSockets, no `ws` package) and
|
|
12
|
+
compiles the bundle into a single self-contained executable via
|
|
13
|
+
`bun build --compile` — no runtime needed on the target host. The default
|
|
14
|
+
remains `--runtime node`, which is unchanged (ships `bundle.js`, run with
|
|
15
|
+
`node bundle.js`).
|
|
16
|
+
|
|
17
|
+
`PikkuBunServer` now accepts an injectable `eventHub` in its options. Inject the
|
|
18
|
+
same `BunEventHubService` you pass to `createSingletonServices` so functions and
|
|
19
|
+
the WebSocket transport share one hub — otherwise a function's
|
|
20
|
+
`eventHub.publish(...)` targets a different hub than the one holding the live
|
|
21
|
+
sockets and broadcasts never reach connected clients. The standalone bun entry
|
|
22
|
+
and the `bun` template now wire this shared hub, fixing cross-connection /
|
|
23
|
+
cross-transport channel pub-sub on bun.
|
|
24
|
+
|
|
25
|
+
Also removes the unused `@yao-pkg/pkg` dependency and its stale type shim from
|
|
26
|
+
`@pikku/deploy-standalone` (the pkg-based binary path was dropped in #489).
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [92cd5b1]
|
|
29
|
+
- @pikku/core@0.12.38
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ServerWebSocket, Server } from 'bun';
|
|
2
|
+
type AnyServer = Server<unknown>;
|
|
3
|
+
import type { EventHubService } from '@pikku/core/channel';
|
|
4
|
+
export declare class BunEventHubService<Mappings extends Record<string, unknown> = {}> implements EventHubService<Mappings> {
|
|
5
|
+
private sockets;
|
|
6
|
+
private server;
|
|
7
|
+
setServer(server: AnyServer): void;
|
|
8
|
+
subscribe<T extends keyof Mappings>(topic: T, channelId: string): Promise<void>;
|
|
9
|
+
unsubscribe<T extends keyof Mappings>(topic: T, channelId: string): Promise<void>;
|
|
10
|
+
publish<T extends keyof Mappings>(topic: T, channelId: string | null, message: Mappings[T], isBinary?: boolean): Promise<void>;
|
|
11
|
+
onChannelOpened(channelId: string, ws: ServerWebSocket<unknown>): Promise<void>;
|
|
12
|
+
onChannelClosed(channelId: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class BunEventHubService {
|
|
2
|
+
sockets = new Map();
|
|
3
|
+
server = null;
|
|
4
|
+
setServer(server) {
|
|
5
|
+
this.server = server;
|
|
6
|
+
}
|
|
7
|
+
async subscribe(topic, channelId) {
|
|
8
|
+
this.sockets.get(channelId)?.subscribe(topic);
|
|
9
|
+
}
|
|
10
|
+
async unsubscribe(topic, channelId) {
|
|
11
|
+
this.sockets.get(channelId)?.unsubscribe(topic);
|
|
12
|
+
}
|
|
13
|
+
async publish(topic, channelId, message, isBinary) {
|
|
14
|
+
if (!this.server)
|
|
15
|
+
return;
|
|
16
|
+
if (isBinary) {
|
|
17
|
+
this.server.publish(topic, message, true);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.server.publish(topic, JSON.stringify(message), false);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async onChannelOpened(channelId, ws) {
|
|
24
|
+
this.sockets.set(channelId, ws);
|
|
25
|
+
}
|
|
26
|
+
async onChannelClosed(channelId) {
|
|
27
|
+
this.sockets.delete(channelId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=bun-event-hub-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bun-event-hub-service.js","sourceRoot":"","sources":["../src/bun-event-hub-service.ts"],"names":[],"mappings":"AAKA,MAAM,OAAO,kBAAkB;IAGrB,OAAO,GAA0C,IAAI,GAAG,EAAE,CAAA;IAC1D,MAAM,GAAqB,IAAI,CAAA;IAEhC,SAAS,CAAC,MAAiB;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAEM,KAAK,CAAC,SAAS,CACpB,KAAQ,EACR,SAAiB;QAEjB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,KAAe,CAAC,CAAA;IACzD,CAAC;IAEM,KAAK,CAAC,WAAW,CACtB,KAAQ,EACR,SAAiB;QAEjB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,KAAe,CAAC,CAAA;IAC3D,CAAC;IAEM,KAAK,CAAC,OAAO,CAClB,KAAQ,EACR,SAAwB,EACxB,OAAoB,EACpB,QAAkB;QAElB,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAM;QACxB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAe,EAAE,OAAc,EAAE,IAAI,CAAC,CAAA;QAC5D,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAe,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAA;QACtE,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,eAAe,CAC1B,SAAiB,EACjB,EAA4B;QAE5B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACjC,CAAC;IAEM,KAAK,CAAC,eAAe,CAAC,SAAiB;QAC5C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAChC,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CoreConfig } from '@pikku/core';
|
|
2
|
+
import type { Logger } from '@pikku/core/services';
|
|
3
|
+
import { type RunHTTPWiringOptions } from '@pikku/core/http';
|
|
4
|
+
import { BunEventHubService } from './bun-event-hub-service.js';
|
|
5
|
+
export type BunServerConfig = CoreConfig & {
|
|
6
|
+
port: number;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
healthCheckPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export type PikkuBunServerOptions = RunHTTPWiringOptions & {
|
|
11
|
+
eventHub?: BunEventHubService;
|
|
12
|
+
};
|
|
13
|
+
export declare class PikkuBunServer {
|
|
14
|
+
private readonly config;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private server;
|
|
17
|
+
private readonly eventHub;
|
|
18
|
+
private readonly options;
|
|
19
|
+
constructor(config: BunServerConfig, logger: Logger, options?: PikkuBunServerOptions);
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
enableExitOnSignals(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { stopSingletonServices } from '@pikku/core';
|
|
2
|
+
import { fetchData, PikkuFetchHTTPRequest, PikkuFetchHTTPResponse, logRoutes as logRegisterRoutes, } from '@pikku/core/http';
|
|
3
|
+
import { logChannels } from '@pikku/core/channel';
|
|
4
|
+
import { runLocalChannel } from '@pikku/core/channel/local';
|
|
5
|
+
import { compileAllSchemas } from '@pikku/core/schema';
|
|
6
|
+
import { BunEventHubService } from './bun-event-hub-service.js';
|
|
7
|
+
const isSerializable = (data) => !(typeof data === 'string' ||
|
|
8
|
+
data instanceof ArrayBuffer ||
|
|
9
|
+
data instanceof Uint8Array ||
|
|
10
|
+
data instanceof Int8Array ||
|
|
11
|
+
data instanceof Uint16Array ||
|
|
12
|
+
data instanceof Int16Array ||
|
|
13
|
+
data instanceof Uint32Array ||
|
|
14
|
+
data instanceof Int32Array ||
|
|
15
|
+
data instanceof Float32Array ||
|
|
16
|
+
data instanceof Float64Array);
|
|
17
|
+
export class PikkuBunServer {
|
|
18
|
+
config;
|
|
19
|
+
logger;
|
|
20
|
+
server = null;
|
|
21
|
+
eventHub;
|
|
22
|
+
options;
|
|
23
|
+
constructor(config, logger, options = {}) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
const { eventHub, ...httpOptions } = options;
|
|
27
|
+
this.eventHub = eventHub ?? new BunEventHubService();
|
|
28
|
+
this.options = httpOptions;
|
|
29
|
+
}
|
|
30
|
+
async init() {
|
|
31
|
+
compileAllSchemas(this.logger);
|
|
32
|
+
logRegisterRoutes(this.logger);
|
|
33
|
+
logChannels(this.logger);
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
const { config, logger, options, eventHub } = this;
|
|
37
|
+
this.server = Bun.serve({
|
|
38
|
+
port: config.port,
|
|
39
|
+
hostname: config.hostname,
|
|
40
|
+
fetch: async (req, server) => {
|
|
41
|
+
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
42
|
+
const pikkuReq = new PikkuFetchHTTPRequest(req);
|
|
43
|
+
const pikkuRes = new PikkuFetchHTTPResponse();
|
|
44
|
+
const channelHandler = await runLocalChannel({
|
|
45
|
+
channelId: crypto.randomUUID(),
|
|
46
|
+
request: pikkuReq,
|
|
47
|
+
response: pikkuRes,
|
|
48
|
+
route: new URL(req.url).pathname,
|
|
49
|
+
});
|
|
50
|
+
if (!channelHandler) {
|
|
51
|
+
return new Response('Forbidden', { status: 403 });
|
|
52
|
+
}
|
|
53
|
+
const upgraded = server.upgrade(req, { data: { channelHandler } });
|
|
54
|
+
if (upgraded)
|
|
55
|
+
return undefined;
|
|
56
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
if (config.healthCheckPath &&
|
|
59
|
+
new URL(req.url).pathname === config.healthCheckPath) {
|
|
60
|
+
return new Response('{"ok":true}', {
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const pikkuReq = new PikkuFetchHTTPRequest(req);
|
|
65
|
+
const pikkuRes = new PikkuFetchHTTPResponse();
|
|
66
|
+
await fetchData(pikkuReq, pikkuRes, {
|
|
67
|
+
respondWith404: true,
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
return pikkuRes.toResponse();
|
|
71
|
+
},
|
|
72
|
+
websocket: {
|
|
73
|
+
open: (ws) => {
|
|
74
|
+
const { channelHandler } = ws.data;
|
|
75
|
+
channelHandler.registerOnSend((data) => {
|
|
76
|
+
ws.send(isSerializable(data) ? JSON.stringify(data) : data);
|
|
77
|
+
});
|
|
78
|
+
channelHandler.registerOnSendBinary((data) => {
|
|
79
|
+
ws.send(data, true);
|
|
80
|
+
});
|
|
81
|
+
channelHandler.registerOnClose(() => {
|
|
82
|
+
ws.close();
|
|
83
|
+
});
|
|
84
|
+
eventHub.onChannelOpened(channelHandler.channelId, ws);
|
|
85
|
+
channelHandler.open();
|
|
86
|
+
},
|
|
87
|
+
message: async (ws, message) => {
|
|
88
|
+
const { channelHandler } = ws.data;
|
|
89
|
+
if (typeof message === 'string') {
|
|
90
|
+
const result = await channelHandler.message(message);
|
|
91
|
+
if (result)
|
|
92
|
+
ws.send(JSON.stringify(result));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const bytes = message instanceof ArrayBuffer
|
|
96
|
+
? new Uint8Array(message)
|
|
97
|
+
: new Uint8Array(message.buffer, message.byteOffset, message.byteLength);
|
|
98
|
+
const result = await channelHandler.binaryMessage(bytes);
|
|
99
|
+
if (result)
|
|
100
|
+
channelHandler.sendBinary(result);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
close: (ws) => {
|
|
104
|
+
const { channelHandler } = ws.data;
|
|
105
|
+
eventHub.onChannelClosed(channelHandler.channelId);
|
|
106
|
+
channelHandler.close();
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
eventHub.setServer(this.server);
|
|
111
|
+
logger.info(`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`);
|
|
112
|
+
}
|
|
113
|
+
async stop() {
|
|
114
|
+
await this.server?.stop();
|
|
115
|
+
this.server = null;
|
|
116
|
+
}
|
|
117
|
+
enableExitOnSignals() {
|
|
118
|
+
const shutdown = async (signal) => {
|
|
119
|
+
this.logger.info(`pikku-bun-server: ${signal} received, stopping`);
|
|
120
|
+
try {
|
|
121
|
+
await stopSingletonServices();
|
|
122
|
+
await this.stop();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
129
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=pikku-bun-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pikku/bun-server",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "Pikku server adapter for Bun (Bun.serve)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "yasser.fadl@gmail.com",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"tsc": "tsc",
|
|
12
|
+
"build": "tsc -b",
|
|
13
|
+
"test": "bash run-tests.sh",
|
|
14
|
+
"test:watch": "bash run-tests.sh --watch",
|
|
15
|
+
"test:coverage": "bash run-tests.sh --coverage",
|
|
16
|
+
"prepublishOnly": "bun run build"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@pikku/core": "^0.12.38"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/run-tests.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
shopt -s nullglob
|
|
4
|
+
|
|
5
|
+
watch_mode=false
|
|
6
|
+
coverage_mode=false
|
|
7
|
+
|
|
8
|
+
while [[ $# -gt 0 ]]; do
|
|
9
|
+
case $1 in
|
|
10
|
+
--watch)
|
|
11
|
+
watch_mode=true
|
|
12
|
+
shift
|
|
13
|
+
;;
|
|
14
|
+
--coverage)
|
|
15
|
+
coverage_mode=true
|
|
16
|
+
shift
|
|
17
|
+
;;
|
|
18
|
+
*)
|
|
19
|
+
echo "Unknown option: $1"
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
files=()
|
|
26
|
+
while IFS= read -r -d '' file; do
|
|
27
|
+
files+=("$file")
|
|
28
|
+
done < <(find src -type f -name "*.test.ts" -print0)
|
|
29
|
+
|
|
30
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
31
|
+
echo "No test files found"
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ "$coverage_mode" = true ]; then
|
|
36
|
+
# Bun writes coverage/lcov.info and instruments imported dist files too.
|
|
37
|
+
# Re-emit a package-root lcov.info containing only src/ records so the
|
|
38
|
+
# repo-wide unit-coverage merge — which expects <pkg>/lcov.info and prefixes
|
|
39
|
+
# its SF paths — maps them correctly, exactly like the node packages'
|
|
40
|
+
# --test-reporter=lcov output.
|
|
41
|
+
bun test --coverage --coverage-reporter=lcov "${files[@]}"
|
|
42
|
+
status=$?
|
|
43
|
+
awk '/^SF:/{keep=/^SF:src\//} keep' coverage/lcov.info > lcov.info 2>/dev/null || true
|
|
44
|
+
rm -rf coverage
|
|
45
|
+
exit $status
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
bun_cmd="bun test"
|
|
49
|
+
|
|
50
|
+
if [ "$watch_mode" = true ]; then
|
|
51
|
+
bun_cmd="$bun_cmd --watch"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
$bun_cmd "${files[@]}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { BunEventHubService } from './bun-event-hub-service.js'
|
|
4
|
+
|
|
5
|
+
type FakeSocket = {
|
|
6
|
+
subscribe: (topic: string) => void
|
|
7
|
+
unsubscribe: (topic: string) => void
|
|
8
|
+
subscribed: string[]
|
|
9
|
+
unsubscribed: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const makeSocket = (): FakeSocket => {
|
|
13
|
+
const socket: FakeSocket = {
|
|
14
|
+
subscribed: [],
|
|
15
|
+
unsubscribed: [],
|
|
16
|
+
subscribe: (topic) => socket.subscribed.push(topic),
|
|
17
|
+
unsubscribe: (topic) => socket.unsubscribed.push(topic),
|
|
18
|
+
}
|
|
19
|
+
return socket
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('BunEventHubService', () => {
|
|
23
|
+
test('subscribe/unsubscribe proxy to the registered socket', async () => {
|
|
24
|
+
const hub = new BunEventHubService()
|
|
25
|
+
const socket = makeSocket()
|
|
26
|
+
await hub.onChannelOpened('c1', socket as any)
|
|
27
|
+
|
|
28
|
+
await hub.subscribe('news', 'c1')
|
|
29
|
+
await hub.unsubscribe('news', 'c1')
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(socket.subscribed, ['news'])
|
|
32
|
+
assert.deepEqual(socket.unsubscribed, ['news'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('subscribe is a no-op for unknown channels', async () => {
|
|
36
|
+
const hub = new BunEventHubService()
|
|
37
|
+
await assert.doesNotReject(hub.subscribe('news', 'missing'))
|
|
38
|
+
await assert.doesNotReject(hub.unsubscribe('news', 'missing'))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('publish does nothing before a server is set', async () => {
|
|
42
|
+
const hub = new BunEventHubService()
|
|
43
|
+
await assert.doesNotReject(hub.publish('news', null, { a: 1 }))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('publish serializes JSON messages and forwards binary as-is', async () => {
|
|
47
|
+
const hub = new BunEventHubService()
|
|
48
|
+
const calls: Array<[string, unknown, boolean]> = []
|
|
49
|
+
hub.setServer({
|
|
50
|
+
publish: (topic: string, data: unknown, binary: boolean) =>
|
|
51
|
+
calls.push([topic, data, binary]),
|
|
52
|
+
} as any)
|
|
53
|
+
|
|
54
|
+
await hub.publish('news', null, { hello: 'world' })
|
|
55
|
+
const bin = new Uint8Array([1, 2, 3])
|
|
56
|
+
await hub.publish('bin', null, bin as any, true)
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(calls[0], ['news', JSON.stringify({ hello: 'world' }), false])
|
|
59
|
+
assert.equal(calls[1][0], 'bin')
|
|
60
|
+
assert.equal(calls[1][1], bin)
|
|
61
|
+
assert.equal(calls[1][2], true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('onChannelClosed removes the socket so later subscribes are no-ops', async () => {
|
|
65
|
+
const hub = new BunEventHubService()
|
|
66
|
+
const socket = makeSocket()
|
|
67
|
+
await hub.onChannelOpened('c1', socket as any)
|
|
68
|
+
await hub.onChannelClosed('c1')
|
|
69
|
+
|
|
70
|
+
await hub.subscribe('news', 'c1')
|
|
71
|
+
assert.deepEqual(socket.subscribed, [])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ServerWebSocket, Server } from 'bun'
|
|
2
|
+
|
|
3
|
+
type AnyServer = Server<unknown>
|
|
4
|
+
import type { EventHubService } from '@pikku/core/channel'
|
|
5
|
+
|
|
6
|
+
export class BunEventHubService<
|
|
7
|
+
Mappings extends Record<string, unknown> = {},
|
|
8
|
+
> implements EventHubService<Mappings> {
|
|
9
|
+
private sockets: Map<string, ServerWebSocket<unknown>> = new Map()
|
|
10
|
+
private server: AnyServer | null = null
|
|
11
|
+
|
|
12
|
+
public setServer(server: AnyServer): void {
|
|
13
|
+
this.server = server
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async subscribe<T extends keyof Mappings>(
|
|
17
|
+
topic: T,
|
|
18
|
+
channelId: string
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
this.sockets.get(channelId)?.subscribe(topic as string)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async unsubscribe<T extends keyof Mappings>(
|
|
24
|
+
topic: T,
|
|
25
|
+
channelId: string
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
this.sockets.get(channelId)?.unsubscribe(topic as string)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async publish<T extends keyof Mappings>(
|
|
31
|
+
topic: T,
|
|
32
|
+
channelId: string | null,
|
|
33
|
+
message: Mappings[T],
|
|
34
|
+
isBinary?: boolean
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
if (!this.server) return
|
|
37
|
+
if (isBinary) {
|
|
38
|
+
this.server.publish(topic as string, message as any, true)
|
|
39
|
+
} else {
|
|
40
|
+
this.server.publish(topic as string, JSON.stringify(message), false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async onChannelOpened(
|
|
45
|
+
channelId: string,
|
|
46
|
+
ws: ServerWebSocket<unknown>
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
this.sockets.set(channelId, ws)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async onChannelClosed(channelId: string): Promise<void> {
|
|
52
|
+
this.sockets.delete(channelId)
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { setSingletonServices } from '@pikku/core'
|
|
4
|
+
import { resetPikkuState } from '@pikku/core/internal'
|
|
5
|
+
import type { Logger } from '@pikku/core/services'
|
|
6
|
+
import { PikkuBunServer } from './pikku-bun-server.js'
|
|
7
|
+
|
|
8
|
+
const PORT = 47817
|
|
9
|
+
const HEALTH = '/__health'
|
|
10
|
+
|
|
11
|
+
const noopLogger = {
|
|
12
|
+
info: () => {},
|
|
13
|
+
error: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
debug: () => {},
|
|
16
|
+
trace: () => {},
|
|
17
|
+
setLevel: () => {},
|
|
18
|
+
} as unknown as Logger
|
|
19
|
+
|
|
20
|
+
describe('PikkuBunServer', () => {
|
|
21
|
+
let server: PikkuBunServer
|
|
22
|
+
|
|
23
|
+
before(async () => {
|
|
24
|
+
setSingletonServices({
|
|
25
|
+
logger: noopLogger,
|
|
26
|
+
schema: {
|
|
27
|
+
compileSchema: () => {},
|
|
28
|
+
getSchemaNames: () => new Set<string>(),
|
|
29
|
+
},
|
|
30
|
+
} as any)
|
|
31
|
+
server = new PikkuBunServer(
|
|
32
|
+
{ port: PORT, hostname: 'localhost', healthCheckPath: HEALTH },
|
|
33
|
+
noopLogger
|
|
34
|
+
)
|
|
35
|
+
await server.init()
|
|
36
|
+
await server.start()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
after(async () => {
|
|
40
|
+
await server.stop()
|
|
41
|
+
resetPikkuState()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('serves the configured health-check path', async () => {
|
|
45
|
+
const res = await fetch(`http://localhost:${PORT}${HEALTH}`)
|
|
46
|
+
assert.equal(res.status, 200)
|
|
47
|
+
assert.equal(res.headers.get('content-type'), 'application/json')
|
|
48
|
+
assert.deepEqual(await res.json(), { ok: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('returns 404 for unregistered routes', async () => {
|
|
52
|
+
const res = await fetch(`http://localhost:${PORT}/nothing/here`)
|
|
53
|
+
assert.equal(res.status, 404)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('stop() is idempotent', async () => {
|
|
57
|
+
const extra = new PikkuBunServer({ port: PORT + 1 }, noopLogger)
|
|
58
|
+
await extra.start()
|
|
59
|
+
await extra.stop()
|
|
60
|
+
await assert.doesNotReject(extra.stop())
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Server as BunServer, ServerWebSocket } from 'bun'
|
|
2
|
+
|
|
3
|
+
import type { CoreConfig } from '@pikku/core'
|
|
4
|
+
import { stopSingletonServices } from '@pikku/core'
|
|
5
|
+
import type { Logger } from '@pikku/core/services'
|
|
6
|
+
import {
|
|
7
|
+
fetchData,
|
|
8
|
+
PikkuFetchHTTPRequest,
|
|
9
|
+
PikkuFetchHTTPResponse,
|
|
10
|
+
logRoutes as logRegisterRoutes,
|
|
11
|
+
type RunHTTPWiringOptions,
|
|
12
|
+
} from '@pikku/core/http'
|
|
13
|
+
import { logChannels } from '@pikku/core/channel'
|
|
14
|
+
import type { PikkuLocalChannelHandler } from '@pikku/core/channel/local'
|
|
15
|
+
import { runLocalChannel } from '@pikku/core/channel/local'
|
|
16
|
+
import { compileAllSchemas } from '@pikku/core/schema'
|
|
17
|
+
|
|
18
|
+
import { BunEventHubService } from './bun-event-hub-service.js'
|
|
19
|
+
|
|
20
|
+
export type BunServerConfig = CoreConfig & {
|
|
21
|
+
port: number
|
|
22
|
+
hostname?: string
|
|
23
|
+
healthCheckPath?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PikkuBunServerOptions = RunHTTPWiringOptions & {
|
|
27
|
+
/**
|
|
28
|
+
* Event hub backing channel pub/sub. Inject the SAME instance passed to
|
|
29
|
+
* `createSingletonServices` so functions and the WebSocket transport share
|
|
30
|
+
* one hub — otherwise a function's `eventHub.publish(...)` goes to a
|
|
31
|
+
* different hub than the one holding the live sockets and never reaches
|
|
32
|
+
* connected clients. Defaults to a fresh `BunEventHubService`.
|
|
33
|
+
*/
|
|
34
|
+
eventHub?: BunEventHubService
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type WsData = { channelHandler: PikkuLocalChannelHandler }
|
|
38
|
+
|
|
39
|
+
const isSerializable = (data: unknown): boolean =>
|
|
40
|
+
!(
|
|
41
|
+
typeof data === 'string' ||
|
|
42
|
+
data instanceof ArrayBuffer ||
|
|
43
|
+
data instanceof Uint8Array ||
|
|
44
|
+
data instanceof Int8Array ||
|
|
45
|
+
data instanceof Uint16Array ||
|
|
46
|
+
data instanceof Int16Array ||
|
|
47
|
+
data instanceof Uint32Array ||
|
|
48
|
+
data instanceof Int32Array ||
|
|
49
|
+
data instanceof Float32Array ||
|
|
50
|
+
data instanceof Float64Array
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bun-native Pikku server built on Bun.serve.
|
|
55
|
+
*
|
|
56
|
+
* Handles HTTP via the fetch handler and WebSocket via Bun.serve's native
|
|
57
|
+
* websocket handler (which is backed by uWebSockets internally).
|
|
58
|
+
*/
|
|
59
|
+
export class PikkuBunServer {
|
|
60
|
+
private server: BunServer<WsData> | null = null
|
|
61
|
+
private readonly eventHub: BunEventHubService
|
|
62
|
+
private readonly options: RunHTTPWiringOptions
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private readonly config: BunServerConfig,
|
|
66
|
+
private readonly logger: Logger,
|
|
67
|
+
options: PikkuBunServerOptions = {}
|
|
68
|
+
) {
|
|
69
|
+
const { eventHub, ...httpOptions } = options
|
|
70
|
+
this.eventHub = eventHub ?? new BunEventHubService()
|
|
71
|
+
this.options = httpOptions
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async init(): Promise<void> {
|
|
75
|
+
compileAllSchemas(this.logger)
|
|
76
|
+
logRegisterRoutes(this.logger)
|
|
77
|
+
logChannels(this.logger)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async start(): Promise<void> {
|
|
81
|
+
const { config, logger, options, eventHub } = this
|
|
82
|
+
|
|
83
|
+
this.server = Bun.serve<WsData>({
|
|
84
|
+
port: config.port,
|
|
85
|
+
hostname: config.hostname,
|
|
86
|
+
|
|
87
|
+
fetch: async (req, server) => {
|
|
88
|
+
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
89
|
+
const pikkuReq = new PikkuFetchHTTPRequest(req)
|
|
90
|
+
const pikkuRes = new PikkuFetchHTTPResponse()
|
|
91
|
+
const channelHandler = await runLocalChannel({
|
|
92
|
+
channelId: crypto.randomUUID(),
|
|
93
|
+
request: pikkuReq,
|
|
94
|
+
response: pikkuRes,
|
|
95
|
+
route: new URL(req.url).pathname,
|
|
96
|
+
})
|
|
97
|
+
if (!channelHandler) {
|
|
98
|
+
return new Response('Forbidden', { status: 403 })
|
|
99
|
+
}
|
|
100
|
+
const upgraded = server.upgrade(req, { data: { channelHandler } })
|
|
101
|
+
if (upgraded) return undefined as unknown as Response
|
|
102
|
+
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
config.healthCheckPath &&
|
|
107
|
+
new URL(req.url).pathname === config.healthCheckPath
|
|
108
|
+
) {
|
|
109
|
+
return new Response('{"ok":true}', {
|
|
110
|
+
headers: { 'content-type': 'application/json' },
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pikkuReq = new PikkuFetchHTTPRequest(req)
|
|
115
|
+
const pikkuRes = new PikkuFetchHTTPResponse()
|
|
116
|
+
await fetchData(pikkuReq, pikkuRes, {
|
|
117
|
+
respondWith404: true,
|
|
118
|
+
...options,
|
|
119
|
+
})
|
|
120
|
+
return pikkuRes.toResponse()
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
websocket: {
|
|
124
|
+
open: (ws: ServerWebSocket<WsData>) => {
|
|
125
|
+
const { channelHandler } = ws.data
|
|
126
|
+
channelHandler.registerOnSend((data) => {
|
|
127
|
+
ws.send(isSerializable(data) ? JSON.stringify(data) : (data as any))
|
|
128
|
+
})
|
|
129
|
+
channelHandler.registerOnSendBinary((data) => {
|
|
130
|
+
ws.send(data, true)
|
|
131
|
+
})
|
|
132
|
+
channelHandler.registerOnClose(() => {
|
|
133
|
+
ws.close()
|
|
134
|
+
})
|
|
135
|
+
eventHub.onChannelOpened(channelHandler.channelId, ws)
|
|
136
|
+
channelHandler.open()
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
message: async (ws: ServerWebSocket<WsData>, message) => {
|
|
140
|
+
const { channelHandler } = ws.data
|
|
141
|
+
if (typeof message === 'string') {
|
|
142
|
+
const result = await channelHandler.message(message)
|
|
143
|
+
if (result) ws.send(JSON.stringify(result))
|
|
144
|
+
} else {
|
|
145
|
+
const bytes =
|
|
146
|
+
message instanceof ArrayBuffer
|
|
147
|
+
? new Uint8Array(message)
|
|
148
|
+
: new Uint8Array(
|
|
149
|
+
message.buffer,
|
|
150
|
+
message.byteOffset,
|
|
151
|
+
message.byteLength
|
|
152
|
+
)
|
|
153
|
+
const result = await channelHandler.binaryMessage(bytes)
|
|
154
|
+
if (result) channelHandler.sendBinary(result)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
close: (ws: ServerWebSocket<WsData>) => {
|
|
159
|
+
const { channelHandler } = ws.data
|
|
160
|
+
eventHub.onChannelClosed(channelHandler.channelId)
|
|
161
|
+
channelHandler.close()
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
eventHub.setServer(this.server as BunServer<unknown>)
|
|
167
|
+
logger.info(
|
|
168
|
+
`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async stop(): Promise<void> {
|
|
173
|
+
await this.server?.stop()
|
|
174
|
+
this.server = null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public enableExitOnSignals(): void {
|
|
178
|
+
const shutdown = async (signal: string) => {
|
|
179
|
+
this.logger.info(`pikku-bun-server: ${signal} received, stopping`)
|
|
180
|
+
try {
|
|
181
|
+
await stopSingletonServices()
|
|
182
|
+
await this.stop()
|
|
183
|
+
} finally {
|
|
184
|
+
process.exit(0)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.once('SIGINT', () => shutdown('SIGINT'))
|
|
188
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'))
|
|
189
|
+
}
|
|
190
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"module": "Node18",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"target": "esnext",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"types": ["bun"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts"],
|
|
12
|
+
"exclude": ["**/*.test.ts", "node_modules", "dist"],
|
|
13
|
+
"references": [
|
|
14
|
+
{
|
|
15
|
+
"path": "../../core/tsconfig.json"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/bun-event-hub-service.ts","./src/index.ts","./src/pikku-bun-server.ts"],"version":"5.9.3"}
|