dreaction-client-core 1.0.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.
- package/lib/client-options.d.ts +77 -0
- package/lib/client-options.d.ts.map +1 -0
- package/lib/client-options.js +2 -0
- package/lib/index.d.ts +190 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +401 -0
- package/lib/plugins/api-response.d.ts +13 -0
- package/lib/plugins/api-response.d.ts.map +1 -0
- package/lib/plugins/api-response.js +23 -0
- package/lib/plugins/benchmark.d.ts +15 -0
- package/lib/plugins/benchmark.d.ts.map +1 -0
- package/lib/plugins/benchmark.js +31 -0
- package/lib/plugins/clear.d.ts +11 -0
- package/lib/plugins/clear.d.ts.map +1 -0
- package/lib/plugins/clear.js +13 -0
- package/lib/plugins/image.d.ts +19 -0
- package/lib/plugins/image.d.ts.map +1 -0
- package/lib/plugins/image.js +24 -0
- package/lib/plugins/logger.d.ts +18 -0
- package/lib/plugins/logger.d.ts.map +1 -0
- package/lib/plugins/logger.js +44 -0
- package/lib/plugins/repl.d.ts +10 -0
- package/lib/plugins/repl.d.ts.map +1 -0
- package/lib/plugins/repl.js +55 -0
- package/lib/plugins/state-responses.d.ts +20 -0
- package/lib/plugins/state-responses.d.ts.map +1 -0
- package/lib/plugins/state-responses.js +38 -0
- package/lib/reactotron-core-client.d.ts +191 -0
- package/lib/reactotron-core-client.d.ts.map +1 -0
- package/lib/reactotron-core-client.js +400 -0
- package/lib/serialize.d.ts +20 -0
- package/lib/serialize.d.ts.map +1 -0
- package/lib/serialize.js +112 -0
- package/lib/stopwatch.d.ts +6 -0
- package/lib/stopwatch.d.ts.map +1 -0
- package/lib/stopwatch.js +45 -0
- package/lib/validate.d.ts +9 -0
- package/lib/validate.d.ts.map +1 -0
- package/lib/validate.js +26 -0
- package/package.json +29 -0
- package/src/client-options.ts +95 -0
- package/src/index.ts +654 -0
- package/src/plugins/api-response.ts +32 -0
- package/src/plugins/benchmark.ts +35 -0
- package/src/plugins/clear.ts +14 -0
- package/src/plugins/image.ts +34 -0
- package/src/plugins/logger.ts +59 -0
- package/src/plugins/repl.ts +63 -0
- package/src/plugins/state-responses.ts +75 -0
- package/src/serialize.ts +125 -0
- package/src/stopwatch.ts +50 -0
- package/src/validate.ts +38 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import type { Command, CommandMap, CommandTypeKey } from 'dreaction-protocol';
|
|
3
|
+
import validate from './validate';
|
|
4
|
+
import logger from './plugins/logger';
|
|
5
|
+
import image from './plugins/image';
|
|
6
|
+
import benchmark from './plugins/benchmark';
|
|
7
|
+
import stateResponses from './plugins/state-responses';
|
|
8
|
+
import apiResponse from './plugins/api-response';
|
|
9
|
+
import clear from './plugins/clear';
|
|
10
|
+
import repl from './plugins/repl';
|
|
11
|
+
import serialize from './serialize';
|
|
12
|
+
import { start } from './stopwatch';
|
|
13
|
+
import { ClientOptions } from './client-options';
|
|
14
|
+
|
|
15
|
+
export type { ClientOptions };
|
|
16
|
+
export { assertHasLoggerPlugin } from './plugins/logger';
|
|
17
|
+
export type { LoggerPlugin } from './plugins/logger';
|
|
18
|
+
export {
|
|
19
|
+
assertHasStateResponsePlugin,
|
|
20
|
+
hasStateResponsePlugin,
|
|
21
|
+
} from './plugins/state-responses';
|
|
22
|
+
export type { StateResponsePlugin } from './plugins/state-responses';
|
|
23
|
+
|
|
24
|
+
export enum ArgType {
|
|
25
|
+
String = 'string',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CustomCommandArg {
|
|
29
|
+
name: string;
|
|
30
|
+
type: ArgType;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// #region Plugin Types
|
|
34
|
+
export interface LifeCycleMethods {
|
|
35
|
+
onCommand?: (command: Command) => void;
|
|
36
|
+
onConnect?: () => void;
|
|
37
|
+
onDisconnect?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type AnyFunction = (...args: any[]) => any;
|
|
41
|
+
export interface Plugin<Client> extends LifeCycleMethods {
|
|
42
|
+
features?: {
|
|
43
|
+
[key: string]: AnyFunction;
|
|
44
|
+
};
|
|
45
|
+
onPlugin?: (client: Client) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type PluginCreator<Client> = (client: Client) => Plugin<Client>;
|
|
49
|
+
|
|
50
|
+
interface DisplayConfig {
|
|
51
|
+
name: string;
|
|
52
|
+
value?: object | string | number | boolean | null | undefined;
|
|
53
|
+
preview?: string;
|
|
54
|
+
image?: string | { uri: string };
|
|
55
|
+
important?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ArgTypeMap {
|
|
59
|
+
[ArgType.String]: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
|
63
|
+
k: infer I
|
|
64
|
+
) => void
|
|
65
|
+
? I
|
|
66
|
+
: never;
|
|
67
|
+
|
|
68
|
+
export type CustomCommandArgs<Args extends CustomCommandArg[]> =
|
|
69
|
+
UnionToIntersection<
|
|
70
|
+
Args extends Array<infer U>
|
|
71
|
+
? U extends CustomCommandArg
|
|
72
|
+
? { [K in U as U['name']]: ArgTypeMap[U['type']] }
|
|
73
|
+
: never
|
|
74
|
+
: never
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
export interface CustomCommand<
|
|
78
|
+
Args extends CustomCommandArg[] = CustomCommandArg[]
|
|
79
|
+
> {
|
|
80
|
+
id?: number;
|
|
81
|
+
command: string;
|
|
82
|
+
handler: (args?: CustomCommandArgs<Args>) => void;
|
|
83
|
+
|
|
84
|
+
title?: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
args?: Args;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type ExtractFeatures<T> = T extends { features: infer U } ? U : never;
|
|
90
|
+
type PluginFeatures<Client, P extends PluginCreator<Client>> = ExtractFeatures<
|
|
91
|
+
ReturnType<P>
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
export type InferFeaturesFromPlugins<
|
|
95
|
+
Client,
|
|
96
|
+
Plugins extends PluginCreator<Client>[]
|
|
97
|
+
> = UnionToIntersection<PluginFeatures<Client, Plugins[number]>>;
|
|
98
|
+
|
|
99
|
+
type InferFeaturesFromPlugin<
|
|
100
|
+
Client,
|
|
101
|
+
P extends PluginCreator<Client>
|
|
102
|
+
> = UnionToIntersection<PluginFeatures<Client, P>>;
|
|
103
|
+
|
|
104
|
+
export interface DReactionCore {
|
|
105
|
+
options: ClientOptions<this>;
|
|
106
|
+
plugins: Plugin<this>[];
|
|
107
|
+
startTimer: () => () => number;
|
|
108
|
+
close: () => this;
|
|
109
|
+
send: <Type extends keyof CommandMap>(
|
|
110
|
+
type: Type,
|
|
111
|
+
payload?: CommandMap[Type],
|
|
112
|
+
important?: boolean
|
|
113
|
+
) => void;
|
|
114
|
+
display: (config: DisplayConfig) => void;
|
|
115
|
+
onCustomCommand: <
|
|
116
|
+
Args extends CustomCommandArg[] = Exclude<CustomCommand['args'], undefined>
|
|
117
|
+
>(
|
|
118
|
+
config: CustomCommand<Args>
|
|
119
|
+
) => () => void | ((config: string, optHandler?: () => void) => () => void);
|
|
120
|
+
/**
|
|
121
|
+
* Set the configuration options.
|
|
122
|
+
*/
|
|
123
|
+
configure: (
|
|
124
|
+
options: ClientOptions<this>
|
|
125
|
+
) => ClientOptions<this>['plugins'] extends PluginCreator<this>[]
|
|
126
|
+
? this & InferFeaturesFromPlugins<this, ClientOptions<this>['plugins']>
|
|
127
|
+
: this;
|
|
128
|
+
|
|
129
|
+
use: <P extends PluginCreator<this>>(
|
|
130
|
+
pluginCreator: P
|
|
131
|
+
) => this & InferFeaturesFromPlugin<this, P>;
|
|
132
|
+
|
|
133
|
+
connect: () => this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type InferFeatures<
|
|
137
|
+
Client = DReactionCore,
|
|
138
|
+
PC extends PluginCreator<Client> = PluginCreator<Client>
|
|
139
|
+
> = PC extends (client: Client) => { features: infer U } ? U : never;
|
|
140
|
+
|
|
141
|
+
export const corePlugins = [
|
|
142
|
+
image(),
|
|
143
|
+
logger(),
|
|
144
|
+
benchmark(),
|
|
145
|
+
stateResponses(),
|
|
146
|
+
apiResponse(),
|
|
147
|
+
clear(),
|
|
148
|
+
repl(),
|
|
149
|
+
] satisfies PluginCreator<DReactionCore>[];
|
|
150
|
+
|
|
151
|
+
export type InferPluginsFromCreators<
|
|
152
|
+
Client,
|
|
153
|
+
PC extends PluginCreator<Client>[]
|
|
154
|
+
> = PC extends Array<infer P extends PluginCreator<Client>>
|
|
155
|
+
? ReturnType<P>[]
|
|
156
|
+
: never;
|
|
157
|
+
// #endregion
|
|
158
|
+
|
|
159
|
+
type CorePluginFeatures = InferFeaturesFromPlugins<
|
|
160
|
+
DReactionCore,
|
|
161
|
+
typeof corePlugins
|
|
162
|
+
>;
|
|
163
|
+
|
|
164
|
+
// export interface Reactotron extends ReactotronCore, CorePluginFeatures {}
|
|
165
|
+
export interface Reactotron extends DReactionCore {}
|
|
166
|
+
|
|
167
|
+
// these are not for you.
|
|
168
|
+
const reservedFeatures = [
|
|
169
|
+
'configure',
|
|
170
|
+
'connect',
|
|
171
|
+
'connected',
|
|
172
|
+
'options',
|
|
173
|
+
'plugins',
|
|
174
|
+
'send',
|
|
175
|
+
'socket',
|
|
176
|
+
'startTimer',
|
|
177
|
+
'use',
|
|
178
|
+
] as const;
|
|
179
|
+
type ReservedKeys = (typeof reservedFeatures)[number];
|
|
180
|
+
const isReservedFeature = (value: string): value is ReservedKeys =>
|
|
181
|
+
reservedFeatures.some((res) => res === value);
|
|
182
|
+
|
|
183
|
+
function emptyPromise() {
|
|
184
|
+
return Promise.resolve('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export class ReactotronImpl
|
|
188
|
+
implements
|
|
189
|
+
Omit<
|
|
190
|
+
DReactionCore,
|
|
191
|
+
'options' | 'plugins' | 'configure' | 'connect' | 'use' | 'close'
|
|
192
|
+
>
|
|
193
|
+
{
|
|
194
|
+
// the configuration options
|
|
195
|
+
options!: ClientOptions<DReactionCore>;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Are we connected to a server?
|
|
199
|
+
*/
|
|
200
|
+
connected = false;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The socket we're using.
|
|
204
|
+
*/
|
|
205
|
+
socket: WebSocket = null as never;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Available plugins.
|
|
209
|
+
*/
|
|
210
|
+
plugins: Plugin<this>[] = [];
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Messages that need to be sent.
|
|
214
|
+
*/
|
|
215
|
+
sendQueue: string[] = [];
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Are we ready to start communicating?
|
|
219
|
+
*/
|
|
220
|
+
isReady = false;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The last time we sent a message.
|
|
224
|
+
*/
|
|
225
|
+
lastMessageDate = new Date();
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The registered custom commands
|
|
229
|
+
*/
|
|
230
|
+
customCommands: CustomCommand[] = [];
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The current ID for custom commands
|
|
234
|
+
*/
|
|
235
|
+
customCommandLatestId = 1;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Starts a timer and returns a function you can call to stop it and return the elapsed time.
|
|
239
|
+
*/
|
|
240
|
+
startTimer = () => start();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Set the configuration options.
|
|
244
|
+
*/
|
|
245
|
+
configure(
|
|
246
|
+
options: ClientOptions<this>
|
|
247
|
+
): ClientOptions<this>['plugins'] extends PluginCreator<this>[]
|
|
248
|
+
? this & InferFeaturesFromPlugins<this, ClientOptions<this>['plugins']>
|
|
249
|
+
: this {
|
|
250
|
+
// options get merged & validated before getting set
|
|
251
|
+
const newOptions = Object.assign(
|
|
252
|
+
{
|
|
253
|
+
createSocket: null as never,
|
|
254
|
+
host: 'localhost',
|
|
255
|
+
port: 9600,
|
|
256
|
+
name: 'reactotron-core-client',
|
|
257
|
+
secure: false,
|
|
258
|
+
plugins: corePlugins,
|
|
259
|
+
safeRecursion: true,
|
|
260
|
+
onCommand: () => null,
|
|
261
|
+
onConnect: () => null,
|
|
262
|
+
onDisconnect: () => null,
|
|
263
|
+
} satisfies ClientOptions<DReactionCore>,
|
|
264
|
+
this.options,
|
|
265
|
+
options
|
|
266
|
+
);
|
|
267
|
+
validate(newOptions);
|
|
268
|
+
this.options = newOptions;
|
|
269
|
+
|
|
270
|
+
// if we have plugins, let's add them here
|
|
271
|
+
if (Array.isArray(this.options.plugins)) {
|
|
272
|
+
this.options.plugins.forEach((p) => this.use(p as never));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return this as this &
|
|
276
|
+
InferFeaturesFromPlugins<
|
|
277
|
+
this,
|
|
278
|
+
Exclude<ClientOptions<this>['plugins'], undefined>
|
|
279
|
+
>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
close() {
|
|
283
|
+
this.connected = false;
|
|
284
|
+
this.socket && this.socket.close && this.socket.close();
|
|
285
|
+
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Connect to the Reactotron server.
|
|
291
|
+
*/
|
|
292
|
+
connect() {
|
|
293
|
+
this.connected = true;
|
|
294
|
+
const {
|
|
295
|
+
createSocket,
|
|
296
|
+
secure,
|
|
297
|
+
host,
|
|
298
|
+
environment,
|
|
299
|
+
port,
|
|
300
|
+
name,
|
|
301
|
+
client = {},
|
|
302
|
+
getClientId,
|
|
303
|
+
} = this.options;
|
|
304
|
+
const { onCommand, onConnect, onDisconnect } = this.options;
|
|
305
|
+
|
|
306
|
+
// establish a connection to the server
|
|
307
|
+
const protocol = secure ? 'wss' : 'ws';
|
|
308
|
+
const socket = createSocket!(`${protocol}://${host}:${port}`);
|
|
309
|
+
|
|
310
|
+
// fires when we talk to the server
|
|
311
|
+
const onOpen = () => {
|
|
312
|
+
// fire our optional onConnect handler
|
|
313
|
+
onConnect && onConnect();
|
|
314
|
+
|
|
315
|
+
// trigger our plugins onConnect
|
|
316
|
+
this.plugins.forEach((p) => p.onConnect && p.onConnect());
|
|
317
|
+
|
|
318
|
+
const getClientIdPromise = getClientId || emptyPromise;
|
|
319
|
+
|
|
320
|
+
getClientIdPromise(name!).then((clientId) => {
|
|
321
|
+
this.isReady = true;
|
|
322
|
+
// introduce ourselves
|
|
323
|
+
this.send('client.intro', {
|
|
324
|
+
environment,
|
|
325
|
+
...client,
|
|
326
|
+
name,
|
|
327
|
+
clientId,
|
|
328
|
+
dreactionCoreClientVersion: 'DREACTION_CORE_CLIENT_VERSION',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// flush the send queue
|
|
332
|
+
while (this.sendQueue.length > 0) {
|
|
333
|
+
const h = this.sendQueue[0];
|
|
334
|
+
this.sendQueue = this.sendQueue.slice(1);
|
|
335
|
+
this.socket.send(h);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// fires when we disconnect
|
|
341
|
+
const onClose = () => {
|
|
342
|
+
this.isReady = false;
|
|
343
|
+
// trigger our disconnect handler
|
|
344
|
+
onDisconnect && onDisconnect();
|
|
345
|
+
|
|
346
|
+
// as well as the plugin's onDisconnect
|
|
347
|
+
this.plugins.forEach((p) => p.onDisconnect && p.onDisconnect());
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const decodeCommandData = (data: unknown) => {
|
|
351
|
+
if (typeof data === 'string') {
|
|
352
|
+
return JSON.parse(data);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (Buffer.isBuffer(data)) {
|
|
356
|
+
return JSON.parse(data.toString());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return data;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// fires when we receive a command, just forward it off
|
|
363
|
+
const onMessage = (data: any) => {
|
|
364
|
+
const command = decodeCommandData(data);
|
|
365
|
+
// trigger our own command handler
|
|
366
|
+
onCommand && onCommand(command);
|
|
367
|
+
|
|
368
|
+
// trigger our plugins onCommand
|
|
369
|
+
this.plugins.forEach((p) => p.onCommand && p.onCommand(command));
|
|
370
|
+
|
|
371
|
+
// trigger our registered custom commands
|
|
372
|
+
if (command.type === 'custom') {
|
|
373
|
+
this.customCommands
|
|
374
|
+
.filter((cc) => {
|
|
375
|
+
if (typeof command.payload === 'string') {
|
|
376
|
+
return cc.command === command.payload;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return cc.command === command.payload.command;
|
|
380
|
+
})
|
|
381
|
+
.forEach((cc) =>
|
|
382
|
+
cc.handler(
|
|
383
|
+
typeof command.payload === 'object'
|
|
384
|
+
? command.payload.args
|
|
385
|
+
: undefined
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
} else if (command.type === 'setClientId') {
|
|
389
|
+
this.options.setClientId && this.options.setClientId(command.payload);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// this is ws style from require('ws') on node js
|
|
394
|
+
if ('on' in socket && socket.on!) {
|
|
395
|
+
const nodeWebSocket = socket as WebSocket;
|
|
396
|
+
nodeWebSocket.on('open', onOpen);
|
|
397
|
+
nodeWebSocket.on('close', onClose);
|
|
398
|
+
nodeWebSocket.on('message', onMessage);
|
|
399
|
+
// assign the socket to the instance
|
|
400
|
+
this.socket = socket;
|
|
401
|
+
} else {
|
|
402
|
+
// this is a browser
|
|
403
|
+
const browserWebSocket = socket as WebSocket;
|
|
404
|
+
socket.onopen = onOpen;
|
|
405
|
+
socket.onclose = onClose;
|
|
406
|
+
socket.onmessage = (evt: WebSocket.MessageEvent) => onMessage(evt.data);
|
|
407
|
+
// assign the socket to the instance
|
|
408
|
+
this.socket = browserWebSocket;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return this;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Sends a command to the server
|
|
416
|
+
*/
|
|
417
|
+
send = <Type extends CommandTypeKey>(
|
|
418
|
+
type: Type,
|
|
419
|
+
payload?: CommandMap[Type]['payload'],
|
|
420
|
+
important?: boolean
|
|
421
|
+
) => {
|
|
422
|
+
// set the timing info
|
|
423
|
+
const date = new Date();
|
|
424
|
+
let deltaTime = date.getTime() - this.lastMessageDate.getTime();
|
|
425
|
+
// glitches in the matrix
|
|
426
|
+
if (deltaTime < 0) {
|
|
427
|
+
deltaTime = 0;
|
|
428
|
+
}
|
|
429
|
+
this.lastMessageDate = date;
|
|
430
|
+
|
|
431
|
+
const fullMessage = {
|
|
432
|
+
type,
|
|
433
|
+
payload,
|
|
434
|
+
important: !!important,
|
|
435
|
+
date: date.toISOString(),
|
|
436
|
+
deltaTime,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const serializedMessage = serialize(fullMessage, this.options.proxyHack);
|
|
440
|
+
|
|
441
|
+
if (this.isReady) {
|
|
442
|
+
// send this command
|
|
443
|
+
try {
|
|
444
|
+
this.socket.send(serializedMessage);
|
|
445
|
+
} catch {
|
|
446
|
+
this.isReady = false;
|
|
447
|
+
console.log(
|
|
448
|
+
'An error occurred communicating with reactotron. Please reload your app'
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// queue it up until we can connect
|
|
453
|
+
this.sendQueue.push(serializedMessage);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Sends a custom command to the server to displays nicely.
|
|
459
|
+
*/
|
|
460
|
+
display(config: DisplayConfig) {
|
|
461
|
+
const { name, value, preview, image: img, important = false } = config;
|
|
462
|
+
const payload = {
|
|
463
|
+
name,
|
|
464
|
+
value: value || null,
|
|
465
|
+
preview: preview || null,
|
|
466
|
+
image: img || null,
|
|
467
|
+
};
|
|
468
|
+
this.send('display', payload, important);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Client libraries can hijack this to report errors.
|
|
473
|
+
*/
|
|
474
|
+
reportError(this: any, error: Error) {
|
|
475
|
+
this.error(error);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Adds a plugin to the system
|
|
480
|
+
*/
|
|
481
|
+
use(
|
|
482
|
+
pluginCreator: PluginCreator<this>
|
|
483
|
+
): this & PluginFeatures<this, typeof pluginCreator> {
|
|
484
|
+
// we're supposed to be given a function
|
|
485
|
+
if (typeof pluginCreator !== 'function') {
|
|
486
|
+
throw new Error('plugins must be a function');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// execute it immediately passing the send function
|
|
490
|
+
const plugin = pluginCreator.bind(this)(this) as ReturnType<
|
|
491
|
+
typeof pluginCreator
|
|
492
|
+
>;
|
|
493
|
+
|
|
494
|
+
// ensure we get an Object-like creature back
|
|
495
|
+
if (typeof plugin !== 'object') {
|
|
496
|
+
throw new Error('plugins must return an object');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// do we have features to mixin?
|
|
500
|
+
if (plugin.features) {
|
|
501
|
+
// validate
|
|
502
|
+
if (typeof plugin.features !== 'object') {
|
|
503
|
+
throw new Error('features must be an object');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// here's how we're going to inject these in
|
|
507
|
+
const inject = (key: string) => {
|
|
508
|
+
// grab the function
|
|
509
|
+
const featureFunction = plugin.features![key];
|
|
510
|
+
|
|
511
|
+
// only functions may pass
|
|
512
|
+
if (typeof featureFunction !== 'function') {
|
|
513
|
+
throw new Error(`feature ${key} is not a function`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ditch reserved names
|
|
517
|
+
if (isReservedFeature(key)) {
|
|
518
|
+
throw new Error(`feature ${key} is a reserved name`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ok, let's glue it up... and lose all respect from elite JS champions.
|
|
522
|
+
(this as any)[key] = featureFunction;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// let's inject
|
|
526
|
+
Object.keys(plugin.features).forEach((key) => inject(key));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// add it to the list
|
|
530
|
+
this.plugins.push(plugin);
|
|
531
|
+
|
|
532
|
+
// call the plugins onPlugin
|
|
533
|
+
plugin.onPlugin &&
|
|
534
|
+
typeof plugin.onPlugin === 'function' &&
|
|
535
|
+
plugin.onPlugin.bind(this)(this);
|
|
536
|
+
|
|
537
|
+
// chain-friendly
|
|
538
|
+
return this as this & PluginFeatures<this, typeof pluginCreator>;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
onCustomCommand(
|
|
542
|
+
config: CustomCommand | string,
|
|
543
|
+
optHandler?: () => void
|
|
544
|
+
): () => void {
|
|
545
|
+
let command: string;
|
|
546
|
+
let handler: () => void;
|
|
547
|
+
let title!: string;
|
|
548
|
+
let description!: string;
|
|
549
|
+
let args!: CustomCommandArg[];
|
|
550
|
+
|
|
551
|
+
if (typeof config === 'string') {
|
|
552
|
+
command = config;
|
|
553
|
+
handler = optHandler!;
|
|
554
|
+
} else {
|
|
555
|
+
command = config.command;
|
|
556
|
+
handler = config.handler;
|
|
557
|
+
|
|
558
|
+
title = config.title!;
|
|
559
|
+
description = config.description!;
|
|
560
|
+
args = config.args!;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Validations
|
|
564
|
+
// Make sure there is a command
|
|
565
|
+
if (!command) {
|
|
566
|
+
throw new Error('A command is required');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Make sure there is a handler
|
|
570
|
+
if (!handler) {
|
|
571
|
+
throw new Error(`A handler is required for command "${command}"`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Make sure the command doesn't already exist
|
|
575
|
+
const existingCommands = this.customCommands.filter(
|
|
576
|
+
(cc) => cc.command === command
|
|
577
|
+
);
|
|
578
|
+
if (existingCommands.length > 0) {
|
|
579
|
+
existingCommands.forEach((command) => {
|
|
580
|
+
this.customCommands = this.customCommands.filter(
|
|
581
|
+
(cc) => cc.id !== command.id
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
this.send('customCommand.unregister', {
|
|
585
|
+
id: command.id,
|
|
586
|
+
command: command.command,
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (args) {
|
|
592
|
+
const argNames = [] as string[];
|
|
593
|
+
|
|
594
|
+
args.forEach((arg) => {
|
|
595
|
+
if (!arg.name) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
`A arg on the command "${command}" is missing a name`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (argNames.indexOf(arg.name) > -1) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`A arg with the name "${arg.name}" already exists in the command "${command}"`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
argNames.push(arg.name);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Create this command handlers object
|
|
612
|
+
const customHandler: CustomCommand = {
|
|
613
|
+
id: this.customCommandLatestId,
|
|
614
|
+
command,
|
|
615
|
+
handler,
|
|
616
|
+
title,
|
|
617
|
+
description,
|
|
618
|
+
args,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Increment our id counter
|
|
622
|
+
this.customCommandLatestId += 1;
|
|
623
|
+
|
|
624
|
+
// Add it to our array
|
|
625
|
+
this.customCommands.push(customHandler);
|
|
626
|
+
|
|
627
|
+
this.send('customCommand.register', {
|
|
628
|
+
id: customHandler.id,
|
|
629
|
+
command: customHandler.command,
|
|
630
|
+
title: customHandler.title,
|
|
631
|
+
description: customHandler.description,
|
|
632
|
+
args: customHandler.args,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return () => {
|
|
636
|
+
this.customCommands = this.customCommands.filter(
|
|
637
|
+
(cc) => cc.id !== customHandler.id
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
this.send('customCommand.unregister', {
|
|
641
|
+
id: customHandler.id,
|
|
642
|
+
command: customHandler.command,
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// convenience factory function
|
|
649
|
+
export function createClient<Client extends DReactionCore = DReactionCore>(
|
|
650
|
+
options?: ClientOptions<Client>
|
|
651
|
+
) {
|
|
652
|
+
const client = new ReactotronImpl();
|
|
653
|
+
return client.configure(options as never) as unknown as Client;
|
|
654
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DReactionCore, Plugin } from '../';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sends API request/response information.
|
|
5
|
+
*/
|
|
6
|
+
const apiResponse = () => (reactotron: DReactionCore) => {
|
|
7
|
+
return {
|
|
8
|
+
features: {
|
|
9
|
+
apiResponse: (
|
|
10
|
+
request: { status: number },
|
|
11
|
+
response: any,
|
|
12
|
+
duration: number
|
|
13
|
+
) => {
|
|
14
|
+
const ok =
|
|
15
|
+
response &&
|
|
16
|
+
response.status &&
|
|
17
|
+
typeof response.status === 'number' &&
|
|
18
|
+
response.status >= 200 &&
|
|
19
|
+
response.status <= 299;
|
|
20
|
+
const important = !ok;
|
|
21
|
+
reactotron.send(
|
|
22
|
+
'api.response',
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
{ request, response, duration },
|
|
25
|
+
important
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} satisfies Plugin<DReactionCore>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default apiResponse;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DReactionCore, Plugin } from '../';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs small high-unscientific benchmarks for you.
|
|
5
|
+
*/
|
|
6
|
+
const benchmark = () => (reactotron: DReactionCore) => {
|
|
7
|
+
const { startTimer } = reactotron;
|
|
8
|
+
|
|
9
|
+
const benchmark = (title: string) => {
|
|
10
|
+
const steps = [] as Array<{ title: string; time: number; delta: number }>;
|
|
11
|
+
const elapsed = startTimer();
|
|
12
|
+
const step = (stepTitle: string) => {
|
|
13
|
+
const previousTime =
|
|
14
|
+
steps.length === 0 ? 0 : (steps[steps.length - 1] as any).time;
|
|
15
|
+
const nextTime = elapsed();
|
|
16
|
+
steps.push({
|
|
17
|
+
title: stepTitle,
|
|
18
|
+
time: nextTime,
|
|
19
|
+
delta: nextTime - previousTime,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
steps.push({ title, time: 0, delta: 0 });
|
|
23
|
+
const stop = (stopTitle: string) => {
|
|
24
|
+
step(stopTitle);
|
|
25
|
+
reactotron.send('benchmark.report', { title, steps });
|
|
26
|
+
};
|
|
27
|
+
return { step, stop, last: stop };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
features: { benchmark },
|
|
32
|
+
} satisfies Plugin<DReactionCore>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default benchmark;
|