@ubean/server 0.1.13 → 0.2.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/dist/analytics-entry.d.ts +2 -0
- package/dist/analytics-entry.js +2 -0
- package/dist/cache-C84ix1Vq.js +173 -0
- package/dist/cache-b-MZlyv0.d.ts +48 -0
- package/dist/cache-directive-C1Nekkza.js +304 -0
- package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
- package/dist/cache-directive.d.ts +2 -0
- package/dist/cache-directive.js +2 -0
- package/dist/cache-entry.d.ts +3 -0
- package/dist/cache-entry.js +3 -0
- package/dist/cron-entry.d.ts +2 -0
- package/dist/cron-entry.js +2 -0
- package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
- package/dist/cron-scheduler-BVuXv7nn.js +258 -0
- package/dist/database-CfpFznl-.d.ts +67 -0
- package/dist/database-DNrY44SQ.js +352 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +2 -0
- package/dist/email-BjfRiR9b.js +354 -0
- package/dist/email-BvpEuNn_.d.ts +226 -0
- package/dist/email.d.ts +2 -0
- package/dist/email.js +2 -0
- package/dist/feature-flags-CdLwsMD2.js +657 -0
- package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
- package/dist/fetch-memo-rbkxxnW4.js +338 -0
- package/dist/index.d.ts +183 -488
- package/dist/index.js +352 -2023
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +3 -0
- package/dist/observability-Cio6Qq1H.js +339 -0
- package/dist/observability-DUNUEjj3.d.ts +70 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -0
- package/dist/queue-Bwzi3mhK.js +210 -0
- package/dist/queue-GOfTAWlz.d.ts +55 -0
- package/dist/queue.d.ts +2 -0
- package/dist/queue.js +2 -0
- package/dist/realtime.d.ts +2 -0
- package/dist/realtime.js +2 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.js +2 -0
- package/dist/sessions-BLqFFQTL.d.ts +217 -0
- package/dist/sessions-BsBsyFAG.js +450 -0
- package/dist/single-flight-BJyhDLdU.d.ts +422 -0
- package/dist/single-flight-mJ4ZKbx1.js +715 -0
- package/dist/sse-Ct72zhic.d.ts +95 -0
- package/dist/sse-a6Ky9Vcl.js +310 -0
- package/dist/static-DPHaovQe.js +90 -0
- package/dist/static-K2dRvjpS.d.ts +11 -0
- package/dist/static.d.ts +2 -0
- package/dist/static.js +2 -0
- package/dist/storage-BZLMaqHr.js +162 -0
- package/dist/storage-QdlPtPtR.d.ts +48 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -0
- package/package.json +68 -6
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Context, MiddlewareHandler } from "hono";
|
|
2
|
+
import { UbeanEnv } from "@ubean/shared";
|
|
3
|
+
//#region src/websocket.d.ts
|
|
4
|
+
interface Peer {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly headers: Headers;
|
|
8
|
+
readonly readyState: number;
|
|
9
|
+
send(data: string | ArrayBuffer | Uint8Array): void;
|
|
10
|
+
publish(topic: string, data: string | ArrayBuffer | Uint8Array): void;
|
|
11
|
+
subscribe(topic: string): void;
|
|
12
|
+
unsubscribe(topic: string): void;
|
|
13
|
+
close(code?: number, reason?: string): void;
|
|
14
|
+
getData<T = unknown>(): T | undefined;
|
|
15
|
+
setData<T>(data: T): void;
|
|
16
|
+
}
|
|
17
|
+
interface WebSocketRoom {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly peers: Set<Peer>;
|
|
20
|
+
broadcast(data: string | ArrayBuffer | Uint8Array, options?: {
|
|
21
|
+
except?: Peer;
|
|
22
|
+
}): void;
|
|
23
|
+
add(peer: Peer): void;
|
|
24
|
+
remove(peer: Peer): void;
|
|
25
|
+
}
|
|
26
|
+
interface WebSocketHooks {
|
|
27
|
+
open?: (peer: Peer) => void | Promise<void>;
|
|
28
|
+
message?: (peer: Peer, message: string | ArrayBuffer) => void | Promise<void>;
|
|
29
|
+
close?: (peer: Peer, code: number, reason: string) => void | Promise<void>;
|
|
30
|
+
error?: (peer: Peer, error: Error) => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
interface WebSocketDefinition {
|
|
33
|
+
path?: string;
|
|
34
|
+
hooks?: WebSocketHooks;
|
|
35
|
+
topics?: string[];
|
|
36
|
+
rooms?: string[];
|
|
37
|
+
}
|
|
38
|
+
declare function createRoom(name: string): WebSocketRoom;
|
|
39
|
+
declare function defineWebSocket(def: WebSocketDefinition): WebSocketDefinition;
|
|
40
|
+
declare function defineRoom(name: string): WebSocketRoom;
|
|
41
|
+
declare function getRoom(name: string): WebSocketRoom | undefined;
|
|
42
|
+
declare function getRooms(): Map<string, WebSocketRoom>;
|
|
43
|
+
declare function broadcast(topic: string, data: string | ArrayBuffer | Uint8Array): void;
|
|
44
|
+
declare function registerWebSocket(path: string, def: WebSocketDefinition): void;
|
|
45
|
+
declare function getWebSocketDefinitions(): Map<string, WebSocketDefinition>;
|
|
46
|
+
interface UpgradeResult {
|
|
47
|
+
response: Response;
|
|
48
|
+
peer: Peer;
|
|
49
|
+
}
|
|
50
|
+
declare function handleUpgrade(c: Context<UbeanEnv>, options: {
|
|
51
|
+
send: (data: string | ArrayBuffer | Uint8Array) => void;
|
|
52
|
+
close: (code?: number, reason?: string) => void;
|
|
53
|
+
raw?: unknown;
|
|
54
|
+
}): UpgradeResult;
|
|
55
|
+
declare function handleMessage(peer: Peer, message: string | ArrayBuffer): void;
|
|
56
|
+
declare function handleClose(peer: Peer, code?: number, reason?: string): void;
|
|
57
|
+
declare function handleError(peer: Peer, error: Error): void;
|
|
58
|
+
declare function createWebSocketMiddleware(): MiddlewareHandler<UbeanEnv>;
|
|
59
|
+
declare function clearWebSocketState(): void;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/sse.d.ts
|
|
62
|
+
interface SSEMessage {
|
|
63
|
+
data?: string | object;
|
|
64
|
+
event?: string;
|
|
65
|
+
id?: string;
|
|
66
|
+
retry?: number;
|
|
67
|
+
comment?: string;
|
|
68
|
+
}
|
|
69
|
+
interface SSEConnection {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
send(message: SSEMessage): void;
|
|
72
|
+
sendData(data: string | object, event?: string): void;
|
|
73
|
+
comment(text: string): void;
|
|
74
|
+
close(): void;
|
|
75
|
+
readonly closed: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface SSEHandler {
|
|
78
|
+
onConnect?: (connection: SSEConnection) => void | Promise<void>;
|
|
79
|
+
onClose?: (connection: SSEConnection) => void | Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
interface SSEOptions {
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
retry?: number;
|
|
84
|
+
keepAlive?: boolean | number;
|
|
85
|
+
}
|
|
86
|
+
declare function formatSSEMessage(msg: SSEMessage): string;
|
|
87
|
+
declare function createSSEStream(c: Context<UbeanEnv>, handler: SSEHandler, options?: SSEOptions): Response;
|
|
88
|
+
declare function defineSSE(handler: SSEHandler, options?: SSEOptions): MiddlewareHandler<UbeanEnv>;
|
|
89
|
+
declare function getSSEConnections(): Map<string, SSEConnection>;
|
|
90
|
+
declare function broadcastSSE(event: string, data: string | object, filter?: (conn: SSEConnection) => boolean): void;
|
|
91
|
+
declare function closeAllSSE(): void;
|
|
92
|
+
declare function sseHeaders(): Record<string, string>;
|
|
93
|
+
declare function clearSSEState(): void;
|
|
94
|
+
//#endregion
|
|
95
|
+
export { handleUpgrade as A, defineWebSocket as C, handleClose as D, getWebSocketDefinitions as E, handleError as O, defineRoom as S, getRooms as T, WebSocketRoom as _, broadcastSSE as a, createRoom as b, createSSEStream as c, getSSEConnections as d, sseHeaders as f, WebSocketHooks as g, WebSocketDefinition as h, SSEOptions as i, registerWebSocket as j, handleMessage as k, defineSSE as l, UpgradeResult as m, SSEHandler as n, clearSSEState as o, Peer as p, SSEMessage as r, closeAllSSE as s, SSEConnection as t, formatSSEMessage as u, broadcast as v, getRoom as w, createWebSocketMiddleware as x, clearWebSocketState as y };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
//#region src/websocket.ts
|
|
2
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
3
|
+
const topicSubscribers = /* @__PURE__ */ new Map();
|
|
4
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
5
|
+
const peerIdSeed = { value: 0 };
|
|
6
|
+
var RoomImpl = class {
|
|
7
|
+
name;
|
|
8
|
+
peers = /* @__PURE__ */ new Set();
|
|
9
|
+
constructor(name) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
}
|
|
12
|
+
broadcast(msg, options) {
|
|
13
|
+
for (const peer of this.peers) {
|
|
14
|
+
if (options?.except && peer === options.except) continue;
|
|
15
|
+
if (peer.readyState === 1) try {
|
|
16
|
+
peer.send(msg);
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
add(peer) {
|
|
21
|
+
this.peers.add(peer);
|
|
22
|
+
peer._rooms.add(this.name);
|
|
23
|
+
}
|
|
24
|
+
remove(peer) {
|
|
25
|
+
this.peers.delete(peer);
|
|
26
|
+
peer._rooms.delete(this.name);
|
|
27
|
+
if (this.peers.size === 0) rooms.delete(this.name);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function generateId$1() {
|
|
31
|
+
peerIdSeed.value++;
|
|
32
|
+
return `peer_${Date.now().toString(36)}_${peerIdSeed.value.toString(36)}`;
|
|
33
|
+
}
|
|
34
|
+
function createPeer(options) {
|
|
35
|
+
const subscriptions = /* @__PURE__ */ new Set();
|
|
36
|
+
const peerRooms = /* @__PURE__ */ new Set();
|
|
37
|
+
let data = void 0;
|
|
38
|
+
const peer = {
|
|
39
|
+
id: options.id,
|
|
40
|
+
url: options.url,
|
|
41
|
+
headers: options.headers,
|
|
42
|
+
get readyState() {
|
|
43
|
+
return 1;
|
|
44
|
+
},
|
|
45
|
+
_subscriptions: subscriptions,
|
|
46
|
+
_rooms: peerRooms,
|
|
47
|
+
_raw: options.raw,
|
|
48
|
+
send: options.send,
|
|
49
|
+
close: options.close,
|
|
50
|
+
publish(topic, msg) {
|
|
51
|
+
const topicPeers = topicSubscribers.get(topic);
|
|
52
|
+
if (topicPeers) {
|
|
53
|
+
for (const p of topicPeers) if (p !== peer && p.readyState === 1) try {
|
|
54
|
+
p.send(msg);
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
subscribe(topic) {
|
|
59
|
+
subscriptions.add(topic);
|
|
60
|
+
let set = topicSubscribers.get(topic);
|
|
61
|
+
if (!set) {
|
|
62
|
+
set = /* @__PURE__ */ new Set();
|
|
63
|
+
topicSubscribers.set(topic, set);
|
|
64
|
+
}
|
|
65
|
+
set.add(peer);
|
|
66
|
+
},
|
|
67
|
+
unsubscribe(topic) {
|
|
68
|
+
subscriptions.delete(topic);
|
|
69
|
+
topicSubscribers.get(topic)?.delete(peer);
|
|
70
|
+
},
|
|
71
|
+
getData() {
|
|
72
|
+
return data;
|
|
73
|
+
},
|
|
74
|
+
setData(value) {
|
|
75
|
+
data = value;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return peer;
|
|
79
|
+
}
|
|
80
|
+
function createRoom(name) {
|
|
81
|
+
const existing = rooms.get(name);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
const room = new RoomImpl(name);
|
|
84
|
+
rooms.set(name, room);
|
|
85
|
+
return room;
|
|
86
|
+
}
|
|
87
|
+
function defineWebSocket(def) {
|
|
88
|
+
return def;
|
|
89
|
+
}
|
|
90
|
+
function defineRoom(name) {
|
|
91
|
+
return createRoom(name);
|
|
92
|
+
}
|
|
93
|
+
function getRoom(name) {
|
|
94
|
+
return rooms.get(name);
|
|
95
|
+
}
|
|
96
|
+
function getRooms() {
|
|
97
|
+
return rooms;
|
|
98
|
+
}
|
|
99
|
+
function broadcast(topic, data) {
|
|
100
|
+
const peers = topicSubscribers.get(topic);
|
|
101
|
+
if (!peers) return;
|
|
102
|
+
for (const peer of peers) if (peer.readyState === 1) try {
|
|
103
|
+
peer.send(data);
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
function registerWebSocket(path, def) {
|
|
107
|
+
definitions.set(path, def);
|
|
108
|
+
}
|
|
109
|
+
function getWebSocketDefinitions() {
|
|
110
|
+
return definitions;
|
|
111
|
+
}
|
|
112
|
+
function handleUpgrade(c, options) {
|
|
113
|
+
const path = new URL(c.req.url).pathname;
|
|
114
|
+
const def = definitions.get(path) || definitions.get("/*");
|
|
115
|
+
const peer = createPeer({
|
|
116
|
+
id: generateId$1(),
|
|
117
|
+
url: c.req.url,
|
|
118
|
+
headers: c.req.raw.headers,
|
|
119
|
+
send: options.send,
|
|
120
|
+
close: options.close,
|
|
121
|
+
raw: options.raw
|
|
122
|
+
});
|
|
123
|
+
if (def?.topics) for (const topic of def.topics) peer.subscribe(topic);
|
|
124
|
+
if (def?.rooms) for (const roomName of def.rooms) createRoom(roomName).add(peer);
|
|
125
|
+
if (def?.hooks?.open) queueMicrotask(() => {
|
|
126
|
+
Promise.resolve(def.hooks.open(peer)).catch(() => {});
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
response: new Response(null, {
|
|
130
|
+
status: 200,
|
|
131
|
+
headers: {
|
|
132
|
+
Upgrade: "websocket",
|
|
133
|
+
Connection: "Upgrade"
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
peer
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function handleMessage(peer, message) {
|
|
140
|
+
const path = new URL(peer.url).pathname;
|
|
141
|
+
const def = definitions.get(path) || definitions.get("/*");
|
|
142
|
+
if (def?.hooks?.message) Promise.resolve(def.hooks.message(peer, message)).catch(() => {});
|
|
143
|
+
}
|
|
144
|
+
function handleClose(peer, code = 1e3, reason = "") {
|
|
145
|
+
const path = new URL(peer.url).pathname;
|
|
146
|
+
const def = definitions.get(path) || definitions.get("/*");
|
|
147
|
+
const internal = peer;
|
|
148
|
+
for (const topic of internal._subscriptions) topicSubscribers.get(topic)?.delete(peer);
|
|
149
|
+
for (const roomName of internal._rooms) rooms.get(roomName)?.remove(peer);
|
|
150
|
+
internal._subscriptions.clear();
|
|
151
|
+
if (def?.hooks?.close) Promise.resolve(def.hooks.close(peer, code, reason)).catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
function handleError(peer, error) {
|
|
154
|
+
const path = new URL(peer.url).pathname;
|
|
155
|
+
const def = definitions.get(path) || definitions.get("/*");
|
|
156
|
+
if (def?.hooks?.error) Promise.resolve(def.hooks.error(peer, error)).catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
function createWebSocketMiddleware() {
|
|
159
|
+
return async function wsMiddleware(c, next) {
|
|
160
|
+
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
|
|
161
|
+
await next();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const path = new URL(c.req.url).pathname;
|
|
165
|
+
if (!definitions.get(path)) {
|
|
166
|
+
await next();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
return new Response("WebSocket upgrade requires platform-specific handler", { status: 426 });
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function clearWebSocketState() {
|
|
173
|
+
rooms.clear();
|
|
174
|
+
topicSubscribers.clear();
|
|
175
|
+
definitions.clear();
|
|
176
|
+
peerIdSeed.value = 0;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/sse.ts
|
|
180
|
+
const connections = /* @__PURE__ */ new Map();
|
|
181
|
+
let idSeed = 0;
|
|
182
|
+
function generateId() {
|
|
183
|
+
idSeed++;
|
|
184
|
+
return `sse_${Date.now().toString(36)}_${idSeed.toString(36)}`;
|
|
185
|
+
}
|
|
186
|
+
function formatSSEMessage(msg) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
if (msg.comment) for (const line of msg.comment.split("\n")) lines.push(`: ${line}`);
|
|
189
|
+
if (msg.id != null) lines.push(`id: ${msg.id}`);
|
|
190
|
+
if (msg.event) lines.push(`event: ${msg.event}`);
|
|
191
|
+
if (msg.retry != null) lines.push(`retry: ${msg.retry}`);
|
|
192
|
+
if (msg.data != null) {
|
|
193
|
+
const data = typeof msg.data === "string" ? msg.data : JSON.stringify(msg.data);
|
|
194
|
+
for (const line of data.split("\n")) lines.push(`data: ${line}`);
|
|
195
|
+
}
|
|
196
|
+
return `${lines.join("\n")}\n\n`;
|
|
197
|
+
}
|
|
198
|
+
var SSEConnectionImpl = class {
|
|
199
|
+
id;
|
|
200
|
+
writer = null;
|
|
201
|
+
encoder = new TextEncoder();
|
|
202
|
+
_closed = false;
|
|
203
|
+
keepAliveTimer = null;
|
|
204
|
+
onCloseCb;
|
|
205
|
+
constructor(writer, options = {}, onClose) {
|
|
206
|
+
this.id = generateId();
|
|
207
|
+
this.writer = writer;
|
|
208
|
+
this.onCloseCb = onClose;
|
|
209
|
+
if (options.keepAlive !== false) {
|
|
210
|
+
const interval = typeof options.keepAlive === "number" ? options.keepAlive : 3e4;
|
|
211
|
+
this.keepAliveTimer = setInterval(() => {
|
|
212
|
+
if (!this._closed) this.comment("keep-alive");
|
|
213
|
+
}, interval);
|
|
214
|
+
}
|
|
215
|
+
connections.set(this.id, this);
|
|
216
|
+
}
|
|
217
|
+
get closed() {
|
|
218
|
+
return this._closed;
|
|
219
|
+
}
|
|
220
|
+
send(msg) {
|
|
221
|
+
if (this._closed || !this.writer) return;
|
|
222
|
+
const raw = formatSSEMessage(msg);
|
|
223
|
+
this.writer.write(this.encoder.encode(raw)).catch(() => this._cleanup());
|
|
224
|
+
}
|
|
225
|
+
sendData(data, event) {
|
|
226
|
+
this.send({
|
|
227
|
+
data,
|
|
228
|
+
event
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
comment(text) {
|
|
232
|
+
if (this._closed || !this.writer) return;
|
|
233
|
+
try {
|
|
234
|
+
const raw = `: ${text}\n\n`;
|
|
235
|
+
this.writer.write(this.encoder.encode(raw)).catch(() => this._cleanup());
|
|
236
|
+
} catch {
|
|
237
|
+
this._cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
close() {
|
|
241
|
+
this._cleanup();
|
|
242
|
+
}
|
|
243
|
+
_cleanup() {
|
|
244
|
+
if (this._closed) return;
|
|
245
|
+
this._closed = true;
|
|
246
|
+
if (this.keepAliveTimer) {
|
|
247
|
+
clearInterval(this.keepAliveTimer);
|
|
248
|
+
this.keepAliveTimer = null;
|
|
249
|
+
}
|
|
250
|
+
if (this.writer) {
|
|
251
|
+
try {
|
|
252
|
+
this.writer.close();
|
|
253
|
+
} catch {}
|
|
254
|
+
this.writer = null;
|
|
255
|
+
}
|
|
256
|
+
connections.delete(this.id);
|
|
257
|
+
if (this.onCloseCb) {
|
|
258
|
+
try {
|
|
259
|
+
Promise.resolve(this.onCloseCb(this)).catch(() => {});
|
|
260
|
+
} catch {}
|
|
261
|
+
this.onCloseCb = void 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function createSSEStream(c, handler, options = {}) {
|
|
266
|
+
const { readable, writable } = new TransformStream();
|
|
267
|
+
const writer = writable.getWriter();
|
|
268
|
+
const headers = {
|
|
269
|
+
"Content-Type": "text/event-stream",
|
|
270
|
+
"Cache-Control": "no-cache, no-transform",
|
|
271
|
+
Connection: "keep-alive",
|
|
272
|
+
"X-Accel-Buffering": "no",
|
|
273
|
+
...options.headers
|
|
274
|
+
};
|
|
275
|
+
const connection = new SSEConnectionImpl(writer, options, handler.onClose);
|
|
276
|
+
if (options.retry != null) connection.send({ retry: options.retry });
|
|
277
|
+
queueMicrotask(() => {
|
|
278
|
+
Promise.resolve(handler.onConnect?.(connection)).catch(() => {});
|
|
279
|
+
});
|
|
280
|
+
return new Response(readable, { headers });
|
|
281
|
+
}
|
|
282
|
+
function defineSSE(handler, options) {
|
|
283
|
+
return (async (c) => {
|
|
284
|
+
return createSSEStream(c, handler, options);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function getSSEConnections() {
|
|
288
|
+
return connections;
|
|
289
|
+
}
|
|
290
|
+
function broadcastSSE(event, data, filter) {
|
|
291
|
+
for (const conn of connections.values()) if (!conn.closed && (!filter || filter(conn))) conn.sendData(data, event);
|
|
292
|
+
}
|
|
293
|
+
function closeAllSSE() {
|
|
294
|
+
for (const conn of Array.from(connections.values())) conn.close();
|
|
295
|
+
connections.clear();
|
|
296
|
+
}
|
|
297
|
+
function sseHeaders() {
|
|
298
|
+
return {
|
|
299
|
+
"Content-Type": "text/event-stream",
|
|
300
|
+
"Cache-Control": "no-cache, no-transform",
|
|
301
|
+
Connection: "keep-alive",
|
|
302
|
+
"X-Accel-Buffering": "no"
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function clearSSEState() {
|
|
306
|
+
closeAllSSE();
|
|
307
|
+
idSeed = 0;
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
export { registerWebSocket as S, getWebSocketDefinitions as _, defineSSE as a, handleMessage as b, sseHeaders as c, createRoom as d, createWebSocketMiddleware as f, getRooms as g, getRoom as h, createSSEStream as i, broadcast as l, defineWebSocket as m, clearSSEState as n, formatSSEMessage as o, defineRoom as p, closeAllSSE as r, getSSEConnections as s, broadcastSSE as t, clearWebSocketState as u, handleClose as v, handleUpgrade as x, handleError as y };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, join } from "pathe";
|
|
3
|
+
//#region src/static.ts
|
|
4
|
+
const MIME_TYPES = {
|
|
5
|
+
".html": "text/html; charset=utf-8",
|
|
6
|
+
".htm": "text/html; charset=utf-8",
|
|
7
|
+
".css": "text/css; charset=utf-8",
|
|
8
|
+
".js": "application/javascript; charset=utf-8",
|
|
9
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
10
|
+
".json": "application/json; charset=utf-8",
|
|
11
|
+
".svg": "image/svg+xml",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".webp": "image/webp",
|
|
17
|
+
".ico": "image/x-icon",
|
|
18
|
+
".avif": "image/avif",
|
|
19
|
+
".woff": "font/woff",
|
|
20
|
+
".woff2": "font/woff2",
|
|
21
|
+
".ttf": "font/ttf",
|
|
22
|
+
".otf": "font/otf",
|
|
23
|
+
".eot": "application/vnd.ms-fontobject",
|
|
24
|
+
".txt": "text/plain; charset=utf-8",
|
|
25
|
+
".xml": "application/xml; charset=utf-8",
|
|
26
|
+
".pdf": "application/pdf",
|
|
27
|
+
".zip": "application/zip",
|
|
28
|
+
".gz": "application/gzip",
|
|
29
|
+
".mp3": "audio/mpeg",
|
|
30
|
+
".mp4": "video/mp4",
|
|
31
|
+
".webm": "video/webm",
|
|
32
|
+
".wasm": "application/wasm",
|
|
33
|
+
".map": "application/json; charset=utf-8"
|
|
34
|
+
};
|
|
35
|
+
function getMimeType(filePath) {
|
|
36
|
+
const ext = extname(filePath).toLowerCase();
|
|
37
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
38
|
+
}
|
|
39
|
+
function serveStatic(options) {
|
|
40
|
+
const { publicDir, indexFiles = ["index.html"], maxAge = 3600 } = options;
|
|
41
|
+
return async (c, next) => {
|
|
42
|
+
const path = decodeURIComponent(c.req.path);
|
|
43
|
+
if (path.startsWith("/_") || path.startsWith("/api/")) return next();
|
|
44
|
+
let filePath = join(publicDir, path);
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
if (path.endsWith("/")) for (const indexFile of indexFiles) {
|
|
48
|
+
const indexPath = join(filePath, indexFile);
|
|
49
|
+
if (existsSync(indexPath) && statSync(indexPath).isFile()) {
|
|
50
|
+
filePath = indexPath;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();
|
|
55
|
+
}
|
|
56
|
+
if (statSync(filePath).isDirectory()) for (const indexFile of indexFiles) {
|
|
57
|
+
const indexPath = join(filePath, indexFile);
|
|
58
|
+
if (existsSync(indexPath) && statSync(indexPath).isFile()) {
|
|
59
|
+
filePath = indexPath;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!existsSync(filePath) || statSync(filePath).isDirectory()) return next();
|
|
64
|
+
const finalStat = statSync(filePath);
|
|
65
|
+
const mimeType = getMimeType(filePath);
|
|
66
|
+
const body = readFileSync(filePath);
|
|
67
|
+
const headers = {
|
|
68
|
+
"Content-Type": mimeType,
|
|
69
|
+
"Content-Length": String(body.length),
|
|
70
|
+
"Cache-Control": `public, max-age=${maxAge}`,
|
|
71
|
+
"X-Content-Type-Options": "nosniff"
|
|
72
|
+
};
|
|
73
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
74
|
+
const etag = `"${finalStat.size.toString(16)}-${finalStat.mtimeMs.toString(16)}"`;
|
|
75
|
+
if (ifNoneMatch === etag) return new Response(null, {
|
|
76
|
+
status: 304,
|
|
77
|
+
headers
|
|
78
|
+
});
|
|
79
|
+
headers.ETag = etag;
|
|
80
|
+
return new Response(body, {
|
|
81
|
+
status: 200,
|
|
82
|
+
headers
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
return next();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { serveStatic as t };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MiddlewareHandler } from "hono";
|
|
2
|
+
import { UbeanEnv } from "@ubean/shared";
|
|
3
|
+
//#region src/static.d.ts
|
|
4
|
+
interface ServeStaticOptions {
|
|
5
|
+
publicDir: string;
|
|
6
|
+
indexFiles?: string[];
|
|
7
|
+
maxAge?: number;
|
|
8
|
+
}
|
|
9
|
+
declare function serveStatic(options: ServeStaticOptions): MiddlewareHandler<UbeanEnv>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { serveStatic as n, ServeStaticOptions as t };
|
package/dist/static.d.ts
ADDED
package/dist/static.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//#region src/storage.ts
|
|
2
|
+
function createMemoryDriver() {
|
|
3
|
+
const store = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
async getItemRaw(key) {
|
|
6
|
+
return store.get(key);
|
|
7
|
+
},
|
|
8
|
+
async setItemRaw(key, value) {
|
|
9
|
+
store.set(key, value);
|
|
10
|
+
},
|
|
11
|
+
async removeItem(key) {
|
|
12
|
+
store.delete(key);
|
|
13
|
+
},
|
|
14
|
+
async getKeys(base) {
|
|
15
|
+
const prefix = base || "";
|
|
16
|
+
return Array.from(store.keys()).filter((k) => k.startsWith(prefix));
|
|
17
|
+
},
|
|
18
|
+
async clear(base) {
|
|
19
|
+
if (!base) {
|
|
20
|
+
store.clear();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const key of Array.from(store.keys())) if (key.startsWith(base)) store.delete(key);
|
|
24
|
+
},
|
|
25
|
+
async hasItem(key) {
|
|
26
|
+
return store.has(key);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createStorage(options = {}) {
|
|
31
|
+
const mounts = /* @__PURE__ */ new Map();
|
|
32
|
+
const rootDriver = options.driver || createMemoryDriver();
|
|
33
|
+
const rootBase = options.base ? `${options.base.replace(/\/$/, "")}:` : "";
|
|
34
|
+
function resolveKey(key) {
|
|
35
|
+
const fullKey = rootBase + key;
|
|
36
|
+
let bestMatch = "";
|
|
37
|
+
let bestDriver = rootDriver;
|
|
38
|
+
for (const [base, driver] of mounts) if (fullKey.startsWith(`${base}:`) && base.length > bestMatch.length) {
|
|
39
|
+
bestMatch = base;
|
|
40
|
+
bestDriver = driver;
|
|
41
|
+
}
|
|
42
|
+
const relativeKey = bestMatch ? fullKey.slice(bestMatch.length + 1) : fullKey;
|
|
43
|
+
return {
|
|
44
|
+
driver: bestDriver,
|
|
45
|
+
relativeKey
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function getEntry(key) {
|
|
49
|
+
const { driver, relativeKey } = resolveKey(key);
|
|
50
|
+
const raw = await driver.getItemRaw(relativeKey);
|
|
51
|
+
if (raw === void 0 || raw === null) return null;
|
|
52
|
+
const entry = raw;
|
|
53
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
54
|
+
await driver.removeItem(relativeKey);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
async get(key) {
|
|
61
|
+
const entry = await getEntry(key);
|
|
62
|
+
return entry ? entry.value : null;
|
|
63
|
+
},
|
|
64
|
+
async set(key, value, ttl) {
|
|
65
|
+
const { driver, relativeKey } = resolveKey(key);
|
|
66
|
+
const entry = {
|
|
67
|
+
value,
|
|
68
|
+
createdAt: Date.now(),
|
|
69
|
+
expiresAt: ttl ? Date.now() + ttl * 1e3 : void 0
|
|
70
|
+
};
|
|
71
|
+
await driver.setItemRaw(relativeKey, entry);
|
|
72
|
+
},
|
|
73
|
+
async remove(key) {
|
|
74
|
+
const { driver, relativeKey } = resolveKey(key);
|
|
75
|
+
await driver.removeItem(relativeKey);
|
|
76
|
+
},
|
|
77
|
+
async keys(base) {
|
|
78
|
+
const prefix = base || "";
|
|
79
|
+
const rootPrefix = rootBase;
|
|
80
|
+
const results = /* @__PURE__ */ new Set();
|
|
81
|
+
const rootKeys = await rootDriver.getKeys(rootPrefix);
|
|
82
|
+
for (const k of rootKeys) {
|
|
83
|
+
const stripped = k.slice(rootPrefix.length);
|
|
84
|
+
if (!prefix || stripped.startsWith(prefix)) results.add(stripped);
|
|
85
|
+
}
|
|
86
|
+
return Array.from(results);
|
|
87
|
+
},
|
|
88
|
+
async clear(base) {
|
|
89
|
+
const rootPrefix = rootBase + (base || "");
|
|
90
|
+
const rootKeys = await rootDriver.getKeys(rootPrefix);
|
|
91
|
+
for (const k of rootKeys) await rootDriver.removeItem(k);
|
|
92
|
+
},
|
|
93
|
+
async has(key) {
|
|
94
|
+
return await getEntry(key) !== null;
|
|
95
|
+
},
|
|
96
|
+
async getMeta(key) {
|
|
97
|
+
const entry = await getEntry(key);
|
|
98
|
+
if (!entry) return null;
|
|
99
|
+
return {
|
|
100
|
+
ttl: entry.expiresAt ? Math.max(0, Math.floor((entry.expiresAt - Date.now()) / 1e3)) : void 0,
|
|
101
|
+
createdAt: entry.createdAt,
|
|
102
|
+
expiresAt: entry.expiresAt
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
mount(base, driver) {
|
|
106
|
+
mounts.set(rootBase + base.replace(/\/$/, ""), driver);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
let globalStorage = null;
|
|
111
|
+
function useStorage(storage) {
|
|
112
|
+
if (storage) {
|
|
113
|
+
globalStorage = storage;
|
|
114
|
+
return storage;
|
|
115
|
+
}
|
|
116
|
+
if (!globalStorage) globalStorage = createStorage();
|
|
117
|
+
return globalStorage;
|
|
118
|
+
}
|
|
119
|
+
function clearGlobalStorage() {
|
|
120
|
+
globalStorage = null;
|
|
121
|
+
}
|
|
122
|
+
function createKV(options = {}) {
|
|
123
|
+
const storage = useStorage();
|
|
124
|
+
const prefix = options.prefix || "kv:default";
|
|
125
|
+
const serialize = options.serialize || ((v) => JSON.stringify(v));
|
|
126
|
+
const deserialize = options.deserialize || ((raw) => JSON.parse(raw));
|
|
127
|
+
const fullKey = (key) => `${prefix}:${key}`;
|
|
128
|
+
return {
|
|
129
|
+
async get(key) {
|
|
130
|
+
const raw = await storage.get(fullKey(key));
|
|
131
|
+
if (raw === null) return null;
|
|
132
|
+
return deserialize(raw);
|
|
133
|
+
},
|
|
134
|
+
async set(key, value, ttl) {
|
|
135
|
+
await storage.set(fullKey(key), serialize(value), ttl ?? options.ttl);
|
|
136
|
+
},
|
|
137
|
+
async remove(key) {
|
|
138
|
+
await storage.remove(fullKey(key));
|
|
139
|
+
},
|
|
140
|
+
async keys() {
|
|
141
|
+
return (await storage.keys(prefix)).map((k) => k.slice(prefix.length + 1)).filter((k) => k.length > 0);
|
|
142
|
+
},
|
|
143
|
+
async has(key) {
|
|
144
|
+
return storage.has(fullKey(key));
|
|
145
|
+
},
|
|
146
|
+
async clear() {
|
|
147
|
+
await storage.clear(prefix);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
152
|
+
function useKV(name = "default", options) {
|
|
153
|
+
if (namespaces.has(name)) return namespaces.get(name);
|
|
154
|
+
const ns = createKV({
|
|
155
|
+
...options,
|
|
156
|
+
prefix: `kv:${name}`
|
|
157
|
+
});
|
|
158
|
+
namespaces.set(name, ns);
|
|
159
|
+
return ns;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { useKV as a, createStorage as i, createKV as n, useStorage as o, createMemoryDriver as r, clearGlobalStorage as t };
|