@sx3/ultra 0.0.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/LICENSE +21 -0
- package/README.md +512 -0
- package/dist/auth.d.mts +56 -0
- package/dist/auth.mjs +84 -0
- package/dist/client.d.mts +27 -0
- package/dist/client.mjs +131 -0
- package/dist/context-ChCsZh7S.d.mts +17 -0
- package/dist/context.d.mts +2 -0
- package/dist/context.mjs +10 -0
- package/dist/cors.d.mts +16 -0
- package/dist/cors.mjs +58 -0
- package/dist/crypto.d.mts +7 -0
- package/dist/crypto.mjs +44 -0
- package/dist/error-CII1zMOR.mjs +45 -0
- package/dist/error.d.mts +25 -0
- package/dist/error.mjs +3 -0
- package/dist/http-BqWCMASL.d.mts +10 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +1 -0
- package/dist/middleware-COKreBGP.d.mts +43 -0
- package/dist/middleware.d.mts +4 -0
- package/dist/middleware.mjs +1 -0
- package/dist/procedure-BN1rLLRX.mjs +86 -0
- package/dist/procedure.d.mts +4 -0
- package/dist/procedure.mjs +3 -0
- package/dist/response-CNhIkAYG.mjs +59 -0
- package/dist/response.d.mts +5 -0
- package/dist/response.mjs +3 -0
- package/dist/rpc-Ch2UXReT.d.mts +23 -0
- package/dist/rpc-_rBI0z-9.mjs +7 -0
- package/dist/rpc.d.mts +2 -0
- package/dist/rpc.mjs +3 -0
- package/dist/session.d.mts +115 -0
- package/dist/session.mjs +181 -0
- package/dist/types-Cn69QrjS.d.mts +11 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/dist/ultra.d.mts +69 -0
- package/dist/ultra.mjs +273 -0
- package/dist/validation-CkRfxQJ_.d.mts +57 -0
- package/dist/validation-Cop5Wvlr.mjs +12 -0
- package/dist/validation.d.mts +2 -0
- package/dist/validation.mjs +3 -0
- package/package.json +55 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { n as DefaultSocketData, r as HTTPContext, t as BaseContext } from "./context-ChCsZh7S.mjs";
|
|
2
|
+
import { a as JSONValue, o as Promisable, r as JSONObject, t as DeepReadonly } from "./types-Cn69QrjS.mjs";
|
|
3
|
+
import "./http-BqWCMASL.mjs";
|
|
4
|
+
import "./middleware-COKreBGP.mjs";
|
|
5
|
+
import { ProceduresMap, Ultra } from "./ultra.mjs";
|
|
6
|
+
import { BunRequest, CookieSameSite, RedisClient } from "bun";
|
|
7
|
+
|
|
8
|
+
//#region src/session.d.ts
|
|
9
|
+
type SessionData = Record<string, JSONValue>;
|
|
10
|
+
/** Session store interface */
|
|
11
|
+
interface SessionStore {
|
|
12
|
+
read: (sessionId: string) => Promisable<SessionData | null>;
|
|
13
|
+
write: (sessionId: string, data: SessionData) => Promisable<void>;
|
|
14
|
+
destroy: (sessionId: string) => Promisable<void>;
|
|
15
|
+
touch: (sessionId: string) => Promisable<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Options user for set session cookie */
|
|
18
|
+
interface SessionCookieOptions {
|
|
19
|
+
/** @default "/" */
|
|
20
|
+
path: string;
|
|
21
|
+
/** @default true */
|
|
22
|
+
httpOnly: boolean;
|
|
23
|
+
/** @default true */
|
|
24
|
+
secure: boolean;
|
|
25
|
+
/** @default "lax" */
|
|
26
|
+
sameSite: CookieSameSite;
|
|
27
|
+
/** In seconds. @default config.ttlSec */
|
|
28
|
+
maxAge: number;
|
|
29
|
+
}
|
|
30
|
+
/** Factory for create session store */
|
|
31
|
+
type SessionStoreFactory = (config: SessionConfig<any>, context: BaseContext) => SessionStore;
|
|
32
|
+
interface SessionConfig<S extends Record<string, SessionStoreFactory> = Record<string, SessionStoreFactory>> {
|
|
33
|
+
/** The name is used as a prefix to cookies and storage with such as Redis */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Session time to live in seconds */
|
|
36
|
+
ttlSec: number;
|
|
37
|
+
/** Secret used to sign session ID cookie */
|
|
38
|
+
secret: string;
|
|
39
|
+
/** Options used to set session cookie */
|
|
40
|
+
cookie?: Partial<SessionCookieOptions>;
|
|
41
|
+
/** Default store */
|
|
42
|
+
store: Extract<keyof S, string>;
|
|
43
|
+
/** Available session stores as factories */
|
|
44
|
+
stores: S;
|
|
45
|
+
}
|
|
46
|
+
/** Session socket data extensions */
|
|
47
|
+
interface SessionSocketData {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
}
|
|
50
|
+
declare function defineConfig<S extends Record<string, SessionStoreFactory>>(config: SessionConfig<S>): SessionConfig<S>;
|
|
51
|
+
/** Extends context and socket data, initiate session instance every request */
|
|
52
|
+
declare function createSessionModule<S extends Record<string, SessionStoreFactory>>(config: SessionConfig<S>): Ultra<ProceduresMap, (BaseContext & {
|
|
53
|
+
session: Session<S>;
|
|
54
|
+
}) & BaseContext, DefaultSocketData & ((context: HTTPContext) => {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
})>;
|
|
57
|
+
/** Stores the ID in a cookie, then moves it to the socket and uses it for requests */
|
|
58
|
+
declare class Session<Stores extends Record<string, SessionStoreFactory> = Record<string, SessionStoreFactory>> {
|
|
59
|
+
/** Make random session id */
|
|
60
|
+
static makeId(): string;
|
|
61
|
+
/** Get existing session ID from request cookie or create a new one */
|
|
62
|
+
static getOrCreateId(request: BunRequest, config: SessionConfig<any>): string;
|
|
63
|
+
protected readonly config: SessionConfig<Stores>;
|
|
64
|
+
protected readonly context: BaseContext;
|
|
65
|
+
protected readonly store: SessionStore;
|
|
66
|
+
protected readonly sessionIdFromClient: string | null;
|
|
67
|
+
protected sessionId: string;
|
|
68
|
+
protected sessionState: JSONObject | null;
|
|
69
|
+
protected modified: boolean;
|
|
70
|
+
constructor(config: SessionConfig<Stores>, context: BaseContext);
|
|
71
|
+
get id(): string;
|
|
72
|
+
/** Load data from session store */
|
|
73
|
+
initiate(): Promise<void>;
|
|
74
|
+
/** Commit data to session store */
|
|
75
|
+
commit(): Promise<void>;
|
|
76
|
+
/** Change session id */
|
|
77
|
+
regenerate(): void;
|
|
78
|
+
get<T extends JSONValue>(key: string): DeepReadonly<T> | null;
|
|
79
|
+
get<T extends JSONValue>(key: string, defaultValue: T): DeepReadonly<T>;
|
|
80
|
+
set(key: string, value: JSONValue): void;
|
|
81
|
+
has(key: string): boolean;
|
|
82
|
+
all(): DeepReadonly<JSONObject>;
|
|
83
|
+
delete(key: string): void;
|
|
84
|
+
clear(): void;
|
|
85
|
+
protected get state(): JSONObject;
|
|
86
|
+
protected get isEmpty(): boolean;
|
|
87
|
+
protected touch(): void;
|
|
88
|
+
}
|
|
89
|
+
declare class RedisSessionStore implements SessionStore {
|
|
90
|
+
protected readonly config: SessionConfig<any>;
|
|
91
|
+
protected readonly connection: RedisClient;
|
|
92
|
+
constructor(config: SessionConfig<any>, connection: RedisClient);
|
|
93
|
+
read(sessionId: string): Promise<SessionData | null>;
|
|
94
|
+
write(sessionId: string, data: SessionData): Promise<void>;
|
|
95
|
+
destroy(sessionId: string): Promise<void>;
|
|
96
|
+
touch(sessionId: string): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
declare class MemorySessionStore implements SessionStore {
|
|
99
|
+
protected readonly config: SessionConfig<any>;
|
|
100
|
+
protected readonly sessions: Map<string, {
|
|
101
|
+
data: SessionData;
|
|
102
|
+
touched: number;
|
|
103
|
+
}>;
|
|
104
|
+
protected readonly sweepIntervalMs: number;
|
|
105
|
+
protected readonly ttlMs: number;
|
|
106
|
+
protected lastSweepAt: number;
|
|
107
|
+
constructor(config: SessionConfig<any>, sweepIntervalSec?: number);
|
|
108
|
+
read(sessionId: string): SessionData | null;
|
|
109
|
+
write(sessionId: string, data: SessionData): void;
|
|
110
|
+
destroy(sessionId: string): void;
|
|
111
|
+
touch(sessionId: string): void;
|
|
112
|
+
protected maybeSweep(now?: number): void;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { MemorySessionStore, RedisSessionStore, Session, SessionConfig, SessionData, SessionSocketData, SessionStore, SessionStoreFactory, createSessionModule, defineConfig };
|
package/dist/session.mjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { i as UnsupportedProtocolError } from "./error-CII1zMOR.mjs";
|
|
2
|
+
import { Ultra } from "./ultra.mjs";
|
|
3
|
+
import { isHTTP, isWS } from "./context.mjs";
|
|
4
|
+
import { sign, unsign } from "./crypto.mjs";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
//#region src/session.ts
|
|
8
|
+
function defineConfig(config) {
|
|
9
|
+
return {
|
|
10
|
+
...config,
|
|
11
|
+
cookie: {
|
|
12
|
+
path: "/",
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: true,
|
|
15
|
+
sameSite: "lax",
|
|
16
|
+
maxAge: config.ttlSec,
|
|
17
|
+
...config?.cookie
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Extends context and socket data, initiate session instance every request */
|
|
22
|
+
function createSessionModule(config) {
|
|
23
|
+
return new Ultra().deriveWS((context) => ({ sessionId: Session.getOrCreateId(context.request, config) })).derive((context) => ({ session: new Session(config, context) })).use(async ({ context, next }) => {
|
|
24
|
+
await context.session.initiate();
|
|
25
|
+
const response = await next();
|
|
26
|
+
await context.session.commit();
|
|
27
|
+
return response;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Stores the ID in a cookie, then moves it to the socket and uses it for requests */
|
|
31
|
+
var Session = class Session {
|
|
32
|
+
/** Make random session id */
|
|
33
|
+
static makeId() {
|
|
34
|
+
return randomBytes(16).toString("base64url");
|
|
35
|
+
}
|
|
36
|
+
/** Get existing session ID from request cookie or create a new one */
|
|
37
|
+
static getOrCreateId(request, config) {
|
|
38
|
+
const cookie = request.cookies.get(config.name);
|
|
39
|
+
if (cookie) return unsign(cookie, config.secret) || Session.makeId();
|
|
40
|
+
return Session.makeId();
|
|
41
|
+
}
|
|
42
|
+
config;
|
|
43
|
+
context;
|
|
44
|
+
store;
|
|
45
|
+
sessionIdFromClient = null;
|
|
46
|
+
sessionId;
|
|
47
|
+
sessionState = null;
|
|
48
|
+
modified = false;
|
|
49
|
+
constructor(config, context) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.context = context;
|
|
52
|
+
this.store = config.stores[config.store](config, context);
|
|
53
|
+
switch (true) {
|
|
54
|
+
case isHTTP(context): {
|
|
55
|
+
const cookie = context.request.cookies.get(config.name);
|
|
56
|
+
if (cookie) this.sessionIdFromClient = unsign(cookie, config.secret);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case isWS(context):
|
|
60
|
+
this.sessionIdFromClient = context.ws.data.sessionId || null;
|
|
61
|
+
break;
|
|
62
|
+
default: throw new UnsupportedProtocolError("Session management is only supported for HTTP and WebSocket protocols.");
|
|
63
|
+
}
|
|
64
|
+
this.sessionId = this.sessionIdFromClient || Session.makeId();
|
|
65
|
+
}
|
|
66
|
+
get id() {
|
|
67
|
+
return this.sessionId;
|
|
68
|
+
}
|
|
69
|
+
/** Load data from session store */
|
|
70
|
+
async initiate() {
|
|
71
|
+
if (this.sessionState) return;
|
|
72
|
+
this.sessionState = await this.store.read(this.sessionId) || {};
|
|
73
|
+
}
|
|
74
|
+
/** Commit data to session store */
|
|
75
|
+
async commit() {
|
|
76
|
+
this.touch();
|
|
77
|
+
if (this.isEmpty && this.sessionIdFromClient) return this.store.destroy(this.sessionIdFromClient);
|
|
78
|
+
if (this.sessionIdFromClient && this.sessionIdFromClient !== this.sessionId) {
|
|
79
|
+
await this.store.destroy(this.sessionIdFromClient);
|
|
80
|
+
await this.store.write(this.sessionId, this.state);
|
|
81
|
+
} else if (this.modified) await this.store.write(this.sessionId, this.state);
|
|
82
|
+
else await this.store.touch(this.sessionId);
|
|
83
|
+
}
|
|
84
|
+
/** Change session id */
|
|
85
|
+
regenerate() {
|
|
86
|
+
this.sessionId = Session.makeId();
|
|
87
|
+
}
|
|
88
|
+
get(key, defaultValue) {
|
|
89
|
+
return this.state[key] ?? defaultValue ?? null;
|
|
90
|
+
}
|
|
91
|
+
set(key, value) {
|
|
92
|
+
this.state[key] = value;
|
|
93
|
+
this.modified = true;
|
|
94
|
+
}
|
|
95
|
+
has(key) {
|
|
96
|
+
return Object.hasOwn(this.state, key);
|
|
97
|
+
}
|
|
98
|
+
all() {
|
|
99
|
+
return this.state;
|
|
100
|
+
}
|
|
101
|
+
delete(key) {
|
|
102
|
+
delete this.state[key];
|
|
103
|
+
this.modified = true;
|
|
104
|
+
}
|
|
105
|
+
clear() {
|
|
106
|
+
this.sessionState = {};
|
|
107
|
+
this.modified = true;
|
|
108
|
+
}
|
|
109
|
+
get state() {
|
|
110
|
+
if (!this.sessionState) throw new Error("Session is not initiated yet.");
|
|
111
|
+
return this.sessionState;
|
|
112
|
+
}
|
|
113
|
+
get isEmpty() {
|
|
114
|
+
return Object.keys(this.state).length === 0;
|
|
115
|
+
}
|
|
116
|
+
touch() {
|
|
117
|
+
if ("request" in this.context) this.context.request.cookies.set(this.config.name, sign(this.sessionId, this.config.secret), this.config.cookie);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var RedisSessionStore = class {
|
|
121
|
+
config;
|
|
122
|
+
connection;
|
|
123
|
+
constructor(config, connection) {
|
|
124
|
+
this.config = config;
|
|
125
|
+
this.connection = connection;
|
|
126
|
+
}
|
|
127
|
+
async read(sessionId) {
|
|
128
|
+
const value = await this.connection.get(`${this.config.name}:${sessionId}`);
|
|
129
|
+
if (!value) return null;
|
|
130
|
+
return JSON.parse(value);
|
|
131
|
+
}
|
|
132
|
+
async write(sessionId, data) {
|
|
133
|
+
await this.connection.set(`${this.config.name}:${sessionId}`, JSON.stringify(data), "EX", this.config.ttlSec);
|
|
134
|
+
}
|
|
135
|
+
async destroy(sessionId) {
|
|
136
|
+
await this.connection.del(`${this.config.name}:${sessionId}`);
|
|
137
|
+
}
|
|
138
|
+
async touch(sessionId) {
|
|
139
|
+
await this.connection.expire(`${this.config.name}:${sessionId}`, this.config.ttlSec);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var MemorySessionStore = class {
|
|
143
|
+
config;
|
|
144
|
+
sessions = /* @__PURE__ */ new Map();
|
|
145
|
+
sweepIntervalMs;
|
|
146
|
+
ttlMs;
|
|
147
|
+
lastSweepAt = Date.now();
|
|
148
|
+
constructor(config, sweepIntervalSec = config.ttlSec) {
|
|
149
|
+
this.config = config;
|
|
150
|
+
this.sweepIntervalMs = sweepIntervalSec * 1e3;
|
|
151
|
+
this.ttlMs = config.ttlSec * 1e3;
|
|
152
|
+
}
|
|
153
|
+
read(sessionId) {
|
|
154
|
+
this.maybeSweep();
|
|
155
|
+
return this.sessions.get(sessionId)?.data || null;
|
|
156
|
+
}
|
|
157
|
+
write(sessionId, data) {
|
|
158
|
+
this.maybeSweep();
|
|
159
|
+
this.sessions.set(sessionId, {
|
|
160
|
+
data,
|
|
161
|
+
touched: Date.now()
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
destroy(sessionId) {
|
|
165
|
+
this.maybeSweep();
|
|
166
|
+
this.sessions.delete(sessionId);
|
|
167
|
+
}
|
|
168
|
+
touch(sessionId) {
|
|
169
|
+
this.maybeSweep();
|
|
170
|
+
const entry = this.sessions.get(sessionId);
|
|
171
|
+
if (entry) entry.touched = Date.now();
|
|
172
|
+
}
|
|
173
|
+
maybeSweep(now = Date.now()) {
|
|
174
|
+
if (now - this.lastSweepAt < this.sweepIntervalMs) return;
|
|
175
|
+
this.lastSweepAt = now;
|
|
176
|
+
for (const [sessionId, entry] of this.sessions) if (now - entry.touched > this.ttlMs) this.sessions.delete(sessionId);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
export { MemorySessionStore, RedisSessionStore, Session, createSessionModule, defineConfig };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type Promisable<T> = T | Promise<T>;
|
|
3
|
+
type JSONPrimitive = string | number | boolean | null;
|
|
4
|
+
interface JSONObject {
|
|
5
|
+
[key: string]: JSONValue;
|
|
6
|
+
}
|
|
7
|
+
type JSONArray = JSONValue[];
|
|
8
|
+
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
|
|
9
|
+
type DeepReadonly<T> = T extends Array<infer R> ? ReadonlyArray<DeepReadonly<R>> : T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : Readonly<T>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { JSONValue as a, JSONPrimitive as i, JSONArray as n, Promisable as o, JSONObject as r, DeepReadonly as t };
|
package/dist/types.d.mts
ADDED
package/dist/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/ultra.d.mts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { n as DefaultSocketData, t as BaseContext } from "./context-ChCsZh7S.mjs";
|
|
2
|
+
import { o as Promisable } from "./types-Cn69QrjS.mjs";
|
|
3
|
+
import { n as BunRoutes } from "./http-BqWCMASL.mjs";
|
|
4
|
+
import { c as StandardSchemaV1 } from "./validation-CkRfxQJ_.mjs";
|
|
5
|
+
import { a as ProcedureHandler, i as Procedure, t as Middleware } from "./middleware-COKreBGP.mjs";
|
|
6
|
+
import { n as Payload } from "./rpc-Ch2UXReT.mjs";
|
|
7
|
+
import { BunRequest, ErrorLike, Server, ServerWebSocket } from "bun";
|
|
8
|
+
|
|
9
|
+
//#region src/ultra.d.ts
|
|
10
|
+
interface ProceduresMap {
|
|
11
|
+
[key: string]: Procedure<any, any, any> | ProceduresMap;
|
|
12
|
+
}
|
|
13
|
+
interface ServerEventMap<SocketData extends DefaultSocketData = DefaultSocketData> {
|
|
14
|
+
'error': [ErrorLike];
|
|
15
|
+
'unhandled:error': [ErrorLike];
|
|
16
|
+
'http:request': [BunRequest, Server<SocketData>];
|
|
17
|
+
'ws:open': [ServerWebSocket<SocketData>];
|
|
18
|
+
'ws:message': [ServerWebSocket<SocketData>, string | Buffer<ArrayBuffer>];
|
|
19
|
+
'ws:close': [ServerWebSocket<SocketData>, number, string];
|
|
20
|
+
'server:started': [Server<SocketData>];
|
|
21
|
+
'server:stopped': [Server<SocketData>, closeActiveConnections: boolean];
|
|
22
|
+
}
|
|
23
|
+
type ServerEventListener<K extends keyof ServerEventMap> = (...args: ServerEventMap[K]) => any;
|
|
24
|
+
type DeriveFunction<C extends BaseContext> = (context: C) => Promisable<Record<PropertyKey, any>>;
|
|
25
|
+
type DeriveValue<C extends BaseContext> = DeriveFunction<C> | Record<PropertyKey, any>;
|
|
26
|
+
type ExtractDerive<C extends BaseContext, T extends DeriveValue<C>> = T extends DeriveFunction<C> ? Awaited<ReturnType<T>> : T;
|
|
27
|
+
type StartOptions<SocketData extends DefaultSocketData> = Omit<Partial<Bun.Serve.Options<SocketData>>, 'websocket' | 'error' | 'routes'>;
|
|
28
|
+
interface UltraOptions {
|
|
29
|
+
http: boolean;
|
|
30
|
+
}
|
|
31
|
+
type InputFactory<C extends BaseContext> = <I>(schema?: StandardSchemaV1<I>) => Procedure<I, unknown, C>;
|
|
32
|
+
type ProcedureMapInitializer<R extends ProceduresMap, C extends BaseContext = BaseContext> = (input: InputFactory<C>) => R;
|
|
33
|
+
declare class Ultra<Procedures extends ProceduresMap = ProceduresMap, Context extends BaseContext = BaseContext, SocketData extends DefaultSocketData = DefaultSocketData> {
|
|
34
|
+
protected readonly initializers: Set<ProcedureMapInitializer<any, Context>>;
|
|
35
|
+
protected readonly handlers: Map<string, ProcedureHandler<any, any, Context>>;
|
|
36
|
+
protected readonly events: Map<keyof ServerEventMap<SocketData>, Set<ServerEventListener<any>>>;
|
|
37
|
+
protected readonly middlewares: Set<Middleware<any, any, Context>>;
|
|
38
|
+
protected readonly derived: Set<DeriveValue<Context>>;
|
|
39
|
+
protected readonly derivedWS: Set<DeriveValue<Context>>;
|
|
40
|
+
protected readonly options: UltraOptions;
|
|
41
|
+
protected server?: Server<SocketData>;
|
|
42
|
+
constructor(options?: Partial<UltraOptions>);
|
|
43
|
+
/** Register procedures */
|
|
44
|
+
routes<const P extends ProceduresMap>(initializer: ProcedureMapInitializer<P, Context>): Ultra<Procedures & P, Context, SocketData>;
|
|
45
|
+
/** Register middleware or another Ultra instance */
|
|
46
|
+
use<const PluginProcedures extends ProceduresMap, const PluginContext extends BaseContext, const PluginSocketData extends DefaultSocketData>(entity: Middleware<unknown, unknown, Context> | Ultra<PluginProcedures, PluginContext, PluginSocketData>): Ultra<Procedures & PluginProcedures, Context & PluginContext, SocketData & PluginSocketData>;
|
|
47
|
+
/** Extends context values for every request with provided values */
|
|
48
|
+
derive<const D extends DeriveValue<Context>>(derive: D): Ultra<Procedures, Context & ExtractDerive<Context, D>, SocketData>;
|
|
49
|
+
/** Extends WS data for every ws connection */
|
|
50
|
+
deriveWS<const D extends DeriveValue<Context>>(derive: D): Ultra<Procedures, Context, SocketData & ExtractDerive<Context, D>>;
|
|
51
|
+
start(options?: StartOptions<SocketData>): Server<SocketData>;
|
|
52
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
53
|
+
on<E extends keyof ServerEventMap>(event: E, listener: ServerEventListener<E>): this;
|
|
54
|
+
off<E extends keyof ServerEventMap>(event: E, listener: ServerEventListener<E>): this;
|
|
55
|
+
emit<E extends keyof ServerEventMap>(event: E, ...args: ServerEventMap[E]): this;
|
|
56
|
+
protected handleRPC(ws: ServerWebSocket<SocketData>, payload: Payload): Promise<void>;
|
|
57
|
+
/** Enrich context with derived values */
|
|
58
|
+
protected enrichContext(context: BaseContext): Promise<Context>;
|
|
59
|
+
/** Merge other Ultra instance with deduplication */
|
|
60
|
+
protected merge(module: Ultra<any, any, any>): void;
|
|
61
|
+
/** Wrap procedure handler with global middlewares */
|
|
62
|
+
protected wrapHandler(handler: ProcedureHandler<any, any, any>): ProcedureHandler<any, any, any>;
|
|
63
|
+
/** Build flat map from procedures tree and write handles map */
|
|
64
|
+
protected buildProcedures(): Map<string, Procedure<any, any, Context>>;
|
|
65
|
+
/** Build Bun native HTTP routes */
|
|
66
|
+
protected buildRoutes(procedures: Map<string, Procedure<any, any, Context>>): BunRoutes;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { ProceduresMap, Ultra };
|
package/dist/ultra.mjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { t as Procedure } from "./procedure-BN1rLLRX.mjs";
|
|
2
|
+
import { n as toRPCResponse, t as toHTTPResponse } from "./response-CNhIkAYG.mjs";
|
|
3
|
+
import { t as isRPC } from "./rpc-_rBI0z-9.mjs";
|
|
4
|
+
import { serve } from "bun";
|
|
5
|
+
|
|
6
|
+
//#region src/ultra.ts
|
|
7
|
+
var Ultra = class {
|
|
8
|
+
initializers = /* @__PURE__ */ new Set();
|
|
9
|
+
handlers = /* @__PURE__ */ new Map();
|
|
10
|
+
events = /* @__PURE__ */ new Map();
|
|
11
|
+
middlewares = /* @__PURE__ */ new Set();
|
|
12
|
+
derived = /* @__PURE__ */ new Set();
|
|
13
|
+
derivedWS = /* @__PURE__ */ new Set();
|
|
14
|
+
options = { http: true };
|
|
15
|
+
server;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
Object.assign(this.options, options);
|
|
18
|
+
}
|
|
19
|
+
/** Register procedures */
|
|
20
|
+
routes(initializer) {
|
|
21
|
+
this.initializers.add(initializer);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
/** Register middleware or another Ultra instance */
|
|
25
|
+
use(entity) {
|
|
26
|
+
if (typeof entity === "function") {
|
|
27
|
+
this.middlewares.add(entity);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
this.merge(entity);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/** Extends context values for every request with provided values */
|
|
34
|
+
derive(derive) {
|
|
35
|
+
this.derived.add(derive);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/** Extends WS data for every ws connection */
|
|
39
|
+
deriveWS(derive) {
|
|
40
|
+
this.derivedWS.add(derive);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
start(options) {
|
|
44
|
+
if (this.server) {
|
|
45
|
+
console.warn("Server is already running");
|
|
46
|
+
return this.server;
|
|
47
|
+
}
|
|
48
|
+
const procedures = this.buildProcedures();
|
|
49
|
+
const notFoundHandler = this.wrapHandler(() => new Response("Not Found", { status: 404 }));
|
|
50
|
+
this.server = serve({
|
|
51
|
+
...options ?? {},
|
|
52
|
+
routes: {
|
|
53
|
+
...this.options.http && {
|
|
54
|
+
...this.buildRoutes(procedures),
|
|
55
|
+
"/*": async (request, server) => {
|
|
56
|
+
this.emit("http:request", request, server);
|
|
57
|
+
return notFoundHandler({
|
|
58
|
+
input: null,
|
|
59
|
+
context: this.derived.size ? await this.enrichContext({
|
|
60
|
+
server,
|
|
61
|
+
request
|
|
62
|
+
}) : {
|
|
63
|
+
server,
|
|
64
|
+
request
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"/ws": async (request, server) => {
|
|
70
|
+
if (this.derivedWS.size) {
|
|
71
|
+
const data = {};
|
|
72
|
+
const context = this.derived.size ? await this.enrichContext({
|
|
73
|
+
server,
|
|
74
|
+
request
|
|
75
|
+
}) : {
|
|
76
|
+
server,
|
|
77
|
+
request
|
|
78
|
+
};
|
|
79
|
+
for (const derive of this.derivedWS) Object.assign(data, typeof derive === "function" ? await derive(context) : derive);
|
|
80
|
+
server.upgrade(request, { data });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
server.upgrade(request);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
websocket: {
|
|
87
|
+
open: (ws) => {
|
|
88
|
+
this.emit("ws:open", ws);
|
|
89
|
+
},
|
|
90
|
+
close: (ws, code, reason) => {
|
|
91
|
+
this.emit("ws:close", ws, code, reason);
|
|
92
|
+
},
|
|
93
|
+
message: (ws, message) => {
|
|
94
|
+
this.emit("ws:message", ws, message);
|
|
95
|
+
if (typeof message !== "string") return;
|
|
96
|
+
const data = JSON.parse(message);
|
|
97
|
+
if (isRPC(data)) this.handleRPC(ws, data);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
error: (error) => {
|
|
101
|
+
this.emit("unhandled:error", error);
|
|
102
|
+
console.error("Unhandled server error:", error);
|
|
103
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.emit("server:started", this.server);
|
|
107
|
+
return this.server;
|
|
108
|
+
}
|
|
109
|
+
async stop(closeActiveConnections = false) {
|
|
110
|
+
if (!this.server) return console.error("Server is not running");
|
|
111
|
+
await this.server.stop(closeActiveConnections);
|
|
112
|
+
this.emit("server:stopped", this.server, closeActiveConnections);
|
|
113
|
+
}
|
|
114
|
+
on(event, listener) {
|
|
115
|
+
if (!this.events.has(event)) this.events.set(event, /* @__PURE__ */ new Set());
|
|
116
|
+
this.events.get(event).add(listener);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
off(event, listener) {
|
|
120
|
+
this.events.get(event)?.delete(listener);
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
emit(event, ...args) {
|
|
124
|
+
this.events.get(event)?.forEach((listener) => listener(...args));
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
async handleRPC(ws, payload) {
|
|
128
|
+
const handler = this.handlers.get(payload.method);
|
|
129
|
+
if (!handler) {
|
|
130
|
+
ws.send(`{"id": "${payload.id}", "error": {"code": 404, "message": "Not found"}}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
ws.send(toRPCResponse(payload.id, await handler({
|
|
135
|
+
input: payload.params,
|
|
136
|
+
context: await this.enrichContext({
|
|
137
|
+
server: this.server,
|
|
138
|
+
ws
|
|
139
|
+
})
|
|
140
|
+
})));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.emit("error", error);
|
|
143
|
+
ws.send(toRPCResponse(payload.id, error));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Enrich context with derived values */
|
|
147
|
+
async enrichContext(context) {
|
|
148
|
+
for (const derive of this.derived) Object.assign(context, typeof derive === "function" ? await derive(context) : derive);
|
|
149
|
+
return context;
|
|
150
|
+
}
|
|
151
|
+
/** Merge other Ultra instance with deduplication */
|
|
152
|
+
merge(module) {
|
|
153
|
+
module.initializers.forEach((init) => this.initializers.add(init));
|
|
154
|
+
module.derived.forEach((derive) => this.derived.add(derive));
|
|
155
|
+
module.derivedWS.forEach((derive) => this.derivedWS.add(derive));
|
|
156
|
+
module.middlewares.forEach((mw) => this.middlewares.add(mw));
|
|
157
|
+
module.events.forEach((listeners, event) => {
|
|
158
|
+
if (!this.events.has(event)) this.events.set(event, /* @__PURE__ */ new Set());
|
|
159
|
+
const targetListeners = this.events.get(event);
|
|
160
|
+
listeners.forEach((listener) => targetListeners.add(listener));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Wrap procedure handler with global middlewares */
|
|
164
|
+
wrapHandler(handler) {
|
|
165
|
+
if (!this.middlewares.size) return handler;
|
|
166
|
+
const middlewares = Array.from(this.middlewares);
|
|
167
|
+
return async (options) => {
|
|
168
|
+
let idx = 0;
|
|
169
|
+
const next = () => {
|
|
170
|
+
if (idx === middlewares.length) return handler(options);
|
|
171
|
+
return middlewares[idx++]({
|
|
172
|
+
...options,
|
|
173
|
+
next
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
return next();
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Build flat map from procedures tree and write handles map */
|
|
180
|
+
buildProcedures() {
|
|
181
|
+
const procedures = /* @__PURE__ */ new Map();
|
|
182
|
+
const inputFactory = (schema) => {
|
|
183
|
+
const procedure = new Procedure();
|
|
184
|
+
return schema ? procedure.input(schema) : procedure;
|
|
185
|
+
};
|
|
186
|
+
for (const initializer of this.initializers) {
|
|
187
|
+
const map = initializer(inputFactory);
|
|
188
|
+
const stack = [];
|
|
189
|
+
for (const [key, value] of Object.entries(map)) stack.push({
|
|
190
|
+
path: key,
|
|
191
|
+
value
|
|
192
|
+
});
|
|
193
|
+
while (stack.length) {
|
|
194
|
+
const { path, value } = stack.pop();
|
|
195
|
+
if (value instanceof Procedure) {
|
|
196
|
+
if (procedures.has(path)) throw new Error(`Procedure conflict at path "${path}"`);
|
|
197
|
+
procedures.set(path, value);
|
|
198
|
+
this.handlers.set(path, this.wrapHandler(value.wrap()));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
202
|
+
const nextPath = path ? `${path}/${childKey}` : childKey;
|
|
203
|
+
stack.push({
|
|
204
|
+
path: nextPath,
|
|
205
|
+
value: childValue
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return procedures;
|
|
211
|
+
}
|
|
212
|
+
/** Build Bun native HTTP routes */
|
|
213
|
+
buildRoutes(procedures) {
|
|
214
|
+
const routes = {};
|
|
215
|
+
for (const [path, procedure] of procedures) {
|
|
216
|
+
const procedureInfo = procedure.getInfo();
|
|
217
|
+
if (!procedureInfo.http?.enabled) continue;
|
|
218
|
+
const httpPath = `/${path}`;
|
|
219
|
+
const handler = this.handlers.get(path);
|
|
220
|
+
if (!handler) throw new Error(`Handler for procedure at path "${path}" is not defined`);
|
|
221
|
+
const httpHandler = async (request, server) => {
|
|
222
|
+
this.emit("http:request", request, server);
|
|
223
|
+
let input = request.body;
|
|
224
|
+
const baseContext = {
|
|
225
|
+
server,
|
|
226
|
+
request
|
|
227
|
+
};
|
|
228
|
+
const context = this.derived.size ? await this.enrichContext(baseContext) : baseContext;
|
|
229
|
+
if (input && procedureInfo.hasInput) {
|
|
230
|
+
if (request.method === "GET") {
|
|
231
|
+
const query = request.url.indexOf("?");
|
|
232
|
+
if (query !== -1 && query < request.url.length - 1) input = Object.fromEntries(new URLSearchParams(request.url.slice(query + 1)).entries());
|
|
233
|
+
} else if (request.headers.get("Content-Length") !== "0") {
|
|
234
|
+
const type = request.headers.get("Content-Type");
|
|
235
|
+
if (type) switch (true) {
|
|
236
|
+
case type.startsWith("application/json"):
|
|
237
|
+
input = await request.json();
|
|
238
|
+
break;
|
|
239
|
+
case type.startsWith("text"):
|
|
240
|
+
input = await request.text();
|
|
241
|
+
break;
|
|
242
|
+
case type.startsWith("multipart/form-data"):
|
|
243
|
+
input = await request.formData();
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
console.error(`Unsupported Content-Type for procedure ${path}: ${type}`);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
return toHTTPResponse(await handler({
|
|
253
|
+
input,
|
|
254
|
+
context
|
|
255
|
+
}));
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.emit("error", error);
|
|
258
|
+
return toHTTPResponse(error);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
if (!procedureInfo.http.method) {
|
|
262
|
+
routes[httpPath] = httpHandler;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!routes[httpPath]) routes[httpPath] = {};
|
|
266
|
+
routes[httpPath][procedureInfo.http.method] = httpHandler;
|
|
267
|
+
}
|
|
268
|
+
return routes;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
export { Ultra };
|