@excali-boards/boards-api-client 1.1.33 โ†’ 1.1.34

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 CHANGED
@@ -1,88 +1,59 @@
1
1
  # ๐Ÿงฐ boards-api-client
2
2
 
3
- A TypeScript client library for interacting with the [Boards Room API](https://github.com/Excali-Boards), the backend behind the collaborative whiteboarding platform. This SDK simplifies API integration for developers building apps on top of Boards infrastructure.
3
+ A comprehensive TypeScript client library for the [Boards Room API](https://github.com/Excali-Boards), featuring both REST API and WebSocket support. Build real-time collaborative applications with ease.
4
4
 
5
5
  ---
6
6
 
7
- ## ๐Ÿš€ Features
7
+ ## Features
8
8
 
9
- * Fully typed API wrapper for the Boards backend
10
- * CRUD support for:
9
+ - Fully typed REST client (boards, users, permissions, files, sessions, categories, flashcards, metrics, invites)
10
+ - WebSocket client with auto-reconnect, heartbeat, and message queueing
11
+ - React hooks: generic `useWebSocket` and `useExecutor` for code execution streams
12
+ - Zero-config defaults; override `baseUrl` when needed
11
13
 
12
- * ๐Ÿข Groups and ๐Ÿ“‚ Categories
13
- * ๐Ÿ“ Boards and ๐Ÿ–ผ๏ธ Rooms
14
- * ๐Ÿ‘ค Users and ๐Ÿ” Permissions
15
- * Real-time room metadata access and user management
16
- * OAuth-based authentication support
17
- * Utility endpoints for resolving board references and cleanup
18
- * Built-in Axios request handler with date transformation
19
-
20
- ---
21
-
22
- ## ๐Ÿ“ฆ Installation
14
+ ## Installation
23
15
 
24
16
  ```bash
25
- npm install boards-api-client
17
+ pnpm add @excali-boards/boards-api-client
26
18
  # or
27
- pnpm add boards-api-client
19
+ npm install @excali-boards/boards-api-client
28
20
  ```
29
21
 
30
- ---
22
+ ## Quick start
31
23
 
32
- ## โœจ Usage
33
-
34
- ### Initialize the client
24
+ ### REST
35
25
 
36
26
  ```ts
37
- import { BoardsManager } from 'boards-api-client';
27
+ import { BoardsManager } from "@excali-boards/boards-api-client";
38
28
 
39
- const client = new BoardsManager('https://your-api-url.com');
29
+ const api = new BoardsManager("https://api.example.com");
30
+ const boards = await api.boards.list();
40
31
  ```
41
32
 
42
- ### Access a module
33
+ ### WebSocket (generic)
43
34
 
44
35
  ```ts
45
- const authToken = 'Bearer YOUR_TOKEN_HERE';
36
+ import { createConnection } from "@excali-boards/boards-api-client";
46
37
 
47
- const groups = await client.groups.getGroups({ auth: authToken });
48
- console.log(groups);
38
+ const ws = createConnection("/executor", { baseUrl: "localhost:3000" });
39
+ await ws.connect();
40
+ ws.send(1, { code: "print(42)", language: "python" });
41
+ ws.on(3, (data) => console.log("output:", data.output));
49
42
  ```
50
43
 
51
- ### Common endpoints
52
-
53
- * `client.auth.authenticate(...)` โ€” Login a user
54
- * `client.groups.getAllSorted(...)` โ€” Fetch full hierarchy
55
- * `client.boards.getBoard(...)` โ€” Fetch single board info
56
- * `client.admin.updateUserPermissions(...)` โ€” Update admin permissions
57
- * `client.stats.globalStats(...)` โ€” Fetch global app statistics
58
- * `client.utils.resolveBoard(...)` โ€” Resolve board ID by name
59
-
60
- ---
44
+ ### React hook (executor)
61
45
 
62
- ## ๐Ÿงช Example
46
+ ```tsx
47
+ import { useExecutor } from "@excali-boards/boards-api-client";
63
48
 
64
- ```ts
65
- const data = await client.boards.getBoard({
66
- auth: token,
67
- groupId: 'grp123',
68
- categoryId: 'cat456',
69
- boardId: 'brd789'
70
- });
71
-
72
- console.log(data.board.name);
49
+ const { isConnected, startSession, sendInput, outputs } = useExecutor();
73
50
  ```
74
51
 
75
- ---
76
-
77
- ## ๐Ÿ› ๏ธ Development
52
+ ## Integrations
78
53
 
79
- Clone the repo and install dependencies:
80
-
81
- ```bash
82
- git clone https://github.com/Excali-Boards/boards-api-client.git
83
- cd boards-api-client
84
- pnpm install
85
- ```
54
+ - React/Next: `useWebSocket`, `useExecutor`
55
+ - Node/SSR: `createConnection` without React
56
+ - TypeScript-first: message payloads and REST responses are typed
86
57
 
87
58
  ---
88
59
 
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export * from './core/manager';
2
2
  export * from './types';
3
3
  export * from './external/types';
4
4
  export * from './external/vars';
5
+ export * from './websocket/manager';
6
+ export * from './websocket/client';
7
+ export * from './websocket/types';
5
8
  export * from './classes/permissions';
6
9
  export * from './classes/categories';
7
10
  export * from './classes/flashcards';
package/dist/index.js CHANGED
@@ -18,6 +18,9 @@ __exportStar(require("./core/manager"), exports);
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./external/types"), exports);
20
20
  __exportStar(require("./external/vars"), exports);
21
+ __exportStar(require("./websocket/manager"), exports);
22
+ __exportStar(require("./websocket/client"), exports);
23
+ __exportStar(require("./websocket/types"), exports);
21
24
  __exportStar(require("./classes/permissions"), exports);
22
25
  __exportStar(require("./classes/categories"), exports);
23
26
  __exportStar(require("./classes/flashcards"), exports);
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/types.ts","../src/classes/admin.ts","../src/classes/boards.ts","../src/classes/calendar.ts","../src/classes/categories.ts","../src/classes/files.ts","../src/classes/flashcards.ts","../src/classes/groups.ts","../src/classes/invites.ts","../src/classes/metrics.ts","../src/classes/permissions.ts","../src/classes/sessions.ts","../src/classes/users.ts","../src/classes/utils.ts","../src/core/manager.ts","../src/core/utils.ts","../src/external/types.ts","../src/external/vars.ts"],"version":"5.9.2"}
1
+ {"root":["../src/index.ts","../src/types.ts","../src/classes/admin.ts","../src/classes/boards.ts","../src/classes/calendar.ts","../src/classes/categories.ts","../src/classes/files.ts","../src/classes/flashcards.ts","../src/classes/groups.ts","../src/classes/invites.ts","../src/classes/metrics.ts","../src/classes/permissions.ts","../src/classes/sessions.ts","../src/classes/users.ts","../src/classes/utils.ts","../src/core/manager.ts","../src/core/utils.ts","../src/external/types.ts","../src/external/vars.ts","../src/websocket/client.ts","../src/websocket/manager.ts","../src/websocket/types.ts","../src/websocket/hooks/useExecutor.ts","../src/websocket/hooks/useWebSocket.ts"],"version":"5.9.2"}
@@ -0,0 +1,33 @@
1
+ import { WebSocketMessage, WSConnectionState, ExtractOpCode, ExtractDataForOp } from './types.js';
2
+ export type HandlerFunction = (data: unknown) => void;
3
+ export declare class WSClient<TMessage extends WebSocketMessage = WebSocketMessage> {
4
+ private state;
5
+ private ws;
6
+ private reconnectAttempts;
7
+ private reconnectTimer;
8
+ private heartbeatTimer;
9
+ private handlers;
10
+ private messageQueue;
11
+ private url;
12
+ private onConnect?;
13
+ private onDisconnect?;
14
+ private onError?;
15
+ constructor(url: string, options?: {
16
+ onConnect?: () => void;
17
+ onDisconnect?: (code?: number, reason?: string) => void;
18
+ onError?: (error: string) => void;
19
+ });
20
+ connect(): Promise<void>;
21
+ disconnect(): void;
22
+ send<TOp extends ExtractOpCode<TMessage>>(op: TOp, data: ExtractDataForOp<TMessage, TOp>): boolean;
23
+ private sendInternal;
24
+ on<TOp extends ExtractOpCode<TMessage>>(op: TOp, handler: (data: ExtractDataForOp<TMessage, TOp>) => void): void;
25
+ off(op: number): void;
26
+ getState(): WSConnectionState;
27
+ private handleMessage;
28
+ private startHeartbeat;
29
+ private stopHeartbeat;
30
+ private shouldReconnect;
31
+ private scheduleReconnect;
32
+ private flushMessageQueue;
33
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WSClient = void 0;
4
+ const types_js_1 = require("./types.js");
5
+ class WSClient {
6
+ state = types_js_1.WSConnectionState.Disconnected;
7
+ ws = null;
8
+ reconnectAttempts = 0;
9
+ reconnectTimer = null;
10
+ heartbeatTimer = null;
11
+ handlers = new Map();
12
+ messageQueue = [];
13
+ url;
14
+ onConnect;
15
+ onDisconnect;
16
+ onError;
17
+ constructor(url, options = {}) {
18
+ this.url = url;
19
+ this.onConnect = options.onConnect;
20
+ this.onDisconnect = options.onDisconnect;
21
+ this.onError = options.onError;
22
+ }
23
+ async connect() {
24
+ if (this.state === types_js_1.WSConnectionState.Connecting || this.state === types_js_1.WSConnectionState.Connected) {
25
+ return;
26
+ }
27
+ this.state = types_js_1.WSConnectionState.Connecting;
28
+ return new Promise((resolve, reject) => {
29
+ try {
30
+ this.ws = new WebSocket(this.url);
31
+ this.ws.onopen = () => {
32
+ this.state = types_js_1.WSConnectionState.Connected;
33
+ this.reconnectAttempts = 0;
34
+ this.startHeartbeat();
35
+ this.flushMessageQueue();
36
+ this.onConnect?.();
37
+ resolve();
38
+ };
39
+ this.ws.onmessage = (event) => {
40
+ try {
41
+ const message = JSON.parse(event.data);
42
+ this.handleMessage(message);
43
+ }
44
+ catch (error) {
45
+ console.error('Failed to parse WebSocket message:', error);
46
+ }
47
+ };
48
+ this.ws.onclose = (event) => {
49
+ this.state = types_js_1.WSConnectionState.Disconnected;
50
+ this.stopHeartbeat();
51
+ this.onDisconnect?.(event.code, event.reason);
52
+ if (this.shouldReconnect(event.code)) {
53
+ this.scheduleReconnect();
54
+ }
55
+ };
56
+ this.ws.onerror = () => {
57
+ this.state = types_js_1.WSConnectionState.Error;
58
+ this.onError?.('WebSocket connection error');
59
+ reject(new Error('WebSocket connection failed'));
60
+ };
61
+ }
62
+ catch (error) {
63
+ this.state = types_js_1.WSConnectionState.Error;
64
+ reject(error);
65
+ }
66
+ });
67
+ }
68
+ disconnect() {
69
+ this.state = types_js_1.WSConnectionState.Disconnected;
70
+ if (this.reconnectTimer) {
71
+ clearTimeout(this.reconnectTimer);
72
+ this.reconnectTimer = null;
73
+ }
74
+ this.stopHeartbeat();
75
+ if (this.ws) {
76
+ this.ws.close(1000, 'Client disconnect');
77
+ this.ws = null;
78
+ }
79
+ this.messageQueue = [];
80
+ this.handlers.clear();
81
+ }
82
+ send(op, data) {
83
+ return this.sendInternal(op, data);
84
+ }
85
+ sendInternal(op, data) {
86
+ const message = { op, data };
87
+ if (this.state !== types_js_1.WSConnectionState.Connected || !this.ws) {
88
+ if (this.state === types_js_1.WSConnectionState.Connecting) {
89
+ this.messageQueue.push(message);
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+ try {
95
+ this.ws.send(JSON.stringify(message));
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ console.error('Failed to send WebSocket message:', error);
100
+ return false;
101
+ }
102
+ }
103
+ on(op, handler) {
104
+ this.handlers.set(op, handler);
105
+ }
106
+ off(op) {
107
+ this.handlers.delete(op);
108
+ }
109
+ getState() {
110
+ return this.state;
111
+ }
112
+ handleMessage(message) {
113
+ if (message.op === types_js_1.SpecialOPCodes.Ping) {
114
+ this.sendInternal(types_js_1.SpecialOPCodes.Pong);
115
+ return;
116
+ }
117
+ if (message.op === types_js_1.SpecialOPCodes.Pong) {
118
+ return;
119
+ }
120
+ const handler = this.handlers.get(message.op);
121
+ if (handler) {
122
+ try {
123
+ handler(message.data);
124
+ }
125
+ catch (error) {
126
+ console.error('Error in message handler:', error);
127
+ }
128
+ }
129
+ }
130
+ startHeartbeat() {
131
+ this.heartbeatTimer = setInterval(() => {
132
+ if (this.state === types_js_1.WSConnectionState.Connected) {
133
+ this.sendInternal(types_js_1.SpecialOPCodes.Ping);
134
+ }
135
+ }, 30000);
136
+ }
137
+ stopHeartbeat() {
138
+ if (this.heartbeatTimer) {
139
+ clearInterval(this.heartbeatTimer);
140
+ this.heartbeatTimer = null;
141
+ }
142
+ }
143
+ shouldReconnect(code) {
144
+ if (code === 1000 || this.reconnectAttempts >= 5)
145
+ return false;
146
+ return true;
147
+ }
148
+ scheduleReconnect() {
149
+ if (this.reconnectTimer)
150
+ return;
151
+ this.state = types_js_1.WSConnectionState.Reconnecting;
152
+ this.reconnectAttempts++;
153
+ const delay = 1000 * Math.pow(2, Math.min(this.reconnectAttempts - 1, 4));
154
+ this.reconnectTimer = setTimeout(async () => {
155
+ this.reconnectTimer = null;
156
+ try {
157
+ await this.connect();
158
+ }
159
+ catch (error) {
160
+ console.error('Reconnection failed:', error);
161
+ }
162
+ }, delay);
163
+ }
164
+ flushMessageQueue() {
165
+ while (this.messageQueue.length > 0 && this.state === types_js_1.WSConnectionState.Connected) {
166
+ const message = this.messageQueue.shift();
167
+ this.sendInternal(message.op, message.data);
168
+ }
169
+ }
170
+ }
171
+ exports.WSClient = WSClient;
@@ -0,0 +1,58 @@
1
+ export type UseExecutorOptions = {
2
+ autoConnect?: boolean;
3
+ baseUrl?: string;
4
+ onOutput?: (output: string, type: 'stdout' | 'stderr') => void;
5
+ onSessionComplete?: (exitCode: number) => void;
6
+ onError?: (error: string) => void;
7
+ };
8
+ export declare function useExecutor(options?: UseExecutorOptions): {
9
+ connectionState: import("../types").WSConnectionState;
10
+ error: string | null;
11
+ isConnected: boolean;
12
+ connect: () => Promise<import("../client").WSClient<ExecutorWSMessageUnion>>;
13
+ disconnect: () => void;
14
+ startSession: (code: string, language: string) => Promise<boolean>;
15
+ sendInput: (input: string) => boolean;
16
+ stopSession: () => boolean;
17
+ sessionId: string | null;
18
+ outputs: {
19
+ output: string;
20
+ type: "stdout" | "stderr";
21
+ timestamp: number;
22
+ }[];
23
+ isSessionActive: boolean;
24
+ };
25
+ export declare enum ExecutorWSOpCodes {
26
+ StartCodeSession = 1,
27
+ CodeSessionCreated = 2,
28
+ CodeOutput = 3,
29
+ CodeSessionError = 5,
30
+ CodeSessionCompleted = 6,
31
+ SendCodeInput = 7,
32
+ StopCodeSession = 8
33
+ }
34
+ export type ExecutorWSMessage<T extends ExecutorWSOpCodes> = {
35
+ op: T;
36
+ data: T extends ExecutorWSOpCodes.StartCodeSession ? {
37
+ code: string;
38
+ language: string;
39
+ } : T extends ExecutorWSOpCodes.CodeSessionCreated ? {
40
+ sessionId: string;
41
+ } : T extends ExecutorWSOpCodes.CodeOutput ? {
42
+ sessionId: string;
43
+ output: string;
44
+ type: 'stdout' | 'stderr';
45
+ } : T extends ExecutorWSOpCodes.CodeSessionError ? {
46
+ sessionId: string | null;
47
+ error: string;
48
+ } : T extends ExecutorWSOpCodes.CodeSessionCompleted ? {
49
+ sessionId: string;
50
+ exitCode: number;
51
+ } : T extends ExecutorWSOpCodes.SendCodeInput ? {
52
+ sessionId: string;
53
+ input: string;
54
+ } : T extends ExecutorWSOpCodes.StopCodeSession ? {
55
+ sessionId: string;
56
+ } : never;
57
+ };
58
+ export type ExecutorWSMessageUnion = ExecutorWSMessage<ExecutorWSOpCodes.StartCodeSession> | ExecutorWSMessage<ExecutorWSOpCodes.CodeSessionCreated> | ExecutorWSMessage<ExecutorWSOpCodes.CodeOutput> | ExecutorWSMessage<ExecutorWSOpCodes.CodeSessionError> | ExecutorWSMessage<ExecutorWSOpCodes.CodeSessionCompleted> | ExecutorWSMessage<ExecutorWSOpCodes.SendCodeInput> | ExecutorWSMessage<ExecutorWSOpCodes.StopCodeSession>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExecutorWSOpCodes = void 0;
4
+ exports.useExecutor = useExecutor;
5
+ const react_1 = require("react");
6
+ const useWebSocket_1 = require("./useWebSocket");
7
+ function useExecutor(options = {}) {
8
+ const [sessionId, setSessionId] = (0, react_1.useState)(null);
9
+ const [outputs, setOutputs] = (0, react_1.useState)([]);
10
+ const { connectionState, error, isConnected, connect, disconnect, send, on, } = (0, useWebSocket_1.useWebSocket)('/executor', {
11
+ autoConnect: options.autoConnect,
12
+ baseUrl: options.baseUrl,
13
+ onError: options.onError,
14
+ });
15
+ (0, react_1.useEffect)(() => {
16
+ if (!isConnected)
17
+ return;
18
+ on(ExecutorWSOpCodes.CodeSessionCreated, (data) => {
19
+ setSessionId(data.sessionId);
20
+ });
21
+ on(ExecutorWSOpCodes.CodeOutput, (data) => {
22
+ const outputEntry = {
23
+ output: data.output,
24
+ type: data.type,
25
+ timestamp: Date.now(),
26
+ };
27
+ setOutputs((prev) => [...prev, outputEntry]);
28
+ options.onOutput?.(data.output, data.type);
29
+ });
30
+ on(ExecutorWSOpCodes.CodeSessionCompleted, (data) => {
31
+ setSessionId(null);
32
+ options.onSessionComplete?.(data.exitCode);
33
+ });
34
+ on(ExecutorWSOpCodes.CodeSessionError, (data) => {
35
+ options.onError?.(data.error);
36
+ });
37
+ }, [isConnected, on, options]);
38
+ const startSession = (0, react_1.useCallback)(async (code, language) => {
39
+ if (!isConnected)
40
+ await connect();
41
+ setOutputs([]);
42
+ return send(ExecutorWSOpCodes.StartCodeSession, { code, language });
43
+ }, [isConnected, connect, send]);
44
+ const sendInput = (0, react_1.useCallback)((input) => {
45
+ if (!sessionId)
46
+ return false;
47
+ return send(ExecutorWSOpCodes.SendCodeInput, { sessionId, input });
48
+ }, [sessionId, send]);
49
+ const stopSession = (0, react_1.useCallback)(() => {
50
+ if (!sessionId)
51
+ return false;
52
+ return send(ExecutorWSOpCodes.StopCodeSession, { sessionId });
53
+ }, [sessionId, send]);
54
+ return {
55
+ connectionState,
56
+ error,
57
+ isConnected,
58
+ connect,
59
+ disconnect,
60
+ startSession,
61
+ sendInput,
62
+ stopSession,
63
+ sessionId,
64
+ outputs,
65
+ isSessionActive: sessionId !== null,
66
+ };
67
+ }
68
+ // Executor WebSocket message types and opcodes
69
+ var ExecutorWSOpCodes;
70
+ (function (ExecutorWSOpCodes) {
71
+ ExecutorWSOpCodes[ExecutorWSOpCodes["StartCodeSession"] = 1] = "StartCodeSession";
72
+ ExecutorWSOpCodes[ExecutorWSOpCodes["CodeSessionCreated"] = 2] = "CodeSessionCreated";
73
+ ExecutorWSOpCodes[ExecutorWSOpCodes["CodeOutput"] = 3] = "CodeOutput";
74
+ ExecutorWSOpCodes[ExecutorWSOpCodes["CodeSessionError"] = 5] = "CodeSessionError";
75
+ ExecutorWSOpCodes[ExecutorWSOpCodes["CodeSessionCompleted"] = 6] = "CodeSessionCompleted";
76
+ ExecutorWSOpCodes[ExecutorWSOpCodes["SendCodeInput"] = 7] = "SendCodeInput";
77
+ ExecutorWSOpCodes[ExecutorWSOpCodes["StopCodeSession"] = 8] = "StopCodeSession";
78
+ })(ExecutorWSOpCodes || (exports.ExecutorWSOpCodes = ExecutorWSOpCodes = {}));
@@ -0,0 +1,15 @@
1
+ import { WSConnectionState, WSOptions, WebSocketMessage, ExtractOpCode, ExtractDataForOp } from '../types.js';
2
+ import type { WSClient } from '../client.js';
3
+ export declare function useWebSocket<TMessage extends WebSocketMessage = WebSocketMessage>(path: string, options?: WSOptions & {
4
+ autoConnect?: boolean;
5
+ }): {
6
+ error: string | null;
7
+ connectionState: WSConnectionState;
8
+ client: WSClient<TMessage> | null;
9
+ isConnected: boolean;
10
+ isConnecting: boolean;
11
+ connect: () => Promise<WSClient<TMessage>>;
12
+ disconnect: () => void;
13
+ send: <TOp extends ExtractOpCode<TMessage>>(op: TOp, data: ExtractDataForOp<TMessage, TOp>) => boolean;
14
+ on: <TOp extends ExtractOpCode<TMessage>>(op: TOp, handler: (data: ExtractDataForOp<TMessage, TOp>) => void) => void;
15
+ };
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useWebSocket = useWebSocket;
4
+ const types_js_1 = require("../types.js");
5
+ const react_1 = require("react");
6
+ const manager_js_1 = require("../manager.js");
7
+ function useWebSocket(path, options = {}) {
8
+ const { autoConnect = true, ...wsOptions } = options;
9
+ const [connectionState, setConnectionState] = (0, react_1.useState)(types_js_1.WSConnectionState.Disconnected);
10
+ const [error, setError] = (0, react_1.useState)(null);
11
+ const clientRef = (0, react_1.useRef)(null);
12
+ const connect = (0, react_1.useCallback)(async () => {
13
+ if (clientRef.current?.getState() === types_js_1.WSConnectionState.Connected)
14
+ return clientRef.current;
15
+ try {
16
+ setConnectionState(types_js_1.WSConnectionState.Connecting);
17
+ const client = (0, manager_js_1.createConnection)(path, {
18
+ ...wsOptions,
19
+ onConnect: () => {
20
+ setConnectionState(types_js_1.WSConnectionState.Connected);
21
+ setError(null);
22
+ wsOptions.onConnect?.();
23
+ },
24
+ onDisconnect: (code, reason) => {
25
+ setConnectionState(types_js_1.WSConnectionState.Disconnected);
26
+ clientRef.current = null;
27
+ wsOptions.onDisconnect?.(code, reason);
28
+ },
29
+ onError: (err) => {
30
+ setConnectionState(types_js_1.WSConnectionState.Error);
31
+ setError(err);
32
+ wsOptions.onError?.(err);
33
+ },
34
+ });
35
+ clientRef.current = client;
36
+ await client.connect();
37
+ return client;
38
+ }
39
+ catch (err) {
40
+ setConnectionState(types_js_1.WSConnectionState.Error);
41
+ setError(err instanceof Error ? err.message : 'Connection failed');
42
+ throw err;
43
+ }
44
+ }, [path, wsOptions]);
45
+ const disconnect = (0, react_1.useCallback)(() => {
46
+ clientRef.current?.disconnect();
47
+ clientRef.current = null;
48
+ setConnectionState(types_js_1.WSConnectionState.Disconnected);
49
+ }, []);
50
+ const send = (0, react_1.useCallback)((op, data) => {
51
+ return clientRef.current?.send(op, data) ?? false;
52
+ }, []);
53
+ const on = (0, react_1.useCallback)((op, handler) => {
54
+ clientRef.current?.on(op, handler);
55
+ }, []);
56
+ (0, react_1.useEffect)(() => {
57
+ if (autoConnect)
58
+ connect().catch(console.error);
59
+ return disconnect;
60
+ }, [autoConnect, connect, disconnect]);
61
+ return {
62
+ error,
63
+ connectionState,
64
+ client: clientRef.current,
65
+ isConnected: connectionState === types_js_1.WSConnectionState.Connected,
66
+ isConnecting: connectionState === types_js_1.WSConnectionState.Connecting,
67
+ connect,
68
+ disconnect,
69
+ send,
70
+ on,
71
+ };
72
+ }
@@ -0,0 +1,5 @@
1
+ import { WSOptions, WebSocketMessage } from './types.js';
2
+ import { WSClient } from './client.js';
3
+ export declare function createConnection<TMessage extends WebSocketMessage = WebSocketMessage>(path: string, options?: WSOptions): WSClient<TMessage>;
4
+ export * from './hooks/useExecutor.js';
5
+ export * from './hooks/useWebSocket.js';
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createConnection = createConnection;
18
+ const types_js_1 = require("./types.js");
19
+ const client_js_1 = require("./client.js");
20
+ const connections = new Map();
21
+ function createWebSocketUrl(path, baseUrl) {
22
+ const base = baseUrl || (typeof window !== 'undefined' ? window.location.host : 'localhost:3000');
23
+ const protocol = base.startsWith('https') || base.includes('443') ? 'wss' : 'ws';
24
+ return `${protocol}://${base.replace(/^https?:\/\//g, '')}${path}`;
25
+ }
26
+ function createConnection(path, options = {}) {
27
+ const url = createWebSocketUrl(path, options.baseUrl);
28
+ const existing = connections.get(path);
29
+ if (existing && existing.getState() === types_js_1.WSConnectionState.Connected)
30
+ return existing;
31
+ const client = new client_js_1.WSClient(url, {
32
+ onError: options.onError,
33
+ onConnect: options.onConnect,
34
+ onDisconnect: (code, reason) => {
35
+ connections.delete(path);
36
+ options.onDisconnect?.(code, reason);
37
+ },
38
+ });
39
+ connections.set(path, client);
40
+ return client;
41
+ }
42
+ // Exports.
43
+ __exportStar(require("./hooks/useExecutor.js"), exports);
44
+ __exportStar(require("./hooks/useWebSocket.js"), exports);
@@ -0,0 +1,28 @@
1
+ export type WebSocketMessage<T = unknown> = {
2
+ op: number;
3
+ data: T;
4
+ };
5
+ export declare enum SpecialOPCodes {
6
+ Ping = -1,
7
+ Pong = -2
8
+ }
9
+ export declare enum WSConnectionState {
10
+ Disconnected = 0,
11
+ Connecting = 1,
12
+ Connected = 2,
13
+ Reconnecting = 3,
14
+ Error = 4
15
+ }
16
+ export type WSOptions = {
17
+ baseUrl?: string;
18
+ onConnect?: () => void;
19
+ onDisconnect?: (code?: number, reason?: string) => void;
20
+ onError?: (error: string) => void;
21
+ };
22
+ export type ExtractOpCode<T> = T extends {
23
+ op: infer U;
24
+ } ? U : never;
25
+ export type ExtractDataForOp<TMessage, TOp> = TMessage extends {
26
+ op: TOp;
27
+ data: infer U;
28
+ } ? U : never;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WSConnectionState = exports.SpecialOPCodes = void 0;
4
+ var SpecialOPCodes;
5
+ (function (SpecialOPCodes) {
6
+ SpecialOPCodes[SpecialOPCodes["Ping"] = -1] = "Ping";
7
+ SpecialOPCodes[SpecialOPCodes["Pong"] = -2] = "Pong";
8
+ })(SpecialOPCodes || (exports.SpecialOPCodes = SpecialOPCodes = {}));
9
+ var WSConnectionState;
10
+ (function (WSConnectionState) {
11
+ WSConnectionState[WSConnectionState["Disconnected"] = 0] = "Disconnected";
12
+ WSConnectionState[WSConnectionState["Connecting"] = 1] = "Connecting";
13
+ WSConnectionState[WSConnectionState["Connected"] = 2] = "Connected";
14
+ WSConnectionState[WSConnectionState["Reconnecting"] = 3] = "Reconnecting";
15
+ WSConnectionState[WSConnectionState["Error"] = 4] = "Error";
16
+ })(WSConnectionState || (exports.WSConnectionState = WSConnectionState = {}));
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.33",
2
+ "version": "1.1.34",
3
3
  "name": "@excali-boards/boards-api-client",
4
4
  "description": "A simple API client for the Boards API.",
5
5
  "repository": "https://github.com/Excali-Boards/boards-api-client",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "20.14.13",
35
+ "@types/react": "^19.2.8",
35
36
  "@typescript-eslint/eslint-plugin": "6.18.0",
36
37
  "@typescript-eslint/parser": "6.18.0",
37
38
  "eslint": "8.56.0",
@@ -44,7 +45,11 @@
44
45
  "@prisma/client": "6.16.2",
45
46
  "axios": "1.12.2",
46
47
  "prisma": "6.16.2",
48
+ "react": "^18.2.0",
47
49
  "ts-prisma": "1.3.3",
48
50
  "zod": "4.1.9"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
49
54
  }
50
55
  }
@@ -43,7 +43,8 @@ model Board {
43
43
  categoryId String
44
44
  category Category @relation(fields: [categoryId], references: [categoryId], onDelete: Cascade)
45
45
 
46
- flashcardDeck FlashcardDeck?
46
+ flashcardDeck FlashcardDeck?
47
+ codeSnippetCollection CodeSnippetCollection?
47
48
 
48
49
  files File[]
49
50
  boardPermission BoardPermission[]
@@ -0,0 +1,33 @@
1
+ model CodeSnippetCollection {
2
+ dbId String @id @default(uuid())
3
+ collectionId String @unique
4
+
5
+ createdAt DateTime @default(now())
6
+ updatedAt DateTime @updatedAt
7
+
8
+ boardId String @unique
9
+ board Board @relation(fields: [boardId], references: [boardId], onDelete: Cascade)
10
+
11
+ snippets CodeSnippet[]
12
+
13
+ @@index([boardId])
14
+ }
15
+
16
+ model CodeSnippet {
17
+ dbId String @id @default(uuid())
18
+ snippetId String @unique
19
+
20
+ title String
21
+ description String @default("")
22
+ code String @db.Text
23
+ language String @default("cpp")
24
+ index Int
25
+
26
+ createdAt DateTime @default(now())
27
+ updatedAt DateTime @updatedAt
28
+
29
+ collectionId String
30
+ collection CodeSnippetCollection @relation(fields: [collectionId], references: [collectionId], onDelete: Cascade)
31
+
32
+ @@index([collectionId])
33
+ }