emoemu 0.1.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 (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,2 @@
1
+ /** ASCII code for '{' - indicates start of JSON */
2
+ export const JSON_OPEN_BRACE = 0x7b;
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Save Services
3
+ *
4
+ * Service classes for save state and battery save operations.
5
+ * These hold config internally so callers don't need to pass it to every method.
6
+ * Services manage their own directory cache for efficient batch operations.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from 'fs';
10
+ import { basename, dirname, extname, join } from 'path';
11
+ import { decompress, detectCompressionFormat } from '../../utils/compression';
12
+ import type { Config } from '../config';
13
+ import { resolveSaveStateDir, resolveSaveFileDir } from '../config';
14
+ import { type DirectoryCache, createDirectoryCache, getCachedDirectoryListing } from '../directoryCache';
15
+ import { logger } from '../../utils/logger';
16
+ import { getErrorMessage } from '../../utils/getErrorMessage';
17
+
18
+ export * from './consts';
19
+
20
+ import { JSON_OPEN_BRACE } from './consts';
21
+
22
+ /**
23
+ * Escape special regex characters in a string
24
+ */
25
+ const escapeRegExp = (str: string): string => {
26
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
+ };
28
+
29
+ /**
30
+ * Details loaded from a save state file
31
+ */
32
+ export interface SaveStateDetails {
33
+ /** Timestamp when the save state was last modified (from file system) */
34
+ savedAt?: Date;
35
+ }
36
+
37
+ /**
38
+ * Result of checking for a save state
39
+ */
40
+ export interface SaveStateCheckResult {
41
+ exists: boolean;
42
+ date?: Date;
43
+ path?: string;
44
+ }
45
+
46
+ /**
47
+ * Result of checking for a battery save
48
+ */
49
+ export interface BatterySaveCheckResult {
50
+ exists: boolean;
51
+ date?: Date;
52
+ path?: string;
53
+ }
54
+
55
+ /**
56
+ * Service for save state operations.
57
+ * Holds config internally so callers don't need to pass it to every method.
58
+ * Manages its own directory cache for efficient batch operations.
59
+ */
60
+ export class SaveStateService {
61
+ private cache: DirectoryCache = createDirectoryCache();
62
+
63
+ constructor(private config: Config) {}
64
+
65
+ /**
66
+ * Clear the internal directory cache.
67
+ * Call this after batch operations or when files may have changed.
68
+ */
69
+ clearCache(): void {
70
+ this.cache.clear();
71
+ }
72
+
73
+ /**
74
+ * Get the directory where save states should be stored for a ROM.
75
+ * Uses savestates_in_content_dir setting to determine location.
76
+ */
77
+ getSaveDir(romPath: string): string {
78
+ return resolveSaveStateDir(dirname(romPath), this.config);
79
+ }
80
+
81
+ /**
82
+ * Get the path for writing a new save state file.
83
+ * Format: [rom basename without extension].state.auto
84
+ */
85
+ getStatePath(romPath: string): string {
86
+ const dir = this.getSaveDir(romPath);
87
+ const name = basename(romPath, extname(romPath));
88
+ return join(dir, `${name}.state.auto`);
89
+ }
90
+
91
+ /**
92
+ * Find an existing save state file for a ROM.
93
+ * Searches only in the directory specified by config settings.
94
+ *
95
+ * Priority:
96
+ * 1. [romname].state.auto (highest priority)
97
+ * 2. [romname].[anything].state (legacy format with coreId or other suffix)
98
+ * 3. [romname].state (legacy format without suffix)
99
+ *
100
+ * Returns the path to the save state file, or null if none found.
101
+ */
102
+ findExistingStatePath(romPath: string): string | null {
103
+ const dir = this.getSaveDir(romPath);
104
+ const name = basename(romPath, extname(romPath));
105
+
106
+ // Priority 1: Check for .state.auto (preferred format)
107
+ const autoPath = join(dir, `${name}.state.auto`);
108
+ if (existsSync(autoPath)) {
109
+ return autoPath;
110
+ }
111
+
112
+ // Priority 2 & 3: Look for legacy formats
113
+ try {
114
+ const files = readdirSync(dir);
115
+ const statePattern = new RegExp(`^${escapeRegExp(name)}(\\.[^.]+)?\\.state$`);
116
+
117
+ for (const file of files) {
118
+ if (statePattern.test(file)) {
119
+ return join(dir, file);
120
+ }
121
+ }
122
+ } catch {
123
+ // Directory read failed
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Check if a save state exists for a ROM.
131
+ * Uses internal directory cache for efficient batch lookups.
132
+ */
133
+ checkExists(romPath: string): SaveStateCheckResult {
134
+ const dir = this.getSaveDir(romPath);
135
+ const romName = basename(romPath, extname(romPath));
136
+
137
+ // Use cached directory listing for O(1) lookup
138
+ const listing = getCachedDirectoryListing(dir, this.cache);
139
+
140
+ // Priority 1: Check for .state.auto
141
+ const autoFilename = `${romName}.state.auto`;
142
+ const autoMtime = listing.get(autoFilename);
143
+ if (autoMtime) {
144
+ return { exists: true, date: autoMtime, path: join(dir, autoFilename) };
145
+ }
146
+
147
+ // Priority 2 & 3: Check legacy formats
148
+ const statePattern = new RegExp(`^${escapeRegExp(romName)}(\\.[^.]+)?\\.state$`);
149
+ for (const [filename, mtime] of listing.entries()) {
150
+ if (statePattern.test(filename)) {
151
+ return { exists: true, date: mtime, path: join(dir, filename) };
152
+ }
153
+ }
154
+
155
+ return { exists: false };
156
+ }
157
+
158
+ /**
159
+ * Load detailed information from a save state file.
160
+ * Uses file modification time since save states no longer contain metadata.
161
+ */
162
+ loadDetails(romPath: string): SaveStateDetails {
163
+ const result = this.checkExists(romPath);
164
+ if (result.exists && result.date) {
165
+ return { savedAt: result.date };
166
+ }
167
+ return {};
168
+ }
169
+
170
+ /**
171
+ * Load raw binary state directly from file.
172
+ * Automatically decompresses if the file is compressed (zstd, zlib, or gzip).
173
+ */
174
+ loadRawState(romPath: string): Buffer | null {
175
+ const statePath = this.findExistingStatePath(romPath);
176
+
177
+ if (!statePath) {
178
+ return null;
179
+ }
180
+
181
+ try {
182
+ const data = readFileSync(statePath);
183
+ return decompress(data);
184
+ } catch (err) {
185
+ logger.error(`Failed to load raw state: ${statePath} - ${getErrorMessage(err)}`, 'SaveState');
186
+ return null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Save raw binary state directly to file (RetroArch-compatible format).
192
+ * Always writes to .state.auto format.
193
+ */
194
+ saveRawState(romPath: string, buffer: Buffer): void {
195
+ const statePath = this.getStatePath(romPath);
196
+
197
+ try {
198
+ writeFileSync(statePath, buffer);
199
+ } catch (err) {
200
+ logger.error(`Failed to save raw state: ${statePath} - ${getErrorMessage(err)}`, 'SaveState');
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Check if the saved state appears to be a raw binary file (not JSON).
206
+ * Handles compressed files by checking the decompressed content.
207
+ */
208
+ isRawStateFile(romPath: string): boolean {
209
+ const statePath = this.findExistingStatePath(romPath);
210
+
211
+ if (!statePath) {
212
+ return false;
213
+ }
214
+
215
+ try {
216
+ const data = readFileSync(statePath);
217
+ const format = detectCompressionFormat(data);
218
+ const decompressed = format !== 'none' ? decompress(data) : data;
219
+
220
+ if (decompressed.length > 0 && decompressed[0] === JSON_OPEN_BRACE) {
221
+ return false;
222
+ }
223
+
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Delete the save state file for a ROM.
232
+ */
233
+ deleteState(romPath: string): void {
234
+ const statePath = this.findExistingStatePath(romPath);
235
+ if (statePath) {
236
+ try {
237
+ unlinkSync(statePath);
238
+ } catch {
239
+ // Ignore errors when deleting
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Check if a save state exists for this ROM
246
+ */
247
+ hasSavedState(romPath: string): boolean {
248
+ return this.findExistingStatePath(romPath) !== null;
249
+ }
250
+
251
+ /**
252
+ * Get the game ID (ROM filename without path)
253
+ */
254
+ getGameId(romPath: string): string {
255
+ return basename(romPath);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Service for battery save (.srm) operations.
261
+ * Holds config internally so callers don't need to pass it to every method.
262
+ * Manages its own directory cache for efficient batch operations.
263
+ */
264
+ export class BatterySaveService {
265
+ private cache: DirectoryCache = createDirectoryCache();
266
+
267
+ constructor(private config: Config) {}
268
+
269
+ /**
270
+ * Clear the internal directory cache.
271
+ * Call this after batch operations or when files may have changed.
272
+ */
273
+ clearCache(): void {
274
+ this.cache.clear();
275
+ }
276
+
277
+ /**
278
+ * Get the directory where battery saves should be stored for a ROM.
279
+ * Uses savefiles_in_content_dir setting to determine location.
280
+ */
281
+ getSaveDir(romPath: string): string {
282
+ return resolveSaveFileDir(dirname(romPath), this.config);
283
+ }
284
+
285
+ /**
286
+ * Get the path for a battery save file.
287
+ * Format: [rom basename without extension].srm
288
+ */
289
+ getSavePath(romPath: string): string {
290
+ const dir = this.getSaveDir(romPath);
291
+ const name = basename(romPath, extname(romPath));
292
+ return join(dir, `${name}.srm`);
293
+ }
294
+
295
+ /**
296
+ * Check if a battery save exists for a ROM.
297
+ * Uses internal directory cache for efficient batch lookups.
298
+ */
299
+ checkExists(romPath: string): BatterySaveCheckResult {
300
+ const dir = this.getSaveDir(romPath);
301
+ const romName = basename(romPath, extname(romPath));
302
+ const srmFilename = `${romName}.srm`;
303
+
304
+ // Use cached directory listing for O(1) lookup
305
+ const listing = getCachedDirectoryListing(dir, this.cache);
306
+ const mtime = listing.get(srmFilename);
307
+ if (mtime) {
308
+ return { exists: true, date: mtime, path: join(dir, srmFilename) };
309
+ }
310
+
311
+ return { exists: false };
312
+ }
313
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Service Provider
3
+ *
4
+ * Centralized service management for both React and non-React code.
5
+ * Services are initialized once with config and can be accessed anywhere.
6
+ *
7
+ * Usage:
8
+ * // At app startup (CLI or React app mount)
9
+ * initializeServices(config);
10
+ *
11
+ * // Anywhere in the codebase
12
+ * const saveStateService = getSaveStateService();
13
+ * const path = saveStateService.findExistingStatePath(romPath);
14
+ */
15
+
16
+ import type { Config } from '../config';
17
+ import { getPlaylistsDirectory } from '../config';
18
+ import { SaveStateService, BatterySaveService } from '../saveServices';
19
+ import { buildCrcCacheFromDirectory, type CrcCache } from '../playlist';
20
+ import { ServiceError } from './types';
21
+
22
+ /** Current config (set at initialization) */
23
+ let currentConfig: Config | null = null;
24
+
25
+ /** Singleton service instances */
26
+ let saveStateService: SaveStateService | null = null;
27
+ let batterySaveService: BatterySaveService | null = null;
28
+
29
+ /**
30
+ * Initialize services with config.
31
+ * Call this once at app startup before using any services.
32
+ */
33
+ export const initializeServices = (config: Config): void => {
34
+ currentConfig = config;
35
+ saveStateService = new SaveStateService(config);
36
+ batterySaveService = new BatterySaveService(config);
37
+ };
38
+
39
+ /**
40
+ * Update services with new config.
41
+ * Call this when config changes (e.g., settings update).
42
+ */
43
+ export const updateServices = (config: Config): void => {
44
+ currentConfig = config;
45
+ saveStateService = new SaveStateService(config);
46
+ batterySaveService = new BatterySaveService(config);
47
+ };
48
+
49
+ /**
50
+ * Get the current config.
51
+ * Returns null if services haven't been initialized.
52
+ */
53
+ export const getConfig = (): Config | null => currentConfig;
54
+
55
+ /**
56
+ * Get the SaveStateService instance.
57
+ * Throws if services haven't been initialized.
58
+ */
59
+ export const getSaveStateService = (): SaveStateService => {
60
+ if (!saveStateService) {
61
+ throw new ServiceError('NOT_INITIALIZED');
62
+ }
63
+ return saveStateService;
64
+ };
65
+
66
+ /**
67
+ * Get the BatterySaveService instance.
68
+ * Throws if services haven't been initialized.
69
+ */
70
+ export const getBatterySaveService = (): BatterySaveService => {
71
+ if (!batterySaveService) {
72
+ throw new ServiceError('NOT_INITIALIZED');
73
+ }
74
+ return batterySaveService;
75
+ };
76
+
77
+ /**
78
+ * Check if services have been initialized.
79
+ */
80
+ export const areServicesInitialized = (): boolean => {
81
+ return currentConfig !== null && saveStateService !== null && batterySaveService !== null;
82
+ };
83
+
84
+ /**
85
+ * Get the playlist directory from config.
86
+ * Throws if services haven't been initialized.
87
+ */
88
+ export const getPlaylistDirectory = (): string => {
89
+ if (!currentConfig) {
90
+ throw new ServiceError('NOT_INITIALIZED');
91
+ }
92
+ return getPlaylistsDirectory(currentConfig);
93
+ };
94
+
95
+ /**
96
+ * Build a CRC cache from all playlists in the configured playlist directory.
97
+ * This avoids recalculating CRC32s for ROMs that are already in playlists.
98
+ * Returns an empty map if services haven't been initialized.
99
+ */
100
+ export const buildCrcCache = (): CrcCache => {
101
+ if (!currentConfig) {
102
+ return new Map();
103
+ }
104
+ const playlistDir = getPlaylistsDirectory(currentConfig);
105
+ return buildCrcCacheFromDirectory(playlistDir);
106
+ };
107
+
108
+ export * from "./types";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Service provider error types
3
+ */
4
+
5
+ import { createTypedError } from '../../utils/typedError';
6
+
7
+ export type ServiceErrorCode =
8
+ | 'NOT_INITIALIZED';
9
+
10
+ const { TypedError, isTypedError } = createTypedError<ServiceErrorCode>('ServiceError');
11
+ export const ServiceError = TypedError;
12
+ export type ServiceError = InstanceType<typeof TypedError>;
13
+ export const isServiceError = isTypedError;