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.
Files changed (52) hide show
  1. package/lib/client-options.d.ts +77 -0
  2. package/lib/client-options.d.ts.map +1 -0
  3. package/lib/client-options.js +2 -0
  4. package/lib/index.d.ts +190 -0
  5. package/lib/index.d.ts.map +1 -0
  6. package/lib/index.js +401 -0
  7. package/lib/plugins/api-response.d.ts +13 -0
  8. package/lib/plugins/api-response.d.ts.map +1 -0
  9. package/lib/plugins/api-response.js +23 -0
  10. package/lib/plugins/benchmark.d.ts +15 -0
  11. package/lib/plugins/benchmark.d.ts.map +1 -0
  12. package/lib/plugins/benchmark.js +31 -0
  13. package/lib/plugins/clear.d.ts +11 -0
  14. package/lib/plugins/clear.d.ts.map +1 -0
  15. package/lib/plugins/clear.js +13 -0
  16. package/lib/plugins/image.d.ts +19 -0
  17. package/lib/plugins/image.d.ts.map +1 -0
  18. package/lib/plugins/image.js +24 -0
  19. package/lib/plugins/logger.d.ts +18 -0
  20. package/lib/plugins/logger.d.ts.map +1 -0
  21. package/lib/plugins/logger.js +44 -0
  22. package/lib/plugins/repl.d.ts +10 -0
  23. package/lib/plugins/repl.d.ts.map +1 -0
  24. package/lib/plugins/repl.js +55 -0
  25. package/lib/plugins/state-responses.d.ts +20 -0
  26. package/lib/plugins/state-responses.d.ts.map +1 -0
  27. package/lib/plugins/state-responses.js +38 -0
  28. package/lib/reactotron-core-client.d.ts +191 -0
  29. package/lib/reactotron-core-client.d.ts.map +1 -0
  30. package/lib/reactotron-core-client.js +400 -0
  31. package/lib/serialize.d.ts +20 -0
  32. package/lib/serialize.d.ts.map +1 -0
  33. package/lib/serialize.js +112 -0
  34. package/lib/stopwatch.d.ts +6 -0
  35. package/lib/stopwatch.d.ts.map +1 -0
  36. package/lib/stopwatch.js +45 -0
  37. package/lib/validate.d.ts +9 -0
  38. package/lib/validate.d.ts.map +1 -0
  39. package/lib/validate.js +26 -0
  40. package/package.json +29 -0
  41. package/src/client-options.ts +95 -0
  42. package/src/index.ts +654 -0
  43. package/src/plugins/api-response.ts +32 -0
  44. package/src/plugins/benchmark.ts +35 -0
  45. package/src/plugins/clear.ts +14 -0
  46. package/src/plugins/image.ts +34 -0
  47. package/src/plugins/logger.ts +59 -0
  48. package/src/plugins/repl.ts +63 -0
  49. package/src/plugins/state-responses.ts +75 -0
  50. package/src/serialize.ts +125 -0
  51. package/src/stopwatch.ts +50 -0
  52. 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;