@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 +28 -57
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/websocket/client.d.ts +33 -0
- package/dist/websocket/client.js +171 -0
- package/dist/websocket/hooks/useExecutor.d.ts +58 -0
- package/dist/websocket/hooks/useExecutor.js +78 -0
- package/dist/websocket/hooks/useWebSocket.d.ts +15 -0
- package/dist/websocket/hooks/useWebSocket.js +72 -0
- package/dist/websocket/manager.d.ts +5 -0
- package/dist/websocket/manager.js +44 -0
- package/dist/websocket/types.d.ts +28 -0
- package/dist/websocket/types.js +16 -0
- package/package.json +6 -1
- package/prisma/schema/boards.prisma +2 -1
- package/prisma/schema/codesnippets.prisma +33 -0
package/README.md
CHANGED
|
@@ -1,88 +1,59 @@
|
|
|
1
1
|
# ๐งฐ boards-api-client
|
|
2
2
|
|
|
3
|
-
A TypeScript client library for
|
|
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
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
pnpm add @excali-boards/boards-api-client
|
|
26
18
|
# or
|
|
27
|
-
|
|
19
|
+
npm install @excali-boards/boards-api-client
|
|
28
20
|
```
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
## Quick start
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Initialize the client
|
|
24
|
+
### REST
|
|
35
25
|
|
|
36
26
|
```ts
|
|
37
|
-
import { BoardsManager } from
|
|
27
|
+
import { BoardsManager } from "@excali-boards/boards-api-client";
|
|
38
28
|
|
|
39
|
-
const
|
|
29
|
+
const api = new BoardsManager("https://api.example.com");
|
|
30
|
+
const boards = await api.boards.list();
|
|
40
31
|
```
|
|
41
32
|
|
|
42
|
-
###
|
|
33
|
+
### WebSocket (generic)
|
|
43
34
|
|
|
44
35
|
```ts
|
|
45
|
-
|
|
36
|
+
import { createConnection } from "@excali-boards/boards-api-client";
|
|
46
37
|
|
|
47
|
-
const
|
|
48
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
46
|
+
```tsx
|
|
47
|
+
import { useExecutor } from "@excali-boards/boards-api-client";
|
|
63
48
|
|
|
64
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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
|
|
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
|
+
}
|