@soederpop/luca 0.0.6 → 0.0.7
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/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import axios, { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from "axios";
|
|
2
|
+
import { Client, type ClientOptions, type ClientState } from '../client.js'
|
|
3
|
+
import type { ContainerContext } from '../container.js'
|
|
4
|
+
import { ClientEventsSchema } from '../schemas/base.js'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const RestClientEventsSchema = ClientEventsSchema.extend({}).describe('REST client events')
|
|
8
|
+
|
|
9
|
+
declare module '../client' {
|
|
10
|
+
interface AvailableClients {
|
|
11
|
+
rest: typeof RestClient
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTTP REST client built on top of axios. Provides convenience methods for
|
|
17
|
+
* GET, POST, PUT, PATCH, and DELETE requests with automatic JSON handling,
|
|
18
|
+
* configurable base URL, and error event emission.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const api = container.client('rest', { baseURL: 'https://api.example.com', json: true })
|
|
23
|
+
* const users = await api.get('/users')
|
|
24
|
+
* await api.post('/users', { name: 'Alice' })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class RestClient<
|
|
28
|
+
T extends ClientState = ClientState,
|
|
29
|
+
K extends ClientOptions = ClientOptions
|
|
30
|
+
> extends Client<T, K> {
|
|
31
|
+
axios!: AxiosInstance;
|
|
32
|
+
|
|
33
|
+
static override shortcut: string = "clients.rest"
|
|
34
|
+
static override eventsSchema = RestClientEventsSchema
|
|
35
|
+
static { Client.register(this, 'rest') }
|
|
36
|
+
|
|
37
|
+
constructor(options: K, context: ContainerContext) {
|
|
38
|
+
super(options, context);
|
|
39
|
+
|
|
40
|
+
this.axios = axios.create({
|
|
41
|
+
baseURL: this.baseURL,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (this.useJSON) {
|
|
45
|
+
this.axios.defaults.headers.common = {
|
|
46
|
+
...this.axios.defaults.headers.common,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Accept: "application/json",
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async beforeRequest() {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether JSON content-type headers should be set automatically. */
|
|
57
|
+
get useJSON() {
|
|
58
|
+
return !!this.options.json
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override get baseURL() {
|
|
62
|
+
return this.options.baseURL || '/'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Send a PATCH request. Returns the parsed response body directly (not an
|
|
67
|
+
* axios Response wrapper). On HTTP errors, returns the error as JSON instead
|
|
68
|
+
* of throwing.
|
|
69
|
+
* @param url - Request path relative to baseURL
|
|
70
|
+
* @param data - Request body
|
|
71
|
+
* @param options - Additional axios request config
|
|
72
|
+
* @returns Parsed response body
|
|
73
|
+
*/
|
|
74
|
+
async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
75
|
+
await this.beforeRequest();
|
|
76
|
+
return this.axios({
|
|
77
|
+
...options,
|
|
78
|
+
method: "PATCH",
|
|
79
|
+
url,
|
|
80
|
+
data,
|
|
81
|
+
})
|
|
82
|
+
.then((r) => r.data)
|
|
83
|
+
.catch((e: any) => {
|
|
84
|
+
if (e.isAxiosError) {
|
|
85
|
+
return this.handleError(e);
|
|
86
|
+
} else {
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Send a PUT request. Returns the parsed response body directly (not an
|
|
94
|
+
* axios Response wrapper). On HTTP errors, returns the error as JSON instead
|
|
95
|
+
* of throwing.
|
|
96
|
+
* @param url - Request path relative to baseURL
|
|
97
|
+
* @param data - Request body
|
|
98
|
+
* @param options - Additional axios request config
|
|
99
|
+
* @returns Parsed response body
|
|
100
|
+
*/
|
|
101
|
+
async put(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
102
|
+
await this.beforeRequest();
|
|
103
|
+
return this.axios({
|
|
104
|
+
...options,
|
|
105
|
+
method: "PUT",
|
|
106
|
+
url,
|
|
107
|
+
data,
|
|
108
|
+
})
|
|
109
|
+
.then((r) => r.data)
|
|
110
|
+
.catch((e: any) => {
|
|
111
|
+
if (e.isAxiosError) {
|
|
112
|
+
return this.handleError(e);
|
|
113
|
+
} else {
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Send a POST request. Returns the parsed response body directly (not an
|
|
121
|
+
* axios Response wrapper). On HTTP errors, returns the error as JSON instead
|
|
122
|
+
* of throwing.
|
|
123
|
+
* @param url - Request path relative to baseURL
|
|
124
|
+
* @param data - Request body
|
|
125
|
+
* @param options - Additional axios request config
|
|
126
|
+
* @returns Parsed response body
|
|
127
|
+
*/
|
|
128
|
+
async post(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
129
|
+
await this.beforeRequest();
|
|
130
|
+
return this.axios({
|
|
131
|
+
...options,
|
|
132
|
+
method: "POST",
|
|
133
|
+
url,
|
|
134
|
+
data,
|
|
135
|
+
})
|
|
136
|
+
.then((r) => r.data)
|
|
137
|
+
.catch((e: any) => {
|
|
138
|
+
if (e.isAxiosError) {
|
|
139
|
+
return this.handleError(e);
|
|
140
|
+
} else {
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Send a DELETE request. Returns the parsed response body directly (not an
|
|
148
|
+
* axios Response wrapper). On HTTP errors, returns the error as JSON instead
|
|
149
|
+
* of throwing.
|
|
150
|
+
* @param url - Request path relative to baseURL
|
|
151
|
+
* @param params - Query parameters
|
|
152
|
+
* @param options - Additional axios request config
|
|
153
|
+
* @returns Parsed response body
|
|
154
|
+
*/
|
|
155
|
+
async delete(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
|
|
156
|
+
await this.beforeRequest();
|
|
157
|
+
return this.axios({
|
|
158
|
+
...options,
|
|
159
|
+
method: "DELETE",
|
|
160
|
+
url,
|
|
161
|
+
params,
|
|
162
|
+
})
|
|
163
|
+
.then((r) => r.data)
|
|
164
|
+
.catch((e: any) => {
|
|
165
|
+
if (e.isAxiosError) {
|
|
166
|
+
return this.handleError(e);
|
|
167
|
+
} else {
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Send a GET request. Returns the parsed response body directly (not an
|
|
175
|
+
* axios Response wrapper). On HTTP errors, returns the error as JSON instead
|
|
176
|
+
* of throwing.
|
|
177
|
+
* @param url - Request path relative to baseURL
|
|
178
|
+
* @param params - Query parameters
|
|
179
|
+
* @param options - Additional axios request config
|
|
180
|
+
* @returns Parsed response body
|
|
181
|
+
*/
|
|
182
|
+
async get(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
|
|
183
|
+
await this.beforeRequest()
|
|
184
|
+
return this.axios({
|
|
185
|
+
...options,
|
|
186
|
+
method: "GET",
|
|
187
|
+
url,
|
|
188
|
+
params,
|
|
189
|
+
})
|
|
190
|
+
.then((r) => r.data)
|
|
191
|
+
.catch((e: any) => {
|
|
192
|
+
if (e.isAxiosError) {
|
|
193
|
+
return this.handleError(e);
|
|
194
|
+
} else {
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Handle an axios error by emitting 'failure' and returning the error as JSON. */
|
|
201
|
+
async handleError(error: AxiosError) {
|
|
202
|
+
this.emit('failure', error)
|
|
203
|
+
return error.toJSON();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default RestClient
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { Client } from '../client.js'
|
|
3
|
+
import type { ContainerContext } from '../container.js'
|
|
4
|
+
import {
|
|
5
|
+
WebSocketClientStateSchema, WebSocketClientOptionsSchema, WebSocketClientEventsSchema,
|
|
6
|
+
} from '../schemas/base.js'
|
|
7
|
+
|
|
8
|
+
export type WebSocketClientState = z.infer<typeof WebSocketClientStateSchema>
|
|
9
|
+
export type WebSocketClientOptions = z.infer<typeof WebSocketClientOptionsSchema>
|
|
10
|
+
|
|
11
|
+
declare module '../client' {
|
|
12
|
+
interface AvailableClients {
|
|
13
|
+
websocket: typeof WebSocketClient
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WebSocket client that bridges raw WebSocket events to Luca's Helper event bus,
|
|
19
|
+
* providing a clean interface for sending/receiving messages, tracking connection
|
|
20
|
+
* state, and optional auto-reconnection with exponential backoff.
|
|
21
|
+
*
|
|
22
|
+
* Events emitted:
|
|
23
|
+
* - `open` — connection established
|
|
24
|
+
* - `message` — message received (JSON-parsed when possible)
|
|
25
|
+
* - `close` — connection closed (with code and reason)
|
|
26
|
+
* - `error` — connection error
|
|
27
|
+
* - `reconnecting` — attempting reconnection (with attempt number)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const ws = container.client('websocket', {
|
|
32
|
+
* baseURL: 'ws://localhost:8080',
|
|
33
|
+
* reconnect: true,
|
|
34
|
+
* maxReconnectAttempts: 5
|
|
35
|
+
* })
|
|
36
|
+
* ws.on('message', (data) => console.log('Received:', data))
|
|
37
|
+
* await ws.connect()
|
|
38
|
+
* await ws.send({ type: 'hello' })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class WebSocketClient<
|
|
42
|
+
T extends WebSocketClientState = WebSocketClientState,
|
|
43
|
+
K extends WebSocketClientOptions = WebSocketClientOptions
|
|
44
|
+
> extends Client<T, K> {
|
|
45
|
+
ws!: WebSocket
|
|
46
|
+
_intentionalClose: boolean
|
|
47
|
+
|
|
48
|
+
static override shortcut = "clients.websocket" as const
|
|
49
|
+
static override stateSchema = WebSocketClientStateSchema
|
|
50
|
+
static override optionsSchema = WebSocketClientOptionsSchema
|
|
51
|
+
static override eventsSchema = WebSocketClientEventsSchema
|
|
52
|
+
static { Client.register(this, 'websocket') }
|
|
53
|
+
|
|
54
|
+
constructor(options?: K, context?: ContainerContext) {
|
|
55
|
+
super(options, context)
|
|
56
|
+
this._intentionalClose = false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override get initialState(): T {
|
|
60
|
+
return {
|
|
61
|
+
connected: false,
|
|
62
|
+
reconnectAttempts: 0,
|
|
63
|
+
} as T
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Establish a WebSocket connection to the configured baseURL.
|
|
68
|
+
* Wires all raw WebSocket events (open, message, close, error) to the
|
|
69
|
+
* Helper event bus and updates connection state accordingly.
|
|
70
|
+
* Resolves once the connection is open; rejects on error.
|
|
71
|
+
*/
|
|
72
|
+
override async connect(): Promise<this> {
|
|
73
|
+
if (this.isConnected) {
|
|
74
|
+
return this
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ws = this.ws = new WebSocket(this.baseURL)
|
|
78
|
+
const state = this.state as any
|
|
79
|
+
|
|
80
|
+
await new Promise<void>((resolve, reject) => {
|
|
81
|
+
ws.onopen = () => {
|
|
82
|
+
state.set('connected', true)
|
|
83
|
+
state.set('connectionError', undefined)
|
|
84
|
+
state.set('reconnectAttempts', 0)
|
|
85
|
+
this.emit('open')
|
|
86
|
+
resolve()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ws.onerror = (event: any) => {
|
|
90
|
+
state.set('connectionError', event)
|
|
91
|
+
this.emit('error', event)
|
|
92
|
+
reject(event)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ws.onmessage = (event: any) => {
|
|
96
|
+
let data = event?.data ?? event
|
|
97
|
+
try {
|
|
98
|
+
data = JSON.parse(data)
|
|
99
|
+
} catch {}
|
|
100
|
+
this.emit('message', data)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ws.onclose = (event: any) => {
|
|
104
|
+
state.set('connected', false)
|
|
105
|
+
this.emit('close', event?.code, event?.reason)
|
|
106
|
+
if (!this._intentionalClose) {
|
|
107
|
+
this.maybeReconnect()
|
|
108
|
+
}
|
|
109
|
+
this._intentionalClose = false
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send data over the WebSocket connection. Automatically JSON-serializes
|
|
118
|
+
* the payload. If not currently connected, attempts to connect first.
|
|
119
|
+
* @param data - The data to send (will be JSON.stringify'd)
|
|
120
|
+
*/
|
|
121
|
+
async send(data: any): Promise<void> {
|
|
122
|
+
if (!this.isConnected) {
|
|
123
|
+
await this.connect()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!this.ws) {
|
|
127
|
+
throw new Error('WebSocket instance not available')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.ws.send(JSON.stringify(data))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Gracefully close the WebSocket connection. Suppresses auto-reconnect
|
|
135
|
+
* and updates connection state to disconnected.
|
|
136
|
+
*/
|
|
137
|
+
async disconnect(): Promise<this> {
|
|
138
|
+
this._intentionalClose = true
|
|
139
|
+
if (this.ws) {
|
|
140
|
+
this.ws.close()
|
|
141
|
+
}
|
|
142
|
+
;(this.state as any).set('connected', false)
|
|
143
|
+
return this
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether the client is in an error state. */
|
|
147
|
+
get hasError() {
|
|
148
|
+
return !!(this.state as any).get('connectionError')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Attempt to reconnect if the reconnect option is enabled and we haven't
|
|
153
|
+
* exceeded maxReconnectAttempts. Uses exponential backoff capped at 30s.
|
|
154
|
+
*/
|
|
155
|
+
private maybeReconnect() {
|
|
156
|
+
const opts = this.options as WebSocketClientOptions
|
|
157
|
+
if (!opts.reconnect) return
|
|
158
|
+
|
|
159
|
+
const state = this.state as any
|
|
160
|
+
const maxAttempts = opts.maxReconnectAttempts ?? Infinity
|
|
161
|
+
const baseInterval = opts.reconnectInterval ?? 1000
|
|
162
|
+
const attempts = ((state.get('reconnectAttempts') as number) ?? 0) + 1
|
|
163
|
+
|
|
164
|
+
if (attempts > maxAttempts) return
|
|
165
|
+
|
|
166
|
+
state.set('reconnectAttempts', attempts)
|
|
167
|
+
this.emit('reconnecting', attempts)
|
|
168
|
+
|
|
169
|
+
const delay = Math.min(baseInterval * Math.pow(2, attempts - 1), 30000)
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
this.connect().catch(() => {})
|
|
172
|
+
}, delay)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default WebSocketClient
|