ogi-addon 3.1.0 → 4.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/src/main.ts CHANGED
@@ -1,400 +1,85 @@
1
- import ws, { WebSocket } from 'ws';
1
+ import { EventResponseSocket, randomMessageId } from '@ogi-sdk/connect';
2
+ import type {
3
+ AddonClientToServerEventArgs,
4
+ AddonClientToServerEventName,
5
+ AddonClientToServerWebsocketMessage,
6
+ AddonNotificationMessage,
7
+ AddonProtocolEventListenerTypes,
8
+ AddonSDKLifecycleEventListenerTypes,
9
+ AddonServerToClientWebsocketMessage,
10
+ BasicLibraryInfo,
11
+ CatalogResponse,
12
+ LibraryInfo,
13
+ OGIAddonConfiguration,
14
+ OGIAddonSDKEventListener,
15
+ SearchResult,
16
+ SetupResponse,
17
+ StoreData,
18
+ AddonTaskRunEventArgs,
19
+ } from '@ogi-sdk/connect';
2
20
  import events from 'node:events';
3
21
  import { ConfigurationBuilder } from './config/ConfigurationBuilder';
4
- import type { ConfigurationFile } from './config/ConfigurationBuilder';
5
- import { Configuration } from './config/Configuration';
22
+ import { Configuration, DefiniteConfig } from './config/Configuration';
6
23
  import EventResponse from './EventResponse';
7
- import type { SearchResult } from './SearchEngine';
8
24
  import Fuse, { IFuseOptions } from 'fuse.js';
9
25
 
10
- /**
11
- * Exposed events that the programmer can use to listen to and emit events.
12
- */
13
- export type OGIAddonEvent =
14
- | 'connect'
15
- | 'disconnect'
16
- | 'configure'
17
- | 'authenticate'
18
- | 'search'
19
- | 'setup'
20
- | 'library-search'
21
- | 'game-details'
22
- | 'exit'
23
- | 'check-for-updates'
24
- | 'request-dl'
25
- | 'catalog'
26
- | 'launch-app';
27
-
28
- /**
29
- * The events that the client can send to the server and are handled by the server.
30
- */
31
- export type OGIAddonClientSentEvent =
32
- | 'response'
33
- | 'authenticate'
34
- | 'configure'
35
- | 'defer-update'
36
- | 'notification'
37
- | 'input-asked'
38
- | 'get-app-details'
39
- | 'search-app-name'
40
- | 'flag'
41
- | 'task-update';
42
-
43
- /**
44
- * The events that the server sends to the client
45
- * This is the events that the server can send to the client and are handled by the client.
46
- */
47
- export type OGIAddonServerSentEvent =
48
- | 'authenticate'
49
- | 'configure'
50
- | 'config-update'
51
- | 'launch-app'
52
- | 'search'
53
- | 'setup'
54
- | 'response'
55
- | 'library-search'
56
- | 'check-for-updates'
57
- | 'task-run'
58
- | 'game-details'
59
- | 'request-dl'
60
- | 'catalog';
61
26
  export { ConfigurationBuilder, Configuration, EventResponse };
62
27
  export { extraction };
63
- export type { SearchResult };
64
28
  const defaultPort = 7654;
65
29
  import pjson from '../package.json';
66
30
  import { z } from 'zod';
67
31
  import { extraction } from './extraction';
68
32
  export const VERSION = pjson.version;
69
33
 
70
- export interface ClientSentEventTypes {
71
- response: any;
72
- authenticate: {
73
- name: string;
74
- id: string;
75
- description: string;
76
- version: string;
77
- author: string;
78
- };
79
- configure: ConfigurationFile;
80
- 'defer-update': {
81
- logs: string[];
82
- progress: number;
83
- };
84
- notification: Notification;
85
- 'input-asked': ConfigurationBuilder<
86
- Record<string, string | number | boolean>
87
- >;
88
- 'task-update': {
89
- id: string;
90
- progress: number;
91
- logs: string[];
92
- finished: boolean;
93
- failed: string | undefined;
94
- };
95
- 'get-app-details': {
96
- appID: number;
97
- storefront: string;
98
- };
99
- 'search-app-name': {
100
- query: string;
101
- storefront: string;
102
- };
103
- flag: {
104
- flag: string;
105
- value: string | string[];
106
- };
107
- }
108
-
109
- export type BasicLibraryInfo = {
110
- name: string;
111
- capsuleImage: string;
112
- appID: number;
113
- storefront: string;
114
- };
115
-
116
- export interface CatalogSection {
117
- name: string;
118
- description: string;
119
- listings: BasicLibraryInfo[];
120
- }
121
-
122
- export interface CatalogCarouselItem {
123
- name: string;
124
- description: string;
125
- carouselImage: string;
126
- fullBannerImage?: string;
127
- appID?: number;
128
- storefront?: string;
129
- capsuleImage?: string;
130
- }
131
-
132
- export interface CatalogWithCarousel {
133
- sections: Record<string, CatalogSection>;
134
- carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];
135
- }
136
-
137
- export type CatalogResponse =
138
- | Record<string, CatalogSection>
139
- | CatalogWithCarousel;
140
-
141
- /**
142
- * UMU ID format: 'steam:${number}' or 'umu:${string | number}'
143
- * - steam:${number} → maps to umu-${number} for Steam games
144
- * - umu:${string | number} → maps to umu-${string | number} for non-Steam games
145
- */
146
- export type UmuId = `steam:${number}` | `umu:${string | number}`;
147
-
148
- export type SetupEventResponse = Omit<
34
+ export type {
35
+ AddonClientToServerEventArgs,
36
+ AddonClientToServerEventName,
37
+ AddonClientToServerWebsocketMessage,
38
+ AddonNotificationMessage,
39
+ AddonServerToClientEventName,
40
+ AddonServerToClientWebsocketMessage,
41
+ BasicLibraryInfo,
42
+ CatalogCarouselItem,
43
+ CatalogResponse,
44
+ CatalogSection,
45
+ CatalogWithCarousel,
46
+ ConfigurationFile,
47
+ ConfigurationOptionType,
48
+ ConfigurationOptionWire,
149
49
  LibraryInfo,
150
- | 'capsuleImage'
151
- | 'coverImage'
152
- | 'name'
153
- | 'appID'
154
- | 'storefront'
155
- | 'addonsource'
156
- | 'titleImage'
157
- > & {
158
- redistributables?: {
159
- name: string;
160
- path: string;
161
- }[];
162
- /**
163
- * UMU Proton integration configuration
164
- */
165
- umu?: {
166
- /**
167
- * UMU ID for the game. Format: 'steam:${number}' or 'umu:${string | number}'
168
- * - steam:${number} → maps to umu-${number} for Steam games
169
- * - umu:${string | number} → maps to umu-${string | number} for non-Steam games
170
- */
171
- umuId: UmuId;
172
- /**
173
- * Optional DLL overrides. Can be WINEDLLOVERRIDES-style (e.g. "dinput8=n,b") or bare DLL names.
174
- * Bare names get "=n,b" inferred; entries that already include "=..." are used as-is.
175
- */
176
- dllOverrides?: string[];
177
- /**
178
- * Optional PROTONPATH override to use for UMU launches.
179
- * Omit this unless you absolutely need a specific Proton build/path.
180
- */
181
- protonVersion?: string;
182
- /**
183
- * Optional store identifier for protonfixes (e.g., 'gog', 'egs', 'none')
184
- */
185
- store?: string;
186
- /**
187
- * Cached Steam shortcut app ID after adding UMU game to Steam (avoids re-adding on each launch)
188
- */
189
- steamShortcutId?: number;
190
- };
191
- };
192
-
193
- export interface EventListenerTypes {
194
- /**
195
- * This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.
196
- * @param event
197
- * @returns
198
- */
199
- connect: (event: EventResponse<void>) => void;
200
-
201
- /**
202
- * This event is emitted when the client requests for the addon to disconnect. Addon does not need to resolve this event, but we recommend `process.exit(0)` so the addon can exit gracefully instead of by force by the addon server.
203
- * @param reason
204
- * @returns
205
- */
206
- disconnect: (reason: string) => void;
207
- /**
208
- * This event is emitted when the client requests for the addon to configure itself. Addon should resolve the event with the internal configuration. (See ConfigurationBuilder)
209
- * @param config
210
- * @returns
211
- */
212
- configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
213
- /**
214
- * This event is called when the client provides a response to any event. This should be treated as middleware.
215
- * @param response
216
- * @returns
217
- */
218
- response: (response: any) => void;
219
-
220
- /**
221
- * This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.
222
- * @param config
223
- * @returns
224
- */
225
- authenticate: (config: any) => void;
226
- /**
227
- * This event is emitted when the client requests for a torrent/direct download search to be performed. Addon is given the gameID (could be a steam appID or custom store appID), along with the storefront type. Addon should resolve the event with the search results. (See SearchResult)
228
- * @param query
229
- * @param event
230
- * @returns
231
- */
232
- search: (
233
- query: {
234
- storefront: string;
235
- appID: number;
236
- } & (
237
- | {
238
- for: 'game' | 'task' | 'all';
239
- }
240
- | {
241
- for: 'update';
242
- libraryInfo: LibraryInfo;
243
- }
244
- ),
245
- event: EventResponse<SearchResult[]>
246
- ) => void;
247
- /**
248
- * This event is emitted when the client requests for app setup to be performed. Addon should resolve the event with the metadata for the library entry. (See LibraryInfo)
249
- * @param data
250
- * @param event
251
- * @returns
252
- */
253
- setup: (
254
- data: {
255
- path: string;
256
- type: 'direct' | 'torrent' | 'magnet' | 'empty';
257
- name: string;
258
- usedRealDebrid: boolean;
259
- clearOldFilesBeforeUpdate?: boolean;
260
- multiPartFiles?: {
261
- name: string;
262
- downloadURL: string;
263
- }[];
264
- appID: number;
265
- storefront: string;
266
- manifest?: Record<string, unknown>;
267
- } & (
268
- | {
269
- for: 'game';
270
- }
271
- | {
272
- for: 'update';
273
- currentLibraryInfo: LibraryInfo;
274
- }
275
- ),
276
- event: EventResponse<SetupEventResponse>
277
- ) => void;
278
-
279
- /**
280
- * This event is emitted when the client requires for a search to be performed. Input is the search query.
281
- * @param query
282
- * @param event
283
- * @returns
284
- */
285
- 'library-search': (
286
- query: string,
287
- event: EventResponse<BasicLibraryInfo[]>
288
- ) => void;
289
-
290
- /**
291
- * This event is emitted when the client requests for a game details to be fetched. Addon should resolve the event with the game details. This is used to generate a store page for the game.
292
- * @param appID
293
- * @param event
294
- * @returns
295
- */
296
- 'game-details': (
297
- details: { appID: number; storefront: string },
298
- event: EventResponse<StoreData | undefined>
299
- ) => void;
300
-
301
- /**
302
- * This event is emitted when the client requests for the addon to exit. Use this to perform any cleanup tasks, ending with a `process.exit(0)`.
303
- * @returns
304
- */
305
- exit: () => void;
306
-
307
- /**
308
- * This event is emitted when the client requests for a download to be performed with the 'request' type. Addon should resolve the event with a SearchResult containing the actual download info.
309
- * @param appID
310
- * @param info
311
- * @param event
312
- * @returns
313
- */
314
- 'request-dl': (
315
- appID: number,
316
- info: SearchResult,
317
- event: EventResponse<SearchResult>
318
- ) => void;
319
-
320
- /**
321
- * This event is emitted when the client requests for a catalog to be fetched. Addon should resolve the event with the catalog.
322
- * @param event
323
- * @returns
324
- */
325
- catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
326
-
327
- /**
328
- * This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.
329
- * @param data
330
- * @param event
331
- * @returns
332
- */
333
- 'check-for-updates': (
334
- data: { appID: number; storefront: string; currentVersion: string },
335
- event: EventResponse<
336
- | {
337
- available: true;
338
- version: string;
339
- }
340
- | {
341
- available: false;
342
- }
343
- >
344
- ) => void;
345
-
346
- /**
347
- * This event is emitted when the client is going to launch an app. Addon should use this to perform any pre or post launch tasks.
348
- * @param data {LibraryInfo} The library information for the app to be launched.
349
- * @param launchType { 'pre' | 'post' } The type of launch task to perform.
350
- * @param event {EventResponse<void>} The event response from the server.
351
- */
352
- 'launch-app': (
353
- data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },
354
- event: EventResponse<void>
355
- ) => void;
356
- }
357
-
358
- export interface StoreData {
359
- name: string;
360
- publishers: string[];
361
- developers: string[];
362
- appID: number;
363
- releaseDate: string;
364
- capsuleImage: string;
365
- coverImage: string;
366
- basicDescription: string;
367
- description: string;
368
- headerImage: string;
369
- latestVersion: string;
370
- }
371
- export interface WebsocketMessageClient {
372
- event: OGIAddonClientSentEvent;
373
- id?: string;
374
- args: any;
375
- statusError?: string;
376
- }
377
- export interface WebsocketMessageServer {
378
- event: OGIAddonServerSentEvent;
379
- id?: string;
380
- args: any;
381
- statusError?: string;
382
- }
50
+ OGIAddonConfiguration,
51
+ OGIAddonSDKEventListener,
52
+ SearchResult,
53
+ SetupResponse,
54
+ SetupEventResponse,
55
+ StoreData,
56
+ UmuId,
57
+ SetupCommandData,
58
+ AddonProtocolEventListenerTypes,
59
+ AddonSDKLifecycleEventListenerTypes,
60
+ AddonServerHostEventListeners,
61
+ AddonServerHostEventName,
62
+ AddonServerLifecycleEvent,
63
+ } from '@ogi-sdk/connect';
64
+
65
+ /** @deprecated Use {@link AddonNotificationMessage}. */
66
+ export type Notification = AddonNotificationMessage;
383
67
 
384
68
  /**
385
- * The configuration for the addon. This is used to identify the addon and provide information about it.
386
- * Storefronts is an array of names of stores that the addon supports.
69
+ * Addon SDK listener signatures. Protocol commands come from `addonProtocol` in
70
+ * `@ogi-sdk/connect`; lifecycle and builder-specific hooks are merged below.
387
71
  */
388
- export interface OGIAddonConfiguration {
389
- name: string;
390
- id: string;
391
- description: string;
392
- version: string;
393
-
394
- author: string;
395
- repository: string;
396
- storefronts: string[];
397
- }
72
+ export type EventListenerTypes = AddonSDKLifecycleEventListenerTypes<
73
+ EventResponse<unknown>
74
+ > &
75
+ AddonProtocolEventListenerTypes<
76
+ EventResponse<unknown>,
77
+ 'authenticate' | 'configure' | 'catalog'
78
+ > & {
79
+ authenticate: (config: unknown) => void;
80
+ configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
81
+ catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
82
+ };
398
83
 
399
84
  /**
400
85
  * The main class for the OGI Addon. This class is used to interact with the OGI Addon Server. The OGI Addon Server provides a `--addonSecret` to the addon so it can securely connect.
@@ -416,7 +101,7 @@ export default class OGIAddon {
416
101
  public addonWSListener: OGIAddonWSListener;
417
102
  public addonInfo: OGIAddonConfiguration;
418
103
  public config: Configuration = new Configuration({});
419
- private eventsAvailable: OGIAddonEvent[] = [];
104
+ private eventsAvailable: OGIAddonSDKEventListener[] = [];
420
105
  private registeredConnectEvent: boolean = false;
421
106
  private taskHandlers: Map<
422
107
  string,
@@ -438,10 +123,10 @@ export default class OGIAddon {
438
123
 
439
124
  /**
440
125
  * Register an event listener for the addon. (See EventListenerTypes)
441
- * @param event {OGIAddonEvent}
442
- * @param listener {EventListenerTypes[OGIAddonEvent]}
126
+ * @param event {OGIAddonSDKEventListener}
127
+ * @param listener {EventListenerTypes[OGIAddonSDKEventListener]}
443
128
  */
444
- public on<T extends OGIAddonEvent>(
129
+ public on<T extends OGIAddonSDKEventListener>(
445
130
  event: T,
446
131
  listener: EventListenerTypes[T]
447
132
  ) {
@@ -459,7 +144,7 @@ export default class OGIAddon {
459
144
  }
460
145
  }
461
146
 
462
- public emit<T extends OGIAddonEvent>(
147
+ public emit<T extends OGIAddonSDKEventListener>(
463
148
  event: T,
464
149
  ...args: Parameters<EventListenerTypes[T]>
465
150
  ) {
@@ -470,7 +155,7 @@ export default class OGIAddon {
470
155
  * Notify the client using a notification. Provide the type of notification, the message, and an ID.
471
156
  * @param notification {Notification}
472
157
  */
473
- public notify(notification: Notification) {
158
+ public notify(notification: AddonNotificationMessage) {
474
159
  this.addonWSListener.send('notification', [notification]);
475
160
  }
476
161
 
@@ -481,23 +166,23 @@ export default class OGIAddon {
481
166
  * @returns {Promise<StoreData>}
482
167
  */
483
168
  public async getAppDetails(appID: number, storefront: string) {
484
- const id = this.addonWSListener.send('get-app-details', {
485
- appID,
486
- storefront,
487
- });
488
- return await this.addonWSListener.waitForResponseFromServer<
489
- StoreData | undefined
490
- >(id);
169
+ return await this.addonWSListener.requestResponse<StoreData | undefined>(
170
+ 'get-app-details',
171
+ {
172
+ appID,
173
+ storefront,
174
+ }
175
+ );
491
176
  }
492
177
 
493
178
  public async searchGame(query: string, storefront: string) {
494
- const id = this.addonWSListener.send('search-app-name', {
495
- query,
496
- storefront,
497
- });
498
- return await this.addonWSListener.waitForResponseFromServer<
499
- BasicLibraryInfo[]
500
- >(id);
179
+ return await this.addonWSListener.requestResponse<BasicLibraryInfo[]>(
180
+ 'search-app-name',
181
+ {
182
+ query,
183
+ storefront,
184
+ }
185
+ );
501
186
  }
502
187
 
503
188
  /**
@@ -772,7 +457,7 @@ export class SearchTool<T> {
772
457
  /**
773
458
  * Library Info is the metadata for a library entry after setting up a game.
774
459
  */
775
- export const ZodLibraryInfo = z.object({
460
+ export const ZodLibraryInfo: z.ZodType<LibraryInfo> = z.object({
776
461
  name: z.string(),
777
462
  version: z.string(),
778
463
  cwd: z.string(),
@@ -815,36 +500,52 @@ export const ZodLibraryInfo = z.object({
815
500
  )
816
501
  .optional(),
817
502
  });
818
- export type LibraryInfo = z.infer<typeof ZodLibraryInfo>;
819
- interface Notification {
820
- type: 'warning' | 'error' | 'info' | 'success';
821
- message: string;
822
- id: string;
823
- }
503
+
504
+ export type { AddonTaskRunEventArgs as TaskRunMessageArgs } from '@ogi-sdk/connect';
505
+
824
506
  class OGIAddonWSListener {
825
- private socket: WebSocket;
507
+ private socket: InstanceType<typeof globalThis.WebSocket>;
508
+ private transport: EventResponseSocket<
509
+ AddonServerToClientWebsocketMessage,
510
+ AddonClientToServerWebsocketMessage
511
+ >;
826
512
  public eventEmitter: events.EventEmitter;
827
513
  public addon: OGIAddon;
828
514
 
829
515
  constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {
830
- if (
831
- process.argv[process.argv.length - 1].split('=')[0] !== '--addonSecret'
832
- ) {
516
+ const secret = process.argv
517
+ .find((arg) => arg.startsWith('--addonSecret='))
518
+ ?.split('=')[1];
519
+ if (!secret) {
833
520
  throw new Error(
834
521
  'No secret provided. This usually happens because the addon was not started by the OGI Addon Server.'
835
522
  );
836
523
  }
524
+
525
+ // get the port from the arguments
526
+ let port = process.argv
527
+ .find((arg) => arg.startsWith('--addonPort='))
528
+ ?.split('=')[1];
529
+ if (!port) {
530
+ port = defaultPort.toString();
531
+ }
532
+
837
533
  this.addon = ogiAddon;
838
534
  this.eventEmitter = eventEmitter;
839
- this.socket = new ws('ws://localhost:' + defaultPort);
840
- this.socket.on('open', () => {
535
+ const WebSocketConstructor = globalThis.WebSocket;
536
+ if (!WebSocketConstructor) {
537
+ throw new Error('WebSocket is not available in this runtime');
538
+ }
539
+ this.socket = new WebSocketConstructor('ws://localhost:' + port);
540
+ this.transport = new EventResponseSocket(this.socket);
541
+ this.socket.addEventListener('open', () => {
841
542
  console.log('Connected to OGI Addon Server');
842
543
  console.log('OGI Addon Server Version:', VERSION);
843
544
 
844
545
  // Authenticate with OGI Addon Server
845
- this.send('authenticate', {
546
+ this.send('authenticate' as AddonClientToServerEventName, {
846
547
  ...this.addon.addonInfo,
847
- secret: process.argv[process.argv.length - 1].split('=')[1],
548
+ secret,
848
549
  ogiVersion: VERSION,
849
550
  });
850
551
 
@@ -855,59 +556,44 @@ class OGIAddonWSListener {
855
556
  this.addon.config = new Configuration(configBuilder.build(true));
856
557
 
857
558
  // wait for the config-update to be received then send connect
858
- const configListener = (event: ws.MessageEvent) => {
859
- if (event === undefined) return;
860
- // event can be a Buffer, string, ArrayBuffer, or Buffer[]
861
- let data: string;
862
- if (typeof event === 'string') {
863
- data = event;
864
- } else if (event instanceof Buffer) {
865
- data = event.toString();
866
- } else if (event && typeof (event as any).data === 'string') {
867
- data = (event as any).data;
868
- } else if (event && (event as any).data instanceof Buffer) {
869
- data = (event as any).data.toString();
870
- } else {
871
- // fallback for other types
872
- data = event.toString();
873
- }
874
- const message: WebsocketMessageServer = JSON.parse(data);
875
- if (message.event === 'config-update') {
559
+ const unsubscribeConfigListener = this.transport.on(
560
+ 'config-update',
561
+ () => {
876
562
  console.log('Config update received');
877
- this.socket.off('message', configListener);
563
+ unsubscribeConfigListener();
878
564
  this.eventEmitter.emit(
879
565
  'connect',
880
566
  new EventResponse<void>((screen, name, description) => {
881
- return this.userInputAsked(
882
- screen,
883
- name,
884
- description,
885
- this.socket
886
- );
567
+ return this.userInputAsked(screen, name, description);
887
568
  })
888
569
  );
889
570
  }
890
- };
891
- this.socket.on('message', configListener);
571
+ );
892
572
  });
893
573
 
894
- this.socket.on('error', (error) => {
895
- if (error.message.includes('Failed to connect')) {
574
+ this.socket.addEventListener('error', (event) => {
575
+ this.transport.rejectPendingResponses('Websocket error');
576
+ const message =
577
+ event instanceof ErrorEvent
578
+ ? event.message
579
+ : event.type;
580
+ if (message.includes('Failed to connect')) {
896
581
  throw new Error(
897
582
  'OGI Addon Server is not running/is unreachable. Please start the server and try again.'
898
583
  );
899
584
  }
900
- console.error('An error occurred:', error);
585
+ console.error('An error occurred:', event);
901
586
  });
902
587
 
903
- this.socket.on('close', (code, reason) => {
904
- if (code === 1008) {
905
- console.error('Authentication failed:', reason);
588
+ this.socket.addEventListener('close', (event) => {
589
+ this.transport.rejectPendingResponses('Websocket closed');
590
+ if (event.code === 1008) {
591
+ console.error('Authentication failed:', event.reason);
906
592
  return;
907
593
  }
908
- this.eventEmitter.emit('disconnect', reason);
594
+ this.eventEmitter.emit('disconnect', event.reason);
909
595
  console.log('Disconnected from OGI Addon Server');
910
- console.error(reason.toString());
596
+ console.error(event.reason);
911
597
  this.eventEmitter.emit('exit');
912
598
  this.socket.close();
913
599
  });
@@ -920,173 +606,188 @@ class OGIAddonWSListener {
920
606
  >(
921
607
  configBuilt: ConfigurationBuilder<U>,
922
608
  name: string,
923
- description: string,
924
- socket: WebSocket
609
+ description: string
925
610
  ): Promise<U> {
926
611
  const config = configBuilt.build(false);
927
- const id = Math.random().toString(36).substring(7);
928
- if (!socket) {
929
- throw new Error('Socket is not connected');
930
- }
931
- socket.send(
932
- JSON.stringify({
612
+ const response = await this.transport.send(
613
+ {
933
614
  event: 'input-asked',
934
615
  args: {
935
616
  config,
936
617
  name,
937
618
  description,
938
619
  },
939
- id: id,
940
- })
620
+ } as AddonClientToServerWebsocketMessage,
621
+ { expectResponse: true }
941
622
  );
942
- return await this.waitForResponseFromServer<U>(id);
623
+ return response.args as U;
943
624
  }
944
625
 
945
626
  /**
946
627
  * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
947
628
  */
948
629
  private registerMessageReceiver() {
949
- this.socket.on('message', async (data: string) => {
950
- const message: WebsocketMessageServer = JSON.parse(data);
951
- switch (message.event) {
952
- case 'config-update':
953
- const result = this.addon.config.updateConfig(message.args);
954
- if (!result[0]) {
955
- this.respondToMessage(
956
- message.id!!,
957
- {
958
- success: false,
959
- error: result[1],
960
- },
961
- undefined
630
+ const events: AddonServerToClientWebsocketMessage['event'][] = [
631
+ 'config-update',
632
+ 'search',
633
+ 'setup',
634
+ 'library-search',
635
+ 'game-details',
636
+ 'check-for-updates',
637
+ 'request-dl',
638
+ 'catalog',
639
+ 'task-run',
640
+ 'launch-app',
641
+ ];
642
+
643
+ for (const event of events) {
644
+ this.transport.on(event, async (message) => {
645
+ switch (message.event) {
646
+ case 'config-update':
647
+ const result = this.addon.config.updateConfig(
648
+ message.args as DefiniteConfig
962
649
  );
963
- } else {
964
- this.respondToMessage(message.id!!, { success: true }, undefined);
650
+ if (!result[0]) {
651
+ this.respondToMessage(
652
+ message.id!!,
653
+ {
654
+ success: false,
655
+ error: result[1],
656
+ },
657
+ undefined
658
+ );
659
+ } else {
660
+ this.respondToMessage(message.id!!, { success: true }, undefined);
661
+ }
662
+ break;
663
+ case 'search':
664
+ await this.handleEventWithResponse<SearchResult[]>(
665
+ message,
666
+ (event) => this.eventEmitter.emit('search', message.args, event)
667
+ );
668
+ break;
669
+ case 'setup': {
670
+ let setupEvent = new EventResponse<SetupResponse>(
671
+ (screen, name, description) =>
672
+ this.userInputAsked(screen, name, description)
673
+ );
674
+ this.eventEmitter.emit('setup', message.args, setupEvent);
675
+ const interval = setInterval(() => {
676
+ if (setupEvent.resolved) {
677
+ clearInterval(interval);
678
+ return;
679
+ }
680
+ this.send('defer-update', {
681
+ logs: setupEvent.logs,
682
+ deferID:
683
+ message.args as AddonClientToServerEventArgs['defer-update']['deferID'],
684
+ progress: setupEvent.progress,
685
+ failed: setupEvent.failed,
686
+ } as AddonClientToServerEventArgs['defer-update']);
687
+ }, 100);
688
+ const setupResult = await this.waitForEventToRespond(setupEvent);
689
+ this.respondToMessage(message.id!!, setupResult.data, setupEvent);
690
+ break;
965
691
  }
966
- break;
967
- case 'search':
968
- await this.handleEventWithResponse<SearchResult[]>(message, (event) =>
969
- this.eventEmitter.emit('search', message.args, event)
970
- );
971
- break;
972
- case 'setup': {
973
- let setupEvent = new EventResponse<SetupEventResponse>(
974
- (screen, name, description) =>
975
- this.userInputAsked(screen, name, description, this.socket)
976
- );
977
- this.eventEmitter.emit('setup', message.args, setupEvent);
978
- const interval = setInterval(() => {
979
- if (setupEvent.resolved) {
980
- clearInterval(interval);
981
- return;
692
+ case 'library-search':
693
+ await this.handleEventWithResponse<BasicLibraryInfo[]>(
694
+ message,
695
+ (event) =>
696
+ this.eventEmitter.emit('library-search', message.args, event)
697
+ );
698
+ break;
699
+ case 'game-details':
700
+ await this.handleEventWithResponse<StoreData | undefined>(
701
+ message,
702
+ (event) =>
703
+ this.eventEmitter.emit('game-details', message.args, event),
704
+ {
705
+ requireListener: 'game-details',
706
+ noListenerError: 'No event listener for game-details',
707
+ }
708
+ );
709
+ break;
710
+ case 'check-for-updates':
711
+ await this.handleEventWithResponse<
712
+ { available: true; version: string } | { available: false }
713
+ >(message, (event) =>
714
+ this.eventEmitter.emit('check-for-updates', message.args, event)
715
+ );
716
+ break;
717
+ case 'request-dl':
718
+ let requestDLEvent = new EventResponse<SearchResult>(
719
+ (screen, name, description) =>
720
+ this.userInputAsked(screen, name, description)
721
+ );
722
+ if (this.eventEmitter.listenerCount('request-dl') === 0) {
723
+ this.respondToMessage(
724
+ message.id!!,
725
+ {
726
+ error: 'No event listener for request-dl',
727
+ },
728
+ requestDLEvent
729
+ );
730
+ break;
982
731
  }
983
- this.send('defer-update', {
984
- logs: setupEvent.logs,
985
- deferID: message.args.deferID,
986
- progress: setupEvent.progress,
987
- failed: setupEvent.failed,
988
- } as ClientSentEventTypes['defer-update']);
989
- }, 100);
990
- const setupResult = await this.waitForEventToRespond(setupEvent);
991
- this.respondToMessage(message.id!!, setupResult.data, setupEvent);
992
- break;
993
- }
994
- case 'library-search':
995
- await this.handleEventWithResponse<BasicLibraryInfo[]>(
996
- message,
997
- (event) =>
998
- this.eventEmitter.emit('library-search', message.args, event)
999
- );
1000
- break;
1001
- case 'game-details':
1002
- await this.handleEventWithResponse<StoreData | undefined>(
1003
- message,
1004
- (event) =>
1005
- this.eventEmitter.emit('game-details', message.args, event),
1006
- {
1007
- requireListener: 'game-details',
1008
- noListenerError: 'No event listener for game-details',
732
+ const { appID, info } = message.args as {
733
+ appID: number;
734
+ info: SearchResult;
735
+ };
736
+ this.eventEmitter.emit(
737
+ 'request-dl',
738
+ appID,
739
+ info,
740
+ requestDLEvent as EventResponse<SearchResult>
741
+ );
742
+ const requestDLResult =
743
+ await this.waitForEventToRespond(requestDLEvent);
744
+ if (requestDLEvent.failed) {
745
+ this.respondToMessage(message.id!!, undefined, requestDLEvent);
746
+ break;
747
+ }
748
+ if (
749
+ requestDLEvent.data === undefined ||
750
+ requestDLEvent.data?.downloadType === 'request'
751
+ ) {
752
+ throw new Error(
753
+ 'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'
754
+ );
1009
755
  }
1010
- );
1011
- break;
1012
- case 'check-for-updates':
1013
- await this.handleEventWithResponse<
1014
- { available: true; version: string } | { available: false }
1015
- >(message, (event) =>
1016
- this.eventEmitter.emit('check-for-updates', message.args, event)
1017
- );
1018
- break;
1019
- case 'request-dl':
1020
- let requestDLEvent = new EventResponse<SearchResult>(
1021
- (screen, name, description) =>
1022
- this.userInputAsked(screen, name, description, this.socket)
1023
- );
1024
- if (this.eventEmitter.listenerCount('request-dl') === 0) {
1025
756
  this.respondToMessage(
1026
757
  message.id!!,
1027
- {
1028
- error: 'No event listener for request-dl',
1029
- },
758
+ requestDLResult.data,
1030
759
  requestDLEvent
1031
760
  );
1032
761
  break;
1033
- }
1034
- this.eventEmitter.emit(
1035
- 'request-dl',
1036
- message.args.appID,
1037
- message.args.info,
1038
- requestDLEvent
1039
- );
1040
- const requestDLResult =
1041
- await this.waitForEventToRespond(requestDLEvent);
1042
- if (requestDLEvent.failed) {
1043
- this.respondToMessage(message.id!!, undefined, requestDLEvent);
762
+ case 'catalog':
763
+ await this.handleEventWithResponseNoInput<CatalogResponse>(
764
+ message,
765
+ (event) => this.eventEmitter.emit('catalog', event)
766
+ );
1044
767
  break;
1045
- }
1046
- if (
1047
- requestDLEvent.data === undefined ||
1048
- requestDLEvent.data?.downloadType === 'request'
1049
- ) {
1050
- throw new Error(
1051
- 'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'
768
+ case 'task-run': {
769
+ let taskRunEvent = new EventResponse<void>(
770
+ (screen, name, description) =>
771
+ this.userInputAsked(screen, name, description)
1052
772
  );
1053
- }
1054
- this.respondToMessage(
1055
- message.id!!,
1056
- requestDLResult.data,
1057
- requestDLEvent
1058
- );
1059
- break;
1060
- case 'catalog':
1061
- await this.handleEventWithResponseNoInput<CatalogResponse>(
1062
- message,
1063
- (event) => this.eventEmitter.emit('catalog', event)
1064
- );
1065
- break;
1066
- case 'task-run': {
1067
- let taskRunEvent = new EventResponse<void>(
1068
- (screen, name, description) =>
1069
- this.userInputAsked(screen, name, description, this.socket)
1070
- );
1071
-
1072
- // Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)
1073
- const taskName =
1074
- message.args.taskName && typeof message.args.taskName === 'string'
1075
- ? message.args.taskName
1076
- : message.args.manifest &&
1077
- typeof message.args.manifest === 'object'
1078
- ? (message.args.manifest as Record<string, unknown>).__taskName
1079
- : undefined;
1080
-
1081
- if (
1082
- taskName &&
1083
- typeof taskName === 'string' &&
1084
- this.addon.hasTaskHandler(taskName)
1085
- ) {
1086
- // Use the registered task handler
1087
- const handler = this.addon.getTaskHandler(taskName)!;
1088
- const task = new Task(taskRunEvent);
1089
- try {
773
+ const args = message.args as AddonTaskRunEventArgs;
774
+
775
+ // Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)
776
+ const taskName =
777
+ args.taskName && typeof args.taskName === 'string'
778
+ ? args.taskName
779
+ : args.manifest && typeof args.manifest === 'object'
780
+ ? args.manifest.__taskName
781
+ : undefined;
782
+
783
+ if (
784
+ taskName &&
785
+ typeof taskName === 'string' &&
786
+ this.addon.hasTaskHandler(taskName)
787
+ ) {
788
+ // Use the registered task handler
789
+ const handler = this.addon.getTaskHandler(taskName)!;
790
+ const task = new Task(taskRunEvent);
1090
791
  const interval = setInterval(() => {
1091
792
  if (taskRunEvent.resolved) {
1092
793
  clearInterval(interval);
@@ -1094,48 +795,55 @@ class OGIAddonWSListener {
1094
795
  }
1095
796
  this.send('defer-update', {
1096
797
  logs: taskRunEvent.logs,
1097
- deferID: message.args.deferID,
798
+ deferID: args.deferID ?? '',
1098
799
  progress: taskRunEvent.progress,
1099
800
  failed: taskRunEvent.failed,
1100
- } as ClientSentEventTypes['defer-update']);
801
+ } as AddonClientToServerEventArgs['defer-update']);
1101
802
  }, 100);
1102
- const result = handler(task, {
1103
- manifest: message.args.manifest || {},
1104
- downloadPath: message.args.downloadPath || '',
1105
- name: message.args.name || '',
1106
- libraryInfo: message.args.libraryInfo,
1107
- });
1108
- // If handler returns a promise, wait for it
1109
- if (result instanceof Promise) {
1110
- await result;
803
+ try {
804
+ const result = handler(task, {
805
+ manifest: args.manifest || {},
806
+ downloadPath: args.downloadPath || '',
807
+ name: args.name || '',
808
+ libraryInfo: args.libraryInfo,
809
+ });
810
+ // If handler returns a promise, wait for it
811
+ if (result instanceof Promise) {
812
+ await result;
813
+ }
814
+ } catch (error) {
815
+ taskRunEvent.fail(
816
+ error instanceof Error ? error.message : String(error)
817
+ );
818
+ } finally {
819
+ clearInterval(interval);
1111
820
  }
1112
-
1113
- clearInterval(interval);
1114
- } catch (error) {
821
+ } else {
822
+ // No handler found - fail the task
1115
823
  taskRunEvent.fail(
1116
- error instanceof Error ? error.message : String(error)
824
+ taskName
825
+ ? `No task handler registered for task name: ${taskName}`
826
+ : 'No task name provided'
1117
827
  );
1118
828
  }
1119
- } else {
1120
- // No handler found - fail the task
1121
- taskRunEvent.fail(
1122
- taskName
1123
- ? `No task handler registered for task name: ${taskName}`
1124
- : 'No task name provided'
829
+
830
+ const taskRunResult =
831
+ await this.waitForEventToRespond(taskRunEvent);
832
+ this.respondToMessage(
833
+ message.id!!,
834
+ taskRunResult.data,
835
+ taskRunEvent
1125
836
  );
837
+ break;
1126
838
  }
1127
-
1128
- const taskRunResult = await this.waitForEventToRespond(taskRunEvent);
1129
- this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);
1130
- break;
839
+ case 'launch-app':
840
+ await this.handleEventWithResponse<void>(message, (event) =>
841
+ this.eventEmitter.emit('launch-app', message.args, event)
842
+ );
843
+ break;
1131
844
  }
1132
- case 'launch-app':
1133
- await this.handleEventWithResponse<void>(message, (event) =>
1134
- this.eventEmitter.emit('launch-app', message.args, event)
1135
- );
1136
- break;
1137
- }
1138
- });
845
+ });
846
+ }
1139
847
  }
1140
848
 
1141
849
  private waitForEventToRespond<T>(
@@ -1171,12 +879,12 @@ class OGIAddonWSListener {
1171
879
  * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
1172
880
  */
1173
881
  private async handleEventWithResponse<T>(
1174
- message: WebsocketMessageServer,
882
+ message: AddonServerToClientWebsocketMessage,
1175
883
  emit: (event: EventResponse<T>) => void,
1176
884
  options?: { requireListener: string; noListenerError: string }
1177
885
  ): Promise<void> {
1178
886
  const event = new EventResponse<T>((screen, name, description) =>
1179
- this.userInputAsked(screen, name, description, this.socket)
887
+ this.userInputAsked(screen, name, description)
1180
888
  );
1181
889
  if (
1182
890
  options &&
@@ -1198,7 +906,7 @@ class OGIAddonWSListener {
1198
906
  * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
1199
907
  */
1200
908
  private async handleEventWithResponseNoInput<T>(
1201
- message: WebsocketMessageServer,
909
+ message: AddonServerToClientWebsocketMessage,
1202
910
  emit: (event: EventResponse<T>) => void
1203
911
  ): Promise<void> {
1204
912
  const event = new EventResponse<T>();
@@ -1212,49 +920,41 @@ class OGIAddonWSListener {
1212
920
  response: any,
1213
921
  originalEvent: EventResponse<any> | undefined
1214
922
  ) {
1215
- this.socket.send(
1216
- JSON.stringify({
923
+ void this.transport.send(
924
+ {
1217
925
  event: 'response',
1218
926
  id: messageID,
1219
927
  args: response,
1220
928
  statusError: originalEvent ? originalEvent.failed : undefined,
1221
- })
929
+ } as AddonClientToServerWebsocketMessage,
930
+ { expectResponse: false }
1222
931
  );
1223
932
  console.log('dispatched response to ' + messageID);
1224
933
  }
1225
934
 
1226
- public waitForResponseFromServer<T>(messageID: string): Promise<T> {
1227
- return new Promise((resolve) => {
1228
- const waiter = (data: string) => {
1229
- const message: WebsocketMessageClient = JSON.parse(data);
1230
- if (message.event !== 'response') {
1231
- this.socket.once('message', waiter);
1232
- return;
1233
- }
1234
- console.log('received response from ' + messageID);
1235
-
1236
- if (message.id === messageID) {
1237
- resolve(message.args);
1238
- } else {
1239
- this.socket.once('message', waiter);
1240
- }
1241
- };
1242
- this.socket.once('message', waiter);
1243
- });
935
+ public async requestResponse<T>(
936
+ event: AddonClientToServerEventName,
937
+ args: AddonClientToServerEventArgs[AddonClientToServerEventName]
938
+ ): Promise<T> {
939
+ const response = await this.transport.send(
940
+ { event, args } as AddonClientToServerWebsocketMessage,
941
+ { expectResponse: true }
942
+ );
943
+ return response.args as T;
1244
944
  }
1245
945
 
1246
946
  public send(
1247
- event: OGIAddonClientSentEvent,
1248
- args: ClientSentEventTypes[OGIAddonClientSentEvent]
947
+ event: AddonClientToServerEventName,
948
+ args: AddonClientToServerEventArgs[AddonClientToServerEventName]
1249
949
  ): string {
1250
- // generate a random id
1251
- const id = Math.random().toString(36).substring(7);
1252
- this.socket.send(
1253
- JSON.stringify({
950
+ const id = randomMessageId();
951
+ void this.transport.send(
952
+ {
1254
953
  event,
1255
954
  args,
1256
955
  id,
1257
- })
956
+ } as AddonClientToServerWebsocketMessage,
957
+ { expectResponse: false }
1258
958
  );
1259
959
  return id;
1260
960
  }