dreaction-client-core 1.2.1 → 1.3.0

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.
Files changed (48) hide show
  1. package/lib/core.d.ts +30 -0
  2. package/lib/core.d.ts.map +1 -0
  3. package/lib/core.js +317 -0
  4. package/lib/index.d.ts +14 -194
  5. package/lib/index.d.ts.map +1 -1
  6. package/lib/index.js +20 -461
  7. package/lib/plugins/api-response.d.ts +1 -1
  8. package/lib/plugins/api-response.d.ts.map +1 -1
  9. package/lib/plugins/benchmark.d.ts +1 -1
  10. package/lib/plugins/benchmark.d.ts.map +1 -1
  11. package/lib/plugins/clear.d.ts +1 -1
  12. package/lib/plugins/clear.d.ts.map +1 -1
  13. package/lib/plugins/image.d.ts +1 -1
  14. package/lib/plugins/image.d.ts.map +1 -1
  15. package/lib/plugins/index.d.ts +53 -0
  16. package/lib/plugins/index.d.ts.map +1 -0
  17. package/lib/plugins/index.js +36 -0
  18. package/lib/plugins/issue.d.ts +19 -0
  19. package/lib/plugins/issue.d.ts.map +1 -0
  20. package/lib/plugins/issue.js +20 -0
  21. package/lib/plugins/logger.d.ts +21 -5
  22. package/lib/plugins/logger.d.ts.map +1 -1
  23. package/lib/plugins/logger.js +11 -23
  24. package/lib/plugins/repl.d.ts +1 -1
  25. package/lib/plugins/repl.d.ts.map +1 -1
  26. package/lib/plugins/state-responses.d.ts +17 -5
  27. package/lib/plugins/state-responses.d.ts.map +1 -1
  28. package/lib/plugins/state-responses.js +10 -19
  29. package/lib/types.d.ts +73 -0
  30. package/lib/types.d.ts.map +1 -0
  31. package/lib/types.js +3 -0
  32. package/lib/utils/plugin-guard.d.ts +12 -0
  33. package/lib/utils/plugin-guard.d.ts.map +1 -0
  34. package/lib/utils/plugin-guard.js +19 -0
  35. package/package.json +2 -2
  36. package/src/core.ts +428 -0
  37. package/src/index.ts +36 -720
  38. package/src/plugins/api-response.ts +1 -1
  39. package/src/plugins/benchmark.ts +1 -1
  40. package/src/plugins/clear.ts +1 -1
  41. package/src/plugins/image.ts +1 -1
  42. package/src/plugins/index.ts +26 -0
  43. package/src/plugins/issue.ts +25 -0
  44. package/src/plugins/logger.ts +19 -35
  45. package/src/plugins/state-responses.ts +19 -29
  46. package/src/types.ts +127 -0
  47. package/src/utils/plugin-guard.ts +31 -0
  48. package/src/plugins/repl.ts +0 -63
package/src/core.ts ADDED
@@ -0,0 +1,428 @@
1
+ import WebSocket from 'ws';
2
+ import type {
3
+ CommandMap,
4
+ CommandTypeKey,
5
+ CustomCommandArg,
6
+ } from 'dreaction-protocol';
7
+ import type { ClientOptions } from './client-options';
8
+ import type {
9
+ CustomCommand,
10
+ DisplayConfig,
11
+ DReactionCore,
12
+ InferFeatures,
13
+ Plugin,
14
+ PluginCreator,
15
+ } from './types';
16
+ import validate from './validate';
17
+ import serialize from './serialize';
18
+ import { start } from './stopwatch';
19
+ import { corePlugins } from './plugins';
20
+
21
+ const RESERVED_FEATURES = [
22
+ 'configure',
23
+ 'connect',
24
+ 'connected',
25
+ 'options',
26
+ 'plugins',
27
+ 'send',
28
+ 'socket',
29
+ 'startTimer',
30
+ 'use',
31
+ ] as const;
32
+
33
+ type ReservedKeys = (typeof RESERVED_FEATURES)[number];
34
+
35
+ const isReservedFeature = (value: string): value is ReservedKeys =>
36
+ RESERVED_FEATURES.includes(value as ReservedKeys);
37
+
38
+ function emptyPromise() {
39
+ return Promise.resolve('');
40
+ }
41
+
42
+ export class DReactionImpl
43
+ implements
44
+ Omit<
45
+ DReactionCore,
46
+ 'options' | 'plugins' | 'configure' | 'connect' | 'use' | 'close'
47
+ >
48
+ {
49
+ options!: ClientOptions<DReactionCore>;
50
+ connected = false;
51
+ socket: WebSocket = null as never;
52
+ plugins: Plugin<this>[] = [];
53
+ sendQueue: string[] = [];
54
+ isReady = false;
55
+ lastMessageDate = new Date();
56
+ customCommands: CustomCommand[] = [];
57
+ customCommandLatestId = 1;
58
+
59
+ private connectPromiseResolve: (() => void) | null = null;
60
+ private connectPromiseReject: ((error: Error) => void) | null = null;
61
+ private connectPromise: Promise<void> | null = null;
62
+
63
+ startTimer = () => start();
64
+
65
+ configure(
66
+ options: ClientOptions<this> = {}
67
+ ): ClientOptions<this>['plugins'] extends PluginCreator<this>[]
68
+ ? this & InferFeatures<ClientOptions<this>['plugins']>
69
+ : this {
70
+ const newOptions = {
71
+ createSocket: null as never,
72
+ host: 'localhost',
73
+ port: 9600,
74
+ name: 'dreaction-core-client',
75
+ secure: false,
76
+ plugins: corePlugins as any,
77
+ safeRecursion: true,
78
+ onCommand: () => null,
79
+ onConnect: () => null,
80
+ onDisconnect: () => null,
81
+ ...this.options,
82
+ ...options,
83
+ } satisfies ClientOptions<DReactionCore>;
84
+
85
+ validate(newOptions);
86
+ this.options = newOptions;
87
+
88
+ if (Array.isArray(this.options.plugins)) {
89
+ this.options.plugins.forEach((p) => this.use(p as never));
90
+ }
91
+
92
+ return this as this &
93
+ InferFeatures<Exclude<ClientOptions<this>['plugins'], undefined>>;
94
+ }
95
+
96
+ close() {
97
+ this.connected = false;
98
+ this.socket?.close?.();
99
+
100
+ if (this.connectPromiseReject) {
101
+ this.connectPromiseReject(new Error('Connection closed'));
102
+ this.connectPromiseResolve = null;
103
+ this.connectPromiseReject = null;
104
+ }
105
+
106
+ return this;
107
+ }
108
+
109
+ connect() {
110
+ this.connected = true;
111
+
112
+ this.connectPromise = new Promise<void>((resolve, reject) => {
113
+ this.connectPromiseResolve = resolve;
114
+ this.connectPromiseReject = reject;
115
+ });
116
+
117
+ const {
118
+ createSocket,
119
+ secure,
120
+ host,
121
+ environment,
122
+ port,
123
+ name,
124
+ client = {},
125
+ info = {},
126
+ getClientId,
127
+ onCommand,
128
+ onConnect,
129
+ onDisconnect,
130
+ } = this.options;
131
+
132
+ if (!host) {
133
+ console.log('host is not config, skip connect.');
134
+ if (this.connectPromiseReject) {
135
+ this.connectPromiseReject(new Error('Host is not configured'));
136
+ this.connectPromiseResolve = null;
137
+ this.connectPromiseReject = null;
138
+ }
139
+ return this;
140
+ }
141
+
142
+ const protocol = secure ? 'wss' : 'ws';
143
+ const socket = createSocket!(`${protocol}://${host}:${port}`);
144
+
145
+ const onOpen = () => {
146
+ onConnect?.();
147
+ this.plugins.forEach((p) => p.onConnect?.());
148
+
149
+ if (this.connectPromiseResolve) {
150
+ this.connectPromiseResolve();
151
+ this.connectPromiseResolve = null;
152
+ this.connectPromiseReject = null;
153
+ }
154
+
155
+ const getClientIdPromise = getClientId || emptyPromise;
156
+ getClientIdPromise(name!).then((clientId) => {
157
+ this.isReady = true;
158
+ this.send('client.intro', {
159
+ environment,
160
+ ...client,
161
+ ...info,
162
+ name,
163
+ clientId,
164
+ dreactionCoreClientVersion: 'DREACTION_CORE_CLIENT_VERSION',
165
+ });
166
+
167
+ while (this.sendQueue.length > 0) {
168
+ const h = this.sendQueue.shift()!;
169
+ this.socket.send(h);
170
+ }
171
+ });
172
+ };
173
+
174
+ const onClose = () => {
175
+ this.isReady = false;
176
+
177
+ if (this.connectPromiseReject) {
178
+ this.connectPromiseReject(new Error('Connection failed or closed'));
179
+ this.connectPromiseResolve = null;
180
+ this.connectPromiseReject = null;
181
+ }
182
+
183
+ onDisconnect?.();
184
+ this.plugins.forEach((p) => p.onDisconnect?.());
185
+ };
186
+
187
+ const decodeCommandData = (data: unknown) => {
188
+ if (typeof data === 'string') return JSON.parse(data);
189
+ if (Buffer.isBuffer(data)) return JSON.parse(data.toString());
190
+ return data;
191
+ };
192
+
193
+ const onMessage = (data: any) => {
194
+ const command = decodeCommandData(data);
195
+ onCommand?.(command);
196
+ this.plugins.forEach((p) => p.onCommand?.(command));
197
+
198
+ if (command.type === 'custom') {
199
+ this.customCommands
200
+ .filter((cc) =>
201
+ typeof command.payload === 'string'
202
+ ? cc.command === command.payload
203
+ : cc.command === command.payload.command
204
+ )
205
+ .forEach(async (cc) => {
206
+ const res = await cc.handler(
207
+ typeof command.payload === 'object' ? command.payload.args : {}
208
+ );
209
+ if (res) {
210
+ this.send('customCommand.response', {
211
+ command: cc.command,
212
+ payload: res,
213
+ });
214
+ }
215
+ });
216
+ } else if (command.type === 'setClientId') {
217
+ this.options.setClientId?.(command.payload);
218
+ }
219
+ };
220
+
221
+ if ('on' in socket && typeof socket.on === 'function') {
222
+ const nodeWebSocket = socket as WebSocket;
223
+ nodeWebSocket.on('open', onOpen);
224
+ nodeWebSocket.on('close', onClose);
225
+ nodeWebSocket.on('message', onMessage);
226
+ this.socket = socket;
227
+ } else {
228
+ const browserWebSocket = socket as WebSocket;
229
+ socket.onopen = onOpen;
230
+ socket.onclose = onClose;
231
+ socket.onmessage = (evt: WebSocket.MessageEvent) => onMessage(evt.data);
232
+ this.socket = browserWebSocket;
233
+ }
234
+
235
+ return this;
236
+ }
237
+
238
+ send = <Type extends CommandTypeKey>(
239
+ type: Type,
240
+ payload?: CommandMap[Type]['payload'],
241
+ important?: boolean
242
+ ) => {
243
+ const date = new Date();
244
+ let deltaTime = date.getTime() - this.lastMessageDate.getTime();
245
+ if (deltaTime < 0) deltaTime = 0;
246
+ this.lastMessageDate = date;
247
+
248
+ const fullMessage = {
249
+ type,
250
+ payload,
251
+ important: !!important,
252
+ date: date.toISOString(),
253
+ deltaTime,
254
+ };
255
+
256
+ const serializedMessage = serialize(fullMessage, this.options.proxyHack);
257
+
258
+ if (this.isReady) {
259
+ try {
260
+ this.socket.send(serializedMessage);
261
+ } catch {
262
+ this.isReady = false;
263
+ console.log(
264
+ 'An error occurred communicating with dreaction. Please reload your app'
265
+ );
266
+ }
267
+ } else {
268
+ this.sendQueue.push(serializedMessage);
269
+ }
270
+ };
271
+
272
+ display(config: DisplayConfig) {
273
+ const { name, value, preview, image: img, important = false } = config;
274
+ this.send(
275
+ 'display',
276
+ {
277
+ name,
278
+ value: value || null,
279
+ preview: preview || null,
280
+ image: img || null,
281
+ },
282
+ important
283
+ );
284
+ }
285
+
286
+ reportError(this: any, error: Error) {
287
+ this.error(error);
288
+ }
289
+
290
+ use<P extends PluginCreator<this>>(
291
+ pluginCreator: P
292
+ ): this & InferFeatures<P> {
293
+ if (typeof pluginCreator !== 'function') {
294
+ throw new Error('plugins must be a function');
295
+ }
296
+
297
+ const plugin = pluginCreator.bind(this)(this) as ReturnType<P>;
298
+
299
+ if (typeof plugin !== 'object') {
300
+ throw new Error('plugins must return an object');
301
+ }
302
+
303
+ if (plugin.features) {
304
+ if (typeof plugin.features !== 'object') {
305
+ throw new Error('features must be an object');
306
+ }
307
+
308
+ Object.keys(plugin.features).forEach((key) => {
309
+ const featureFunction = plugin.features![key];
310
+
311
+ if (typeof featureFunction !== 'function') {
312
+ throw new Error(`feature ${key} is not a function`);
313
+ }
314
+
315
+ if (isReservedFeature(key)) {
316
+ throw new Error(`feature ${key} is a reserved name`);
317
+ }
318
+
319
+ (this as any)[key] = featureFunction;
320
+ });
321
+ }
322
+
323
+ this.plugins.push(plugin);
324
+ plugin.onPlugin?.bind(this)(this);
325
+
326
+ return this as this & InferFeatures<P>;
327
+ }
328
+
329
+ registerCustomCommand(
330
+ config: CustomCommand,
331
+ optHandler?: () => void
332
+ ): () => void {
333
+ let command: string;
334
+ let handler: (args: Record<string, any>) => void;
335
+ let title!: string;
336
+ let description!: string;
337
+ let args!: CustomCommandArg[];
338
+
339
+ if (typeof config === 'string') {
340
+ command = config;
341
+ handler = optHandler!;
342
+ } else {
343
+ command = config.command;
344
+ handler = config.handler;
345
+ title = config.title!;
346
+ description = config.description!;
347
+ args = config.args!;
348
+ }
349
+
350
+ if (!command) throw new Error('A command is required');
351
+ if (!handler)
352
+ throw new Error(`A handler is required for command "${command}"`);
353
+
354
+ const existingCommands = this.customCommands.filter(
355
+ (cc) => cc.command === command
356
+ );
357
+ existingCommands.forEach((cmd) => {
358
+ this.customCommands = this.customCommands.filter(
359
+ (cc) => cc.id !== cmd.id
360
+ );
361
+ this.send('customCommand.unregister', {
362
+ id: cmd.id,
363
+ command: cmd.command,
364
+ });
365
+ });
366
+
367
+ if (args) {
368
+ const argNames: string[] = [];
369
+ args.forEach((arg) => {
370
+ if (!arg.name) {
371
+ throw new Error(
372
+ `A arg on the command "${command}" is missing a name`
373
+ );
374
+ }
375
+ if (argNames.includes(arg.name)) {
376
+ throw new Error(
377
+ `A arg with the name "${arg.name}" already exists in the command "${command}"`
378
+ );
379
+ }
380
+ argNames.push(arg.name);
381
+ });
382
+ }
383
+
384
+ const customHandler: CustomCommand = {
385
+ id: this.customCommandLatestId++,
386
+ command,
387
+ handler,
388
+ title,
389
+ description,
390
+ args,
391
+ responseViewType: config.responseViewType,
392
+ };
393
+
394
+ this.customCommands.push(customHandler);
395
+
396
+ this.send('customCommand.register', {
397
+ id: customHandler.id,
398
+ command: customHandler.command,
399
+ title: customHandler.title,
400
+ description: customHandler.description,
401
+ args: customHandler.args,
402
+ responseViewType: customHandler.responseViewType,
403
+ });
404
+
405
+ return () => {
406
+ this.customCommands = this.customCommands.filter(
407
+ (cc) => cc.id !== customHandler.id
408
+ );
409
+ this.send('customCommand.unregister', {
410
+ id: customHandler.id,
411
+ command: customHandler.command,
412
+ });
413
+ };
414
+ }
415
+
416
+ waitForConnect(): Promise<void> {
417
+ if (this.isReady) return Promise.resolve();
418
+ if (this.connectPromise) return this.connectPromise;
419
+ return Promise.reject(new Error('Not connected. Call connect() first.'));
420
+ }
421
+ }
422
+
423
+ export function createClient<Client extends DReactionCore = DReactionCore>(
424
+ options?: ClientOptions<Client>
425
+ ) {
426
+ const client = new DReactionImpl();
427
+ return client.configure(options as never) as unknown as Client;
428
+ }