@firtoz/websocket-do 12.0.0 → 13.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/README.md +5 -1
- package/dist/BaseSession.d.ts +41 -0
- package/dist/BaseSession.js +5 -0
- package/dist/BaseSession.js.map +1 -0
- package/dist/BaseWebSocketDO.d.ts +42 -0
- package/dist/BaseWebSocketDO.js +4 -0
- package/dist/BaseWebSocketDO.js.map +1 -0
- package/dist/StandardSchemaSession.d.ts +41 -0
- package/dist/StandardSchemaSession.js +8 -0
- package/dist/StandardSchemaSession.js.map +1 -0
- package/dist/StandardSchemaWebSocketClient.d.ts +45 -0
- package/dist/StandardSchemaWebSocketClient.js +5 -0
- package/dist/StandardSchemaWebSocketClient.js.map +1 -0
- package/dist/StandardSchemaWebSocketDO.d.ts +28 -0
- package/dist/StandardSchemaWebSocketDO.js +5 -0
- package/dist/StandardSchemaWebSocketDO.js.map +1 -0
- package/dist/WebsocketWrapper.d.ts +9 -0
- package/dist/WebsocketWrapper.js +4 -0
- package/dist/WebsocketWrapper.js.map +1 -0
- package/dist/chunk-3C77OSOD.js +54 -0
- package/dist/chunk-3C77OSOD.js.map +1 -0
- package/dist/chunk-3LWVEY3R.js +130 -0
- package/dist/chunk-3LWVEY3R.js.map +1 -0
- package/dist/chunk-53MFRNQS.js +153 -0
- package/dist/chunk-53MFRNQS.js.map +1 -0
- package/dist/chunk-CAX4POIL.js +13 -0
- package/dist/chunk-CAX4POIL.js.map +1 -0
- package/dist/chunk-KCPOB32E.js +20 -0
- package/dist/chunk-KCPOB32E.js.map +1 -0
- package/dist/chunk-NOUFNU2O.js +10 -0
- package/dist/chunk-NOUFNU2O.js.map +1 -0
- package/dist/chunk-QMGIRIHJ.js +18 -0
- package/dist/chunk-QMGIRIHJ.js.map +1 -0
- package/dist/chunk-ULGH6X42.js +23 -0
- package/dist/chunk-ULGH6X42.js.map +1 -0
- package/dist/chunk-WJIQBI6I.js +35 -0
- package/dist/chunk-WJIQBI6I.js.map +1 -0
- package/dist/chunk-XFB6C3NZ.js +134 -0
- package/dist/chunk-XFB6C3NZ.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/parseStandardSchema.d.ts +9 -0
- package/dist/parseStandardSchema.js +4 -0
- package/dist/parseStandardSchema.js.map +1 -0
- package/dist/standardSchemaMsgpack.d.ts +8 -0
- package/dist/standardSchemaMsgpack.js +5 -0
- package/dist/standardSchemaMsgpack.js.map +1 -0
- package/dist/standardSchemaRpc.d.ts +30 -0
- package/dist/standardSchemaRpc.js +6 -0
- package/dist/standardSchemaRpc.js.map +1 -0
- package/dist/standardSchemaRpcReact.d.ts +27 -0
- package/dist/standardSchemaRpcReact.js +54 -0
- package/dist/standardSchemaRpcReact.js.map +1 -0
- package/package.json +24 -22
package/README.md
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@firtoz/websocket-do)
|
|
5
5
|
[](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://developers.cloudflare.com/durable-objects/)
|
|
9
|
+
[](https://hono.dev)
|
|
10
|
+
|
|
11
|
+
**WebSocket sessions on Durable Objects** — Standard Schema message validation, broadcasting, and Hono integration.
|
|
8
12
|
|
|
9
13
|
> **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
|
10
14
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
|
|
3
|
+
type SessionData<TSession extends BaseSession<any, any, any, any>> = TSession extends BaseSession<infer TData, infer _TServerMessage, infer _TClientMessage, infer _TEnv> ? TData : never;
|
|
4
|
+
type SessionClientMessage<TSession extends BaseSession<any, any, any, any>> = TSession extends BaseSession<infer _TData, infer _TServerMessage, infer TClientMessage, infer _TEnv> ? TClientMessage : never;
|
|
5
|
+
type SessionServerMessage<TSession extends BaseSession<any, any, any, any>> = TSession extends BaseSession<infer _TData, infer TServerMessage, infer _TClientMessage, infer _TEnv> ? TServerMessage : never;
|
|
6
|
+
type SessionEnv<TSession extends BaseSession<any, any, any, any>> = TSession extends BaseSession<infer _TData, infer _TServerMessage, infer _TClientMessage, infer TEnv extends Cloudflare.Env> ? TEnv : never;
|
|
7
|
+
type BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv extends object = Cloudflare.Env> = {
|
|
8
|
+
/**
|
|
9
|
+
* Per-connection state. If omitted, `startFresh` initializes `data` as `{}`
|
|
10
|
+
* (use empty `TData`, e.g. `Record<string, never>`).
|
|
11
|
+
*/
|
|
12
|
+
createData?: (ctx: Context<{
|
|
13
|
+
Bindings: TEnv;
|
|
14
|
+
}>) => TData;
|
|
15
|
+
handleMessage: (message: TClientMessage) => Promise<void>;
|
|
16
|
+
handleBufferMessage: (message: ArrayBuffer) => Promise<void>;
|
|
17
|
+
/** Called when this connection closes; `session` is this {@link BaseSession} instance. */
|
|
18
|
+
handleClose: (session: BaseSession<TData, TServerMessage, TClientMessage, TEnv>) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
declare class BaseSession<TData, TServerMessage, TClientMessage, TEnv extends object = Cloudflare.Env> {
|
|
21
|
+
websocket: WebSocket;
|
|
22
|
+
protected sessions: Map<WebSocket, BaseSession<TData, TServerMessage, TClientMessage, TEnv>>;
|
|
23
|
+
private _data;
|
|
24
|
+
get data(): TData;
|
|
25
|
+
private set data(value);
|
|
26
|
+
private readonly wrapper;
|
|
27
|
+
protected readonly handlers: BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv>;
|
|
28
|
+
constructor(websocket: WebSocket, sessions: Map<WebSocket, BaseSession<TData, TServerMessage, TClientMessage, TEnv>>, handlers: BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv>);
|
|
29
|
+
startFresh(ctx: Context<{
|
|
30
|
+
Bindings: TEnv;
|
|
31
|
+
}>): void;
|
|
32
|
+
resume(): void;
|
|
33
|
+
update(): void;
|
|
34
|
+
broadcast(message: TServerMessage, excludeSelf?: boolean): void;
|
|
35
|
+
send(message: TServerMessage): void;
|
|
36
|
+
handleMessage(message: TClientMessage): Promise<void>;
|
|
37
|
+
handleBufferMessage(message: ArrayBuffer): Promise<void>;
|
|
38
|
+
handleClose(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { BaseSession, type BaseSessionHandlers, type SessionClientMessage, type SessionData, type SessionEnv, type SessionServerMessage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"BaseSession.js"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as hono_hono_base from 'hono/hono-base';
|
|
2
|
+
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
3
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
4
|
+
import { DOWithHonoApp } from '@firtoz/hono-fetcher/honoDoFetcher';
|
|
5
|
+
import { Context, Hono } from 'hono';
|
|
6
|
+
import { BaseSession, SessionEnv } from './BaseSession.js';
|
|
7
|
+
|
|
8
|
+
type BaseWebSocketDOOptions<TSession extends BaseSession<any, any, any, any>, TEnv extends SessionEnv<TSession>> = {
|
|
9
|
+
createSession: (ctx: Context<{
|
|
10
|
+
Bindings: TEnv;
|
|
11
|
+
}> | undefined, websocket: WebSocket) => TSession | Promise<TSession>;
|
|
12
|
+
};
|
|
13
|
+
declare abstract class BaseWebSocketDO<TSession extends BaseSession<any, any, any, any> = BaseSession<any, any, any, any>, TEnv extends SessionEnv<TSession> = SessionEnv<TSession>> extends DurableObject<TEnv> implements DOWithHonoApp {
|
|
14
|
+
#private;
|
|
15
|
+
private readonly options;
|
|
16
|
+
protected readonly sessions: Map<WebSocket, TSession>;
|
|
17
|
+
abstract readonly app: Hono<{
|
|
18
|
+
Bindings: TEnv;
|
|
19
|
+
}>;
|
|
20
|
+
constructor(ctx: DurableObjectState, env: TEnv, options: BaseWebSocketDOOptions<TSession, TEnv>);
|
|
21
|
+
protected getBaseApp(): hono_hono_base.HonoBase<{
|
|
22
|
+
Bindings: TEnv;
|
|
23
|
+
}, {
|
|
24
|
+
"/websocket": {
|
|
25
|
+
$get: {
|
|
26
|
+
input: {};
|
|
27
|
+
output: {};
|
|
28
|
+
outputFormat: string;
|
|
29
|
+
status: hono_utils_http_status.StatusCode;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}, "/", "/websocket">;
|
|
33
|
+
handleSession(ctx: Context<{
|
|
34
|
+
Bindings: TEnv;
|
|
35
|
+
}>, ws: WebSocket): Promise<void>;
|
|
36
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
37
|
+
webSocketClose(ws: WebSocket, _code: number, _reason: string, _wasClean: boolean): Promise<void>;
|
|
38
|
+
webSocketError(ws: WebSocket, error: unknown): Promise<void>;
|
|
39
|
+
fetch(request: Request): Response | Promise<Response>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { BaseWebSocketDO, type BaseWebSocketDOOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"BaseWebSocketDO.js"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { Context } from 'hono';
|
|
3
|
+
import { BaseSession } from './BaseSession.js';
|
|
4
|
+
|
|
5
|
+
type StandardSchemaSessionOptions<TClientMessage, TServerMessage> = {
|
|
6
|
+
clientSchema: StandardSchemaV1<unknown, TClientMessage>;
|
|
7
|
+
serverSchema: StandardSchemaV1<unknown, TServerMessage>;
|
|
8
|
+
serializeJson?: (value: unknown) => string;
|
|
9
|
+
deserializeJson?: (raw: string) => unknown;
|
|
10
|
+
enableBufferMessages?: boolean;
|
|
11
|
+
sendProtocolError?: (websocket: WebSocket, errorMessage: string) => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
type StandardSchemaSessionHandlers<TData, TServerMessage, TClientMessage, TEnv extends object> = {
|
|
14
|
+
createData: (ctx: Context<{
|
|
15
|
+
Bindings: TEnv;
|
|
16
|
+
}>) => TData;
|
|
17
|
+
handleValidatedMessage: (message: TClientMessage) => Promise<void>;
|
|
18
|
+
handleValidationError?: (error: unknown, originalMessage: unknown) => Promise<void>;
|
|
19
|
+
handleClose: (session: StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
declare class StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv extends object = Cloudflare.Env> extends BaseSession<TData, TServerMessage, TClientMessage, TEnv> {
|
|
22
|
+
private readonly options;
|
|
23
|
+
private readonly schemaHandlers;
|
|
24
|
+
private readonly clientCodec;
|
|
25
|
+
private readonly serverCodec;
|
|
26
|
+
protected readonly enableBufferMessages: boolean;
|
|
27
|
+
constructor(websocket: WebSocket, sessions: Map<WebSocket, StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>>, options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>, schemaHandlers: StandardSchemaSessionHandlers<TData, TServerMessage, TClientMessage, TEnv>);
|
|
28
|
+
handleRawMessage(rawMessage: string): Promise<void>;
|
|
29
|
+
private _internalHandleMessage;
|
|
30
|
+
private _internalHandleBufferMessage;
|
|
31
|
+
private _internalHandleValidationError;
|
|
32
|
+
send(message: TServerMessage): void;
|
|
33
|
+
private sendJsonAsync;
|
|
34
|
+
private sendBufferAsync;
|
|
35
|
+
private sendProtocolError;
|
|
36
|
+
private serializeJson;
|
|
37
|
+
private deserializeJson;
|
|
38
|
+
broadcast(message: TServerMessage, excludeSelf?: boolean): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { StandardSchemaSession, type StandardSchemaSessionHandlers, type StandardSchemaSessionOptions };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { StandardSchemaSession } from './chunk-53MFRNQS.js';
|
|
2
|
+
import './chunk-QMGIRIHJ.js';
|
|
3
|
+
import './chunk-3C77OSOD.js';
|
|
4
|
+
import './chunk-KCPOB32E.js';
|
|
5
|
+
import './chunk-CAX4POIL.js';
|
|
6
|
+
import './chunk-NOUFNU2O.js';
|
|
7
|
+
//# sourceMappingURL=StandardSchemaSession.js.map
|
|
8
|
+
//# sourceMappingURL=StandardSchemaSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"StandardSchemaSession.js"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
interface StandardSchemaWebSocketClientOptions<TClientMessage, TServerMessage> {
|
|
4
|
+
/**
|
|
5
|
+
* URL to connect to (required if webSocket not provided)
|
|
6
|
+
*/
|
|
7
|
+
url?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Existing WebSocket to wrap (alternative to url)
|
|
10
|
+
* Useful when getting a WebSocket from honoDoFetcher
|
|
11
|
+
*/
|
|
12
|
+
webSocket?: WebSocket;
|
|
13
|
+
clientSchema: StandardSchemaV1<unknown, TClientMessage>;
|
|
14
|
+
serverSchema: StandardSchemaV1<unknown, TServerMessage>;
|
|
15
|
+
serializeJson?: (value: unknown) => string;
|
|
16
|
+
deserializeJson?: (raw: string) => unknown;
|
|
17
|
+
enableBufferMessages?: boolean;
|
|
18
|
+
onMessage?: (message: TServerMessage) => void;
|
|
19
|
+
onOpen?: (event: Event) => void;
|
|
20
|
+
onClose?: (event: CloseEvent) => void;
|
|
21
|
+
onError?: (event: Event) => void;
|
|
22
|
+
onValidationError?: (error: Error, rawMessage: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
declare class StandardSchemaWebSocketClient<TClientMessage, TServerMessage> {
|
|
25
|
+
private ws;
|
|
26
|
+
private readonly clientSchema;
|
|
27
|
+
private readonly serverSchema;
|
|
28
|
+
private readonly serializeJson;
|
|
29
|
+
private readonly deserializeJson;
|
|
30
|
+
private readonly enableBufferMessages;
|
|
31
|
+
private readonly onMessageCallback?;
|
|
32
|
+
private readonly onValidationError?;
|
|
33
|
+
constructor(options: StandardSchemaWebSocketClientOptions<TClientMessage, TServerMessage>);
|
|
34
|
+
private handleMessageEvent;
|
|
35
|
+
/**
|
|
36
|
+
* Send a message (automatically encodes based on mode).
|
|
37
|
+
*/
|
|
38
|
+
send(message: TClientMessage): Promise<void>;
|
|
39
|
+
close(code?: number, reason?: string): void;
|
|
40
|
+
get readyState(): number;
|
|
41
|
+
get socket(): WebSocket;
|
|
42
|
+
waitForOpen(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { StandardSchemaWebSocketClient, type StandardSchemaWebSocketClientOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"StandardSchemaWebSocketClient.js"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { SessionEnv, SessionClientMessage, SessionServerMessage } from './BaseSession.js';
|
|
3
|
+
import { BaseWebSocketDO } from './BaseWebSocketDO.js';
|
|
4
|
+
import { StandardSchemaSessionOptions, StandardSchemaSession } from './StandardSchemaSession.js';
|
|
5
|
+
import 'hono/hono-base';
|
|
6
|
+
import 'hono/utils/http-status';
|
|
7
|
+
import 'cloudflare:workers';
|
|
8
|
+
import '@firtoz/hono-fetcher/honoDoFetcher';
|
|
9
|
+
import '@standard-schema/spec';
|
|
10
|
+
|
|
11
|
+
type StandardSchemaSessionOptionsOrFactory<TClientMessage, TServerMessage, TEnv extends Cloudflare.Env = Cloudflare.Env> = StandardSchemaSessionOptions<TClientMessage, TServerMessage> | ((ctx: Context<{
|
|
12
|
+
Bindings: TEnv;
|
|
13
|
+
}> | undefined, websocket: WebSocket) => StandardSchemaSessionOptions<TClientMessage, TServerMessage>);
|
|
14
|
+
type StandardSchemaWebSocketDOOptions<TSession extends StandardSchemaSession<any, any, any, any>, TClientMessage, TServerMessage, TEnv extends SessionEnv<TSession>> = {
|
|
15
|
+
standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<TClientMessage, TServerMessage, TEnv>;
|
|
16
|
+
createStandardSchemaSession: (ctx: Context<{
|
|
17
|
+
Bindings: TEnv;
|
|
18
|
+
}> | undefined, websocket: WebSocket, options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>) => TSession | Promise<TSession>;
|
|
19
|
+
};
|
|
20
|
+
declare abstract class StandardSchemaWebSocketDO<TSession extends StandardSchemaSession<any, any, any, any>, TClientMessage extends SessionClientMessage<TSession> = SessionClientMessage<TSession>, TServerMessage extends SessionServerMessage<TSession> = SessionServerMessage<TSession>, TEnv extends SessionEnv<TSession> = SessionEnv<TSession>> extends BaseWebSocketDO<TSession, TEnv> {
|
|
21
|
+
protected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<TClientMessage, TServerMessage, TEnv>;
|
|
22
|
+
protected readonly createStandardSchemaSessionFn: (ctx: Context<{
|
|
23
|
+
Bindings: TEnv;
|
|
24
|
+
}> | undefined, websocket: WebSocket, options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>) => TSession | Promise<TSession>;
|
|
25
|
+
constructor(ctx: DurableObjectState, env: TEnv, options: StandardSchemaWebSocketDOOptions<TSession, TClientMessage, TServerMessage, TEnv>);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { type StandardSchemaSessionOptionsOrFactory, StandardSchemaWebSocketDO, type StandardSchemaWebSocketDOOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"StandardSchemaWebSocketDO.js"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare class WebsocketWrapper<TAttachment, TMessage> {
|
|
2
|
+
webSocket: WebSocket;
|
|
3
|
+
constructor(webSocket: WebSocket);
|
|
4
|
+
send(message: TMessage): void;
|
|
5
|
+
deserializeAttachment(): TAttachment;
|
|
6
|
+
serializeAttachment(attachment: TAttachment): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { WebsocketWrapper };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"WebsocketWrapper.js"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { WebsocketWrapper } from './chunk-KCPOB32E.js';
|
|
2
|
+
|
|
3
|
+
// src/BaseSession.ts
|
|
4
|
+
var BaseSession = class {
|
|
5
|
+
constructor(websocket, sessions, handlers) {
|
|
6
|
+
this.websocket = websocket;
|
|
7
|
+
this.sessions = sessions;
|
|
8
|
+
this.wrapper = new WebsocketWrapper(websocket);
|
|
9
|
+
this.handlers = handlers;
|
|
10
|
+
}
|
|
11
|
+
get data() {
|
|
12
|
+
return this._data;
|
|
13
|
+
}
|
|
14
|
+
set data(data) {
|
|
15
|
+
this._data = data;
|
|
16
|
+
}
|
|
17
|
+
startFresh(ctx) {
|
|
18
|
+
this.data = this.handlers.createData !== void 0 ? this.handlers.createData(ctx) : {};
|
|
19
|
+
this.wrapper.serializeAttachment(this.data);
|
|
20
|
+
}
|
|
21
|
+
resume() {
|
|
22
|
+
const existingData = this.wrapper.deserializeAttachment();
|
|
23
|
+
if (existingData) {
|
|
24
|
+
this.data = existingData;
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error("No data to resume");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
update() {
|
|
30
|
+
this.wrapper.serializeAttachment(this.data);
|
|
31
|
+
}
|
|
32
|
+
broadcast(message, excludeSelf = false) {
|
|
33
|
+
for (const session of this.sessions.values()) {
|
|
34
|
+
if (excludeSelf && session === this) continue;
|
|
35
|
+
session.send(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
send(message) {
|
|
39
|
+
this.wrapper.send(message);
|
|
40
|
+
}
|
|
41
|
+
async handleMessage(message) {
|
|
42
|
+
return this.handlers.handleMessage(message);
|
|
43
|
+
}
|
|
44
|
+
async handleBufferMessage(message) {
|
|
45
|
+
return this.handlers.handleBufferMessage(message);
|
|
46
|
+
}
|
|
47
|
+
async handleClose() {
|
|
48
|
+
return this.handlers.handleClose(this);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export { BaseSession };
|
|
53
|
+
//# sourceMappingURL=chunk-3C77OSOD.js.map
|
|
54
|
+
//# sourceMappingURL=chunk-3C77OSOD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/BaseSession.ts"],"names":[],"mappings":";;;AAwEO,IAAM,cAAN,MAKL;AAAA,EAmBD,WAAA,CACQ,SAAA,EACG,QAAA,EAIV,QAAA,EACC;AANM,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACG,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAMV,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,gBAAA,CAAwC,SAAS,CAAA;AACpE,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAAA,EACjB;AAAA,EA1BA,IAAW,IAAA,GAAc;AACxB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACb;AAAA,EAEA,IAAY,KAAK,IAAA,EAAa;AAC7B,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AAAA,EACd;AAAA,EAsBO,WAAW,GAAA,EAAkC;AACnD,IAAA,IAAA,CAAK,IAAA,GACJ,IAAA,CAAK,QAAA,CAAS,UAAA,KAAe,MAAA,GAC1B,KAAK,QAAA,CAAS,UAAA,CAAW,GAAG,CAAA,GAC3B,EAAC;AACN,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,IAAA,CAAK,IAAI,CAAA;AAAA,EAC3C;AAAA,EAEO,MAAA,GAAS;AACf,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAsB;AACxD,IAAA,IAAI,YAAA,EAAc;AACjB,MAAA,IAAA,CAAK,IAAA,GAAO,YAAA;AAAA,IACb,CAAA,MAAO;AACN,MAAA,MAAM,IAAI,MAAM,mBAAmB,CAAA;AAAA,IACpC;AAAA,EACD;AAAA,EAEO,MAAA,GAAS;AACf,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,IAAA,CAAK,IAAI,CAAA;AAAA,EAC3C;AAAA,EAEO,SAAA,CAAU,OAAA,EAAyB,WAAA,GAAc,KAAA,EAAO;AAC9D,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,CAAS,MAAA,EAAO,EAAG;AAC7C,MAAA,IAAI,WAAA,IAAe,YAAY,IAAA,EAAM;AACrC,MAAA,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,IACrB;AAAA,EACD;AAAA,EAEO,KAAK,OAAA,EAAyB;AACpC,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,EAC1B;AAAA,EAEA,MAAM,cAAc,OAAA,EAAwC;AAC3D,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,oBAAoB,OAAA,EAAqC;AAC9D,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,mBAAA,CAAoB,OAAO,CAAA;AAAA,EACjD;AAAA,EAEA,MAAM,WAAA,GAA6B;AAClC,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,WAAA,CAAY,IAAI,CAAA;AAAA,EACtC;AACD","file":"chunk-3C77OSOD.js","sourcesContent":["import type { Context } from \"hono\";\nimport { WebsocketWrapper } from \"./WebsocketWrapper\";\n\n// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\nexport type SessionData<TSession extends BaseSession<any, any, any, any>> =\n\tTSession extends BaseSession<\n\t\tinfer TData,\n\t\tinfer _TServerMessage,\n\t\tinfer _TClientMessage,\n\t\tinfer _TEnv\n\t>\n\t\t? TData\n\t\t: never;\n\nexport type SessionClientMessage<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends BaseSession<any, any, any, any>,\n> =\n\tTSession extends BaseSession<\n\t\tinfer _TData,\n\t\tinfer _TServerMessage,\n\t\tinfer TClientMessage,\n\t\tinfer _TEnv\n\t>\n\t\t? TClientMessage\n\t\t: never;\n\nexport type SessionServerMessage<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends BaseSession<any, any, any, any>,\n> =\n\tTSession extends BaseSession<\n\t\tinfer _TData,\n\t\tinfer TServerMessage,\n\t\tinfer _TClientMessage,\n\t\tinfer _TEnv\n\t>\n\t\t? TServerMessage\n\t\t: never;\n\nexport type SessionEnv<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends BaseSession<any, any, any, any>,\n> =\n\tTSession extends BaseSession<\n\t\tinfer _TData,\n\t\tinfer _TServerMessage,\n\t\tinfer _TClientMessage,\n\t\tinfer TEnv extends Cloudflare.Env\n\t>\n\t\t? TEnv\n\t\t: never;\n\nexport type BaseSessionHandlers<\n\tTData,\n\tTServerMessage,\n\tTClientMessage,\n\tTEnv extends object = Cloudflare.Env,\n> = {\n\t/**\n\t * Per-connection state. If omitted, `startFresh` initializes `data` as `{}`\n\t * (use empty `TData`, e.g. `Record<string, never>`).\n\t */\n\tcreateData?: (ctx: Context<{ Bindings: TEnv }>) => TData;\n\thandleMessage: (message: TClientMessage) => Promise<void>;\n\thandleBufferMessage: (message: ArrayBuffer) => Promise<void>;\n\t/** Called when this connection closes; `session` is this {@link BaseSession} instance. */\n\thandleClose: (\n\t\tsession: BaseSession<TData, TServerMessage, TClientMessage, TEnv>,\n\t) => Promise<void>;\n};\n\nexport class BaseSession<\n\tTData,\n\tTServerMessage,\n\tTClientMessage,\n\tTEnv extends object = Cloudflare.Env,\n> {\n\tprivate _data!: TData;\n\n\tpublic get data(): TData {\n\t\treturn this._data;\n\t}\n\n\tprivate set data(data: TData) {\n\t\tthis._data = data;\n\t}\n\n\tprivate readonly wrapper: WebsocketWrapper<TData, TServerMessage>;\n\tprotected readonly handlers: BaseSessionHandlers<\n\t\tTData,\n\t\tTServerMessage,\n\t\tTClientMessage,\n\t\tTEnv\n\t>;\n\n\tconstructor(\n\t\tpublic websocket: WebSocket,\n\t\tprotected sessions: Map<\n\t\t\tWebSocket,\n\t\t\tBaseSession<TData, TServerMessage, TClientMessage, TEnv>\n\t\t>,\n\t\thandlers: BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv>,\n\t) {\n\t\tthis.wrapper = new WebsocketWrapper<TData, TServerMessage>(websocket);\n\t\tthis.handlers = handlers;\n\t}\n\n\tpublic startFresh(ctx: Context<{ Bindings: TEnv }>) {\n\t\tthis.data =\n\t\t\tthis.handlers.createData !== undefined\n\t\t\t\t? this.handlers.createData(ctx)\n\t\t\t\t: ({} as TData);\n\t\tthis.wrapper.serializeAttachment(this.data);\n\t}\n\n\tpublic resume() {\n\t\tconst existingData = this.wrapper.deserializeAttachment();\n\t\tif (existingData) {\n\t\t\tthis.data = existingData;\n\t\t} else {\n\t\t\tthrow new Error(\"No data to resume\");\n\t\t}\n\t}\n\n\tpublic update() {\n\t\tthis.wrapper.serializeAttachment(this.data);\n\t}\n\n\tpublic broadcast(message: TServerMessage, excludeSelf = false) {\n\t\tfor (const session of this.sessions.values()) {\n\t\t\tif (excludeSelf && session === this) continue;\n\t\t\tsession.send(message);\n\t\t}\n\t}\n\n\tpublic send(message: TServerMessage) {\n\t\tthis.wrapper.send(message);\n\t}\n\n\tasync handleMessage(message: TClientMessage): Promise<void> {\n\t\treturn this.handlers.handleMessage(message);\n\t}\n\n\tasync handleBufferMessage(message: ArrayBuffer): Promise<void> {\n\t\treturn this.handlers.handleBufferMessage(message);\n\t}\n\n\tasync handleClose(): Promise<void> {\n\t\treturn this.handlers.handleClose(this);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { parseStandardSchema } from './chunk-CAX4POIL.js';
|
|
2
|
+
import { unpack, pack } from 'msgpackr';
|
|
3
|
+
|
|
4
|
+
var StandardSchemaWebSocketClient = class {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.clientSchema = options.clientSchema;
|
|
7
|
+
this.serverSchema = options.serverSchema;
|
|
8
|
+
this.serializeJson = options.serializeJson ?? JSON.stringify;
|
|
9
|
+
this.deserializeJson = options.deserializeJson ?? JSON.parse;
|
|
10
|
+
this.enableBufferMessages = options.enableBufferMessages ?? false;
|
|
11
|
+
this.onMessageCallback = options.onMessage;
|
|
12
|
+
this.onValidationError = options.onValidationError;
|
|
13
|
+
if (options.webSocket) {
|
|
14
|
+
this.ws = options.webSocket;
|
|
15
|
+
} else if (options.url) {
|
|
16
|
+
this.ws = new WebSocket(options.url);
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error("Either 'url' or 'webSocket' must be provided");
|
|
19
|
+
}
|
|
20
|
+
if (this.enableBufferMessages) {
|
|
21
|
+
this.ws.binaryType = "arraybuffer";
|
|
22
|
+
}
|
|
23
|
+
this.ws.addEventListener("open", (event) => {
|
|
24
|
+
options.onOpen?.(event);
|
|
25
|
+
});
|
|
26
|
+
this.ws.addEventListener("message", (event) => {
|
|
27
|
+
void this.handleMessageEvent(event);
|
|
28
|
+
});
|
|
29
|
+
this.ws.addEventListener("close", (event) => {
|
|
30
|
+
options.onClose?.(event);
|
|
31
|
+
});
|
|
32
|
+
this.ws.addEventListener("error", (event) => {
|
|
33
|
+
options.onError?.(event);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async handleMessageEvent(event) {
|
|
37
|
+
try {
|
|
38
|
+
let parsedMessage;
|
|
39
|
+
if (this.enableBufferMessages) {
|
|
40
|
+
if (!(event.data instanceof ArrayBuffer)) {
|
|
41
|
+
console.error(
|
|
42
|
+
"Expected ArrayBuffer but received:",
|
|
43
|
+
typeof event.data
|
|
44
|
+
);
|
|
45
|
+
this.onValidationError?.(
|
|
46
|
+
new Error("Expected ArrayBuffer in buffer mode"),
|
|
47
|
+
event.data
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const unpacked = unpack(new Uint8Array(event.data));
|
|
52
|
+
parsedMessage = await parseStandardSchema(this.serverSchema, unpacked);
|
|
53
|
+
} else {
|
|
54
|
+
if (typeof event.data !== "string") {
|
|
55
|
+
console.error("Expected string but received:", typeof event.data);
|
|
56
|
+
this.onValidationError?.(
|
|
57
|
+
new Error("Expected string in JSON mode"),
|
|
58
|
+
event.data
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const parsed = this.deserializeJson(event.data);
|
|
63
|
+
parsedMessage = await parseStandardSchema(this.serverSchema, parsed);
|
|
64
|
+
}
|
|
65
|
+
this.onMessageCallback?.(parsedMessage);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to process message:", error);
|
|
68
|
+
this.onValidationError?.(
|
|
69
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
70
|
+
event.data
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Send a message (automatically encodes based on mode).
|
|
76
|
+
*/
|
|
77
|
+
async send(message) {
|
|
78
|
+
const validatedMessage = await parseStandardSchema(
|
|
79
|
+
this.clientSchema,
|
|
80
|
+
message
|
|
81
|
+
);
|
|
82
|
+
if (this.enableBufferMessages) {
|
|
83
|
+
const packed = pack(validatedMessage);
|
|
84
|
+
this.ws.send(new Uint8Array(packed));
|
|
85
|
+
} else {
|
|
86
|
+
this.ws.send(this.serializeJson(validatedMessage));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
close(code, reason) {
|
|
90
|
+
this.ws.close(code, reason);
|
|
91
|
+
}
|
|
92
|
+
get readyState() {
|
|
93
|
+
return this.ws.readyState;
|
|
94
|
+
}
|
|
95
|
+
get socket() {
|
|
96
|
+
return this.ws;
|
|
97
|
+
}
|
|
98
|
+
async waitForOpen() {
|
|
99
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const abortController = new AbortController();
|
|
104
|
+
const { signal } = abortController;
|
|
105
|
+
const cleanup = () => {
|
|
106
|
+
abortController.abort();
|
|
107
|
+
};
|
|
108
|
+
this.ws.addEventListener(
|
|
109
|
+
"open",
|
|
110
|
+
() => {
|
|
111
|
+
cleanup();
|
|
112
|
+
resolve();
|
|
113
|
+
},
|
|
114
|
+
{ signal }
|
|
115
|
+
);
|
|
116
|
+
this.ws.addEventListener(
|
|
117
|
+
"error",
|
|
118
|
+
() => {
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(new Error("WebSocket connection failed"));
|
|
121
|
+
},
|
|
122
|
+
{ signal }
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { StandardSchemaWebSocketClient };
|
|
129
|
+
//# sourceMappingURL=chunk-3LWVEY3R.js.map
|
|
130
|
+
//# sourceMappingURL=chunk-3LWVEY3R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/StandardSchemaWebSocketClient.ts"],"names":[],"mappings":"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","file":"chunk-3LWVEY3R.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { pack, unpack } from \"msgpackr\";\nimport { parseStandardSchema } from \"./parseStandardSchema\";\n\nexport interface StandardSchemaWebSocketClientOptions<\n\tTClientMessage,\n\tTServerMessage,\n> {\n\t/**\n\t * URL to connect to (required if webSocket not provided)\n\t */\n\turl?: string;\n\t/**\n\t * Existing WebSocket to wrap (alternative to url)\n\t * Useful when getting a WebSocket from honoDoFetcher\n\t */\n\twebSocket?: WebSocket;\n\tclientSchema: StandardSchemaV1<unknown, TClientMessage>;\n\tserverSchema: StandardSchemaV1<unknown, TServerMessage>;\n\tserializeJson?: (value: unknown) => string;\n\tdeserializeJson?: (raw: string) => unknown;\n\tenableBufferMessages?: boolean;\n\tonMessage?: (message: TServerMessage) => void;\n\tonOpen?: (event: Event) => void;\n\tonClose?: (event: CloseEvent) => void;\n\tonError?: (event: Event) => void;\n\tonValidationError?: (error: Error, rawMessage: unknown) => void;\n}\n\nexport class StandardSchemaWebSocketClient<TClientMessage, TServerMessage> {\n\tprivate ws: WebSocket;\n\tprivate readonly clientSchema: StandardSchemaV1<unknown, TClientMessage>;\n\tprivate readonly serverSchema: StandardSchemaV1<unknown, TServerMessage>;\n\tprivate readonly serializeJson: (value: unknown) => string;\n\tprivate readonly deserializeJson: (raw: string) => unknown;\n\tprivate readonly enableBufferMessages: boolean;\n\tprivate readonly onMessageCallback?: (message: TServerMessage) => void;\n\tprivate readonly onValidationError?: (\n\t\terror: Error,\n\t\trawMessage: unknown,\n\t) => void;\n\n\tconstructor(\n\t\toptions: StandardSchemaWebSocketClientOptions<\n\t\t\tTClientMessage,\n\t\t\tTServerMessage\n\t\t>,\n\t) {\n\t\tthis.clientSchema = options.clientSchema;\n\t\tthis.serverSchema = options.serverSchema;\n\t\tthis.serializeJson = options.serializeJson ?? JSON.stringify;\n\t\tthis.deserializeJson = options.deserializeJson ?? JSON.parse;\n\t\tthis.enableBufferMessages = options.enableBufferMessages ?? false;\n\t\tthis.onMessageCallback = options.onMessage;\n\t\tthis.onValidationError = options.onValidationError;\n\n\t\tif (options.webSocket) {\n\t\t\tthis.ws = options.webSocket;\n\t\t} else if (options.url) {\n\t\t\tthis.ws = new WebSocket(options.url);\n\t\t} else {\n\t\t\tthrow new Error(\"Either 'url' or 'webSocket' must be provided\");\n\t\t}\n\n\t\tif (this.enableBufferMessages) {\n\t\t\tthis.ws.binaryType = \"arraybuffer\";\n\t\t}\n\n\t\tthis.ws.addEventListener(\"open\", (event) => {\n\t\t\toptions.onOpen?.(event);\n\t\t});\n\n\t\tthis.ws.addEventListener(\"message\", (event) => {\n\t\t\tvoid this.handleMessageEvent(event);\n\t\t});\n\n\t\tthis.ws.addEventListener(\"close\", (event) => {\n\t\t\toptions.onClose?.(event);\n\t\t});\n\n\t\tthis.ws.addEventListener(\"error\", (event) => {\n\t\t\toptions.onError?.(event);\n\t\t});\n\t}\n\n\tprivate async handleMessageEvent(event: MessageEvent): Promise<void> {\n\t\ttry {\n\t\t\tlet parsedMessage: TServerMessage;\n\n\t\t\tif (this.enableBufferMessages) {\n\t\t\t\tif (!(event.data instanceof ArrayBuffer)) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\"Expected ArrayBuffer but received:\",\n\t\t\t\t\t\ttypeof event.data,\n\t\t\t\t\t);\n\t\t\t\t\tthis.onValidationError?.(\n\t\t\t\t\t\tnew Error(\"Expected ArrayBuffer in buffer mode\"),\n\t\t\t\t\t\tevent.data,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst unpacked = unpack(new Uint8Array(event.data));\n\t\t\t\tparsedMessage = await parseStandardSchema(this.serverSchema, unpacked);\n\t\t\t} else {\n\t\t\t\tif (typeof event.data !== \"string\") {\n\t\t\t\t\tconsole.error(\"Expected string but received:\", typeof event.data);\n\t\t\t\t\tthis.onValidationError?.(\n\t\t\t\t\t\tnew Error(\"Expected string in JSON mode\"),\n\t\t\t\t\t\tevent.data,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst parsed = this.deserializeJson(event.data);\n\t\t\t\tparsedMessage = await parseStandardSchema(this.serverSchema, parsed);\n\t\t\t}\n\n\t\t\tthis.onMessageCallback?.(parsedMessage);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to process message:\", error);\n\t\t\tthis.onValidationError?.(\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t\tevent.data,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Send a message (automatically encodes based on mode).\n\t */\n\tasync send(message: TClientMessage): Promise<void> {\n\t\tconst validatedMessage = await parseStandardSchema(\n\t\t\tthis.clientSchema,\n\t\t\tmessage,\n\t\t);\n\n\t\tif (this.enableBufferMessages) {\n\t\t\tconst packed = pack(validatedMessage);\n\t\t\tthis.ws.send(new Uint8Array(packed));\n\t\t} else {\n\t\t\tthis.ws.send(this.serializeJson(validatedMessage));\n\t\t}\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.ws.close(code, reason);\n\t}\n\n\tget readyState(): number {\n\t\treturn this.ws.readyState;\n\t}\n\n\tget socket(): WebSocket {\n\t\treturn this.ws;\n\t}\n\n\tasync waitForOpen(): Promise<void> {\n\t\tif (this.ws.readyState === WebSocket.OPEN) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst abortController = new AbortController();\n\t\t\tconst { signal } = abortController;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tabortController.abort();\n\t\t\t};\n\n\t\t\tthis.ws.addEventListener(\n\t\t\t\t\"open\",\n\t\t\t\t() => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\n\t\t\tthis.ws.addEventListener(\n\t\t\t\t\"error\",\n\t\t\t\t() => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(new Error(\"WebSocket connection failed\"));\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t});\n\t}\n}\n"]}
|