@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 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"}
@@ -0,0 +1,3 @@
1
+ export { PikkuBunServer } from './pikku-bun-server.js';
2
+ export { BunEventHubService } from './bun-event-hub-service.js';
3
+ export type { BunServerConfig, PikkuBunServerOptions, } from './pikku-bun-server.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { PikkuBunServer } from './pikku-bun-server.js';
2
+ export { BunEventHubService } from './bun-event-hub-service.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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,6 @@
1
+ export { PikkuBunServer } from './pikku-bun-server.js'
2
+ export { BunEventHubService } from './bun-event-hub-service.js'
3
+ export type {
4
+ BunServerConfig,
5
+ PikkuBunServerOptions,
6
+ } from './pikku-bun-server.js'
@@ -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"}