@omss/core 0.0.2-beta.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/dist/index.mjs ADDED
@@ -0,0 +1,1378 @@
1
+ //#region src/features/hooks/HookRegistry.ts
2
+ /**
3
+ * Hook Registry
4
+ *
5
+ * Manages lifecycle hooks for OMSS events.
6
+ */
7
+ var HookRegistry = class {
8
+ /**
9
+ * Map storing arrays of hook handlers for each hook name.
10
+ */
11
+ #hooks = /* @__PURE__ */ new Map();
12
+ /**
13
+ * Get all registered hooks immutable.
14
+ * @dangerous - Be careful with this. what you are doing might cause side effects.
15
+ */
16
+ get hooks() {
17
+ return this.#hooks;
18
+ }
19
+ /**
20
+ * Clear all registered hooks.
21
+ * @dangerous - Be careful with this. Might cause side effects.
22
+ */
23
+ reset() {
24
+ return this.#hooks.clear();
25
+ }
26
+ /**
27
+ * Run all hooks for a lifecycle event with the provided payload.
28
+ *
29
+ * @typeParam K - The name of the hook to run.
30
+ * @param name - The hook name (key of THooks).
31
+ * @param payload - The payload to pass to each hook handler.
32
+ */
33
+ async run(name, payload) {
34
+ const fns = this.#hooks.get(name) ?? [];
35
+ for (const fn of fns) await fn(payload);
36
+ }
37
+ /**
38
+ * Add a hook handler for a lifecycle event.
39
+ * @param name - The hook name (key of THooks).
40
+ * @param cb - The hook handler function.
41
+ */
42
+ add(name, cb) {
43
+ const existing = this.#hooks.get(name) ?? [];
44
+ this.#hooks.set(name, [...existing, cb]);
45
+ }
46
+ };
47
+ //#endregion
48
+ //#region src/features/hooks/HookService.ts
49
+ var HookService = class {
50
+ #hookRegistry;
51
+ constructor(hookRegistry = new HookRegistry()) {
52
+ this.#hookRegistry = hookRegistry;
53
+ }
54
+ /**
55
+ * Get all registered hooks immutable. TO ADD HOOKS, USE THE ADD METHOD
56
+ * @dangerous - Be careful with this. what you are doing might cause side effects.
57
+ */
58
+ get hooks() {
59
+ return this.#hookRegistry.hooks;
60
+ }
61
+ /**
62
+ * Register a hook for a lifecycle event.
63
+ *
64
+ * @typeParam K - The name of the hook to register.
65
+ * @param name - The hook name (key of THooks).
66
+ * @param cb - The handler function for this hook.
67
+ */
68
+ add(name, cb) {
69
+ this.#hookRegistry.add(name, cb);
70
+ }
71
+ /**
72
+ * Clear all registered hooks.
73
+ * @dangerous - Be careful with this. Might cause side effects.
74
+ */
75
+ reset() {
76
+ return this.#hookRegistry.reset();
77
+ }
78
+ /**
79
+ * Get the hook registry.
80
+ *
81
+ * This is only exposed for internal purposes and should not be accessed in consumer projects.
82
+ * @dangerous
83
+ * @internal
84
+ */
85
+ __getRegistry() {
86
+ return this.#hookRegistry;
87
+ }
88
+ };
89
+ //#endregion
90
+ //#region src/features/plugins/plugin-state.ts
91
+ /**
92
+ * The states of which a plugin can be.
93
+ */
94
+ let PluginState = /* @__PURE__ */ function(PluginState) {
95
+ PluginState[PluginState["Registering"] = 0] = "Registering";
96
+ PluginState[PluginState["Registered"] = 1] = "Registered";
97
+ PluginState[PluginState["Unavailable"] = 2] = "Unavailable";
98
+ return PluginState;
99
+ }({});
100
+ //#endregion
101
+ //#region src/utils/error.ts
102
+ /**
103
+ * Base class for all OMSS framework errors.
104
+ * Use `instanceof OMSSError` to catch any framework error.
105
+ * Always throw a specific subclass, never this directly.
106
+ * Cause is not standardized and may change frequently.
107
+ */
108
+ var OMSSError = class extends Error {
109
+ /**
110
+ * Create a new OMSSError.
111
+ * @param message - Error message
112
+ * @param options - Additional options for the error
113
+ */
114
+ constructor(message, options) {
115
+ super(message, options);
116
+ this.name = this.constructor.name;
117
+ }
118
+ };
119
+ /**
120
+ * Returned when an error gets Returned in the OMSSServer class.
121
+ *
122
+ * @example
123
+ * return ERR(OMSSServerError('config.name must be a non-empty string', {cause: config}))
124
+ */
125
+ var OMSSServerError = class extends OMSSError {};
126
+ /**
127
+ * Returned during plugin registration or execution.
128
+ *
129
+ * @example
130
+ * return ERR(OMSSPluginError(`Plugin "${name}" is already registered`, { cause: plugin }))
131
+ */
132
+ var OMSSPluginError = class extends OMSSError {};
133
+ /**
134
+ * Returned during resolver registration or ID resolution.
135
+ *
136
+ * @example
137
+ * return ERR(OMSSResolverError(`No resolver found for namespace "xyz"`, { cause: rawId }))
138
+ */
139
+ var OMSSResolverError = class extends OMSSError {};
140
+ /**
141
+ * Returned during provider registration or source fetching.
142
+ *
143
+ * @example
144
+ * return ERR(OMSSProviderError('Provider must have at least one resolver', { cause: provider }))
145
+ */
146
+ var OMSSProviderError = class extends OMSSError {};
147
+ /**
148
+ * Returned when an extractor fails to extract the media from a host.
149
+ *
150
+ * @example
151
+ * return ERR(OMSSExtractor('Failed to extract media from host due to host changes', { cause: html }))
152
+ */
153
+ var OMSSExtractorError = class extends OMSSError {};
154
+ /**
155
+ * Returned when something/several things fail during source gathering.
156
+ *
157
+ * @example
158
+ * return ERR(OMSSSourceGatheringError('Failed to gather sources', { cause: providerResults }))
159
+ */
160
+ var OMSSSourceGatheringError = class extends OMSSError {};
161
+ //#endregion
162
+ //#region src/utils/regexp.ts
163
+ /**
164
+ * Regex for validating namespace names.
165
+ */
166
+ const SAFE_UNIQUE_STRING_PATTERN = "[a-z0-9-]+";
167
+ const SAFE_UNIQUE_STRING = new RegExp(`^${SAFE_UNIQUE_STRING_PATTERN}$`);
168
+ /**
169
+ * Regex for matching media formats by keyword or file extension.
170
+ *
171
+ * Matches:
172
+ * - format keywords (`hls`, `dash`, `mp4`, etc.)
173
+ * - common media file extensions
174
+ * - extensions followed by URL query strings, fragments, paths, or the end of the URL
175
+ */
176
+ const HLS_REGEX = /\bhls\b|\.(?:m3u8|ts)(?:$|[\/?#])/i;
177
+ const MP4_REGEX = /\bmp4\b|\.mp4(?:$|[\/?#])/i;
178
+ const DASH_REGEX = /\bdash\b|\.(?:mpd|m4a)(?:$|[\/?#])/i;
179
+ const MKV_REGEX = /\bmkv\b|\.mkv(?:$|[\/?#])/i;
180
+ const VTT_REGEX = /\bvtt\b|\.vtt(?:$|[\/?#])/i;
181
+ const SRT_REGEX = /\bsrt\b|\.srt(?:$|[\/?#])/i;
182
+ /**
183
+ * Regex for validating a single catalog entry value.
184
+ *
185
+ * A catalog entry must be either exactly `"*"` (wildcard — provider supports
186
+ * all IDs in the namespace) or a safe unique string.
187
+ *
188
+ * Mixing `"*"` with other entries in the same catalog array is not allowed
189
+ * and is validated separately at registration time.
190
+ */
191
+ const CATALOG_ENTRY = new RegExp(`^(?:\\*|${SAFE_UNIQUE_STRING_PATTERN})$`);
192
+ //#endregion
193
+ //#region src/utils/utils.ts
194
+ function OK(value) {
195
+ if (arguments.length === 0) return { ok: true };
196
+ return {
197
+ ok: true,
198
+ value
199
+ };
200
+ }
201
+ /**
202
+ * Convenience factory for a failed result.
203
+ */
204
+ const ERR = (error) => ({
205
+ ok: false,
206
+ error
207
+ });
208
+ /**
209
+ * Validate a string is safe for use as a unique identifier (only lowercase letters, numbers, and hyphens).
210
+ * @param value - The string to validate
211
+ * @param name - The name of the identifier, for error messages
212
+ * @param ErrorType - The error type to return if validation fails
213
+ */
214
+ function validateSafeUniqueString(value, name, ErrorType) {
215
+ if (!SAFE_UNIQUE_STRING.test(value)) return ERR(new ErrorType(`Invalid ${name} "${value}". Expected only letters (lowercase), numbers, and hyphens.`));
216
+ return OK();
217
+ }
218
+ //#endregion
219
+ //#region src/features/plugins/PluginRegistry.ts
220
+ /**
221
+ * Registry responsible for executing and managing OMSS Plugins.
222
+ *
223
+ * Plugins are executed when added.
224
+ */
225
+ var PluginRegistry = class {
226
+ /**
227
+ * States of plugin x
228
+ */
229
+ #states = /* @__PURE__ */ new Map();
230
+ #server;
231
+ /**
232
+ * Registration stack used for circular dependency detection.
233
+ */
234
+ #stack = [];
235
+ constructor(server) {
236
+ this.#server = server;
237
+ }
238
+ /**
239
+ * Adds and runs a plugin with its options.
240
+ *
241
+ * @typeParam T - Plugin options type.
242
+ * @param plugin - The plugin function to register.
243
+ * @param options - Plugin options or a factory function that resolves options.
244
+ */
245
+ async add(plugin, options) {
246
+ const state = this.#states.get(plugin);
247
+ if (state === 0) return ERR(new OMSSPluginError(`Circular plugin dependency detected: ${[...this.#stack, plugin].map((p) => p.name).join(" -> ")}`));
248
+ if (state === 1) return ERR(new OMSSPluginError(`Plugin "${plugin.name}" is already registered`));
249
+ this.#states.set(plugin, 0);
250
+ this.#stack.push(plugin);
251
+ const resolved = typeof options === "function" ? options(this.#server) : options;
252
+ try {
253
+ if (plugin.length === 1) await plugin(this.#server);
254
+ else await plugin(this.#server, resolved);
255
+ this.#states.set(plugin, 1);
256
+ return OK(1);
257
+ } catch (err) {
258
+ this.#states.delete(plugin);
259
+ return ERR(err instanceof OMSSPluginError ? err : new OMSSPluginError(String(err), { cause: err }));
260
+ } finally {
261
+ this.#stack.pop();
262
+ }
263
+ }
264
+ /**
265
+ * Get the current plugin state.
266
+ */
267
+ getState(plugin) {
268
+ return this.#states.get(plugin) ?? 2;
269
+ }
270
+ };
271
+ //#endregion
272
+ //#region src/features/plugins/PluginService.ts
273
+ /**
274
+ * The public API for managing OMSS plugins.
275
+ */
276
+ var PluginService = class {
277
+ #pluginRegistry;
278
+ #hookRegistry;
279
+ #insideBeforePluginRegister = false;
280
+ constructor(omssServer, pluginRegistry, hookRegistry) {
281
+ this.#pluginRegistry = pluginRegistry;
282
+ this.#hookRegistry = hookRegistry;
283
+ }
284
+ /**
285
+ * Registers an OMSS plugin that can take a config into the system, but does not need to.
286
+ * @param plugin - Plugin implementation
287
+ * @param options - Plugin configuration
288
+ */
289
+ async register(plugin, options) {
290
+ if (this.#insideBeforePluginRegister) return ERR(new OMSSPluginError("Plugins cannot be registered during beforePluginRegister"));
291
+ this.#insideBeforePluginRegister = true;
292
+ try {
293
+ await this.#hookRegistry.run("beforePluginRegister", {
294
+ plugin,
295
+ options
296
+ });
297
+ } finally {
298
+ this.#insideBeforePluginRegister = false;
299
+ }
300
+ const result = await this.#pluginRegistry.add(plugin, options);
301
+ if (!result.ok) {
302
+ await this.#hookRegistry.run("pluginRegisterFailed", {
303
+ plugin,
304
+ options,
305
+ error: result.error
306
+ });
307
+ return result;
308
+ }
309
+ await this.#hookRegistry.run("afterPluginRegister", {
310
+ plugin,
311
+ options
312
+ });
313
+ return result;
314
+ }
315
+ /**
316
+ * Get the current State of a plugin
317
+ * @param plugin - the plugin to get the state from
318
+ * @returns - a value of the PluginState enum
319
+ */
320
+ getPluginState(plugin) {
321
+ return this.#pluginRegistry.getState(plugin);
322
+ }
323
+ };
324
+ //#endregion
325
+ //#region src/features/resolvers/utils.ts
326
+ /**
327
+ * Parses an OMSS ID in the form `namespace:value_1[:value_2[:...]]`.
328
+ */
329
+ function parseOMSSId(id) {
330
+ if (/\s/.test(id)) return ERR(new OMSSResolverError(`Invalid OMSS ID "${id}": cannot contain whitespace`));
331
+ const parts = id.split(":");
332
+ if (parts.length < 2) return ERR(new OMSSResolverError(`Invalid OMSS ID "${id}": missing namespace separator ":"`));
333
+ const [namespace, ...values] = parts;
334
+ if (!namespace) return ERR(new OMSSResolverError(`Invalid OMSS ID "${id}": namespace cannot be empty`));
335
+ const req = validateSafeUniqueString(namespace, "OMSS namespace", OMSSResolverError);
336
+ if (!req.ok) return ERR(req.error);
337
+ for (const [i, value] of values.entries()) if (value.length === 0) return ERR(new OMSSResolverError(`Invalid OMSS ID "${id}": value ${i + 1} cannot be empty`));
338
+ return OK({
339
+ namespace,
340
+ values: values.map((value) => decodeURIComponent(value)),
341
+ raw: id
342
+ });
343
+ }
344
+ //#endregion
345
+ //#region src/features/providers/ProviderResultEmitter.ts
346
+ /**
347
+ * Creates a fresh `ProviderResultEmitter` instance scoped to a single
348
+ * `getSources()` execution.
349
+ *
350
+ * A new emitter MUST be created per provider call. Emitters hold internal,
351
+ * mutable state (accumulated sources/subtitles/errors) via closures, so
352
+ * reusing a single emitter across concurrent provider executions would
353
+ * cause data from one provider to leak into another's response.
354
+ *
355
+ * @param provider - The provider instance for which this emitter is being created.
356
+ * @param hookReg - The hook registry instance for managing hooks.
357
+ * @param cleaningFunc - A function to clean up source/subtitle URLs and headers.
358
+ * @param id - The parsed OMSS ID of the current request.
359
+ * @returns A new `ProviderResultEmitter` bound to this execution.
360
+ *
361
+ */
362
+ function createProviderResultEmitter(provider, hookReg, cleaningFunc, id) {
363
+ /**
364
+ * Accumulated sources emitted via `source()` during this execution.
365
+ * Flushed into the final result when `done()` is called.
366
+ */
367
+ const sources = [];
368
+ /**
369
+ * Accumulated subtitles emitted via `subtitle()` during this execution.
370
+ * Subtitles are intentionally NOT linked to any source or audio track,
371
+ * so they live in their own flat array regardless of how many sources
372
+ * were emitted.
373
+ */
374
+ const subtitles = [];
375
+ /**
376
+ * Accumulated NON-fatal errors emitted via `error()` during this execution.
377
+ * These are returned alongside successful results (via `done()`) so
378
+ * that partial failures (e.g. "server 2 of 3 failed") don't discard
379
+ * otherwise-valid sources.
380
+ */
381
+ const errors = [];
382
+ return {
383
+ /**
384
+ * Utilities to make your life easier
385
+ */
386
+ utils: {
387
+ /**
388
+ * Utilities for parsing metadata of sources
389
+ */
390
+ source: {
391
+ /**
392
+ * Parse a string into a possible source type.
393
+ * @param possibleType - The string to parse
394
+ */
395
+ parseType(possibleType) {
396
+ if (HLS_REGEX.test(possibleType)) return "hls";
397
+ else if (MP4_REGEX.test(possibleType)) return "mp4";
398
+ else if (DASH_REGEX.test(possibleType)) return "dash";
399
+ else if (MKV_REGEX.test(possibleType)) return "mkv";
400
+ else return "hls";
401
+ },
402
+ /**
403
+ * Parse a string into a possible source quality.
404
+ * @param possibleQuality - The string to parse
405
+ */
406
+ parseQuality(possibleQuality) {
407
+ if (!possibleQuality) return "Auto";
408
+ const value = possibleQuality.toLowerCase().trim().replace(/_/g, " ").replace(/-/g, " ");
409
+ if (/\b(8k|uhd\s*8k|4320p)\b/.test(value)) return "8K";
410
+ if (/\b(4k|uhd|ultra\s*hd|2160p)\b/.test(value)) return "4K";
411
+ if (/\b(qhd|2k|1440p|2560x1440)\b/.test(value)) return "QHD";
412
+ if (/\b(fhd|full\s*hd|1080p|1920x1080)\b/.test(value)) return "FHD";
413
+ if (/\b(hd|720p|1280x720)\b/.test(value)) return "HD";
414
+ if (/\b(sd|480p|576p|360p|240p)\b/.test(value)) return "SD";
415
+ const resolutionMatch = value.match(/(\d{3,4})(?:p|x\d{3,4})?/);
416
+ if (resolutionMatch) {
417
+ const resolution = Number(resolutionMatch[1]);
418
+ if (resolution >= 4320) return "8K";
419
+ if (resolution >= 2160) return "4K";
420
+ if (resolution >= 1440) return "QHD";
421
+ if (resolution >= 1080) return "FHD";
422
+ if (resolution >= 720) return "HD";
423
+ if (resolution > 0) return "SD";
424
+ }
425
+ const bitrateMatch = value.match(/(\d+(?:\.\d+)?)\s*(mbps|kbps)/);
426
+ if (bitrateMatch) {
427
+ const bitrate = Number(bitrateMatch[1]);
428
+ const mbps = bitrateMatch[2] === "kbps" ? bitrate / 1e3 : bitrate;
429
+ if (mbps >= 25) return "4K";
430
+ if (mbps >= 8) return "FHD";
431
+ if (mbps >= 3) return "HD";
432
+ return "SD";
433
+ }
434
+ return "Auto";
435
+ }
436
+ },
437
+ /**
438
+ * Utilities for parsing metadata of subtitles
439
+ */
440
+ subtitle: {
441
+ /**
442
+ * Parse a string into a possible subtitle format.
443
+ * @param possibleFormat - The string to parse
444
+ */
445
+ parseFormat(possibleFormat) {
446
+ if (VTT_REGEX.test(possibleFormat)) return "vtt";
447
+ else if (SRT_REGEX.test(possibleFormat)) return "srt";
448
+ else return "vtt";
449
+ } }
450
+ },
451
+ /**
452
+ * Emits a custom, provider-defined action/event.
453
+ *
454
+ * @param action - A custom event name (e.g. "cache.hit").
455
+ * @param data - Arbitrary payload associated with the event.
456
+ */
457
+ emit(action, data) {
458
+ if (/\s/.test(action) || Object.keys(this).includes(action)) return;
459
+ hookReg.run(action, {
460
+ data,
461
+ provider,
462
+ id,
463
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
464
+ });
465
+ },
466
+ /**
467
+ * Logs verbose debug information. Intended for development/troubleshooting
468
+ * only and should be stripped or gated behind a debug flag in production.
469
+ *
470
+ * @param args - Values to log, forwarded as-is (same semantics as `console.debug`).
471
+ */
472
+ debug(...args) {
473
+ hookReg.run("debug", {
474
+ provider,
475
+ args,
476
+ id,
477
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
478
+ });
479
+ },
480
+ /**
481
+ * Logs general informational messages about provider execution
482
+ * (e.g. "Fetched media", "Cache miss, fetching from upstream").
483
+ *
484
+ * @param args - Values to log.
485
+ */
486
+ info(...args) {
487
+ hookReg.run("info", {
488
+ provider,
489
+ args,
490
+ id,
491
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
492
+ });
493
+ },
494
+ /**
495
+ * Logs a non-fatal warning. Use this for degraded-but-recoverable
496
+ * situations (e.g. "missing quality metadata, defaulting to Auto").
497
+ *
498
+ * @param args - Values to log.
499
+ */
500
+ warn(...args) {
501
+ hookReg.run("warn", {
502
+ provider,
503
+ args,
504
+ id,
505
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
506
+ });
507
+ },
508
+ /**
509
+ * Records a NON-fatal error. The provider continues executing after
510
+ * calling this — use `fatal()` instead if the provider cannot continue.
511
+ *
512
+ * The error is accumulated and returned to the requestor as part of
513
+ * the `diagnostics`/`errors` field once `done()` is called, allowing
514
+ * partial success (e.g. some sources found despite one upstream
515
+ * server failing).
516
+ *
517
+ * @param error - The error to record. Will be surfaced to the client.
518
+ */
519
+ error(error) {
520
+ errors.push(error);
521
+ hookReg.run("error", {
522
+ provider,
523
+ error,
524
+ id,
525
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
526
+ });
527
+ },
528
+ /**
529
+ * Emits a single resolved source.
530
+ *
531
+ * @param source - The fully-formed source object to emit.
532
+ */
533
+ source(source) {
534
+ const cleanedSource = { ...source };
535
+ const cleaned = cleaningFunc({
536
+ url: cleanedSource.url,
537
+ header: cleanedSource.header
538
+ });
539
+ cleanedSource.url = cleaned.url;
540
+ cleanedSource.header = cleaned.header;
541
+ if ("audioTracks" in cleanedSource && cleanedSource.audioTracks) {
542
+ const [first, ...rest] = cleanedSource.audioTracks;
543
+ cleanedSource.audioTracks = [{
544
+ ...first,
545
+ ...cleaningFunc({
546
+ url: first.url,
547
+ header: first.header
548
+ })
549
+ }, ...rest.map((track) => ({
550
+ ...track,
551
+ ...cleaningFunc({
552
+ url: track.url,
553
+ header: track.header
554
+ })
555
+ }))];
556
+ }
557
+ const fullSource = {
558
+ ...cleanedSource,
559
+ provider: {
560
+ id: provider.id,
561
+ name: provider.name
562
+ }
563
+ };
564
+ sources.push(fullSource);
565
+ hookReg.run("source", {
566
+ provider,
567
+ source: fullSource,
568
+ id,
569
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
570
+ });
571
+ },
572
+ /**
573
+ * Emits a single subtitle track.
574
+ *
575
+ * @param subtitle - The subtitle object to emit.
576
+ */
577
+ subtitle(subtitle) {
578
+ const { url, header } = cleaningFunc({
579
+ url: subtitle.url,
580
+ header: subtitle.header
581
+ });
582
+ subtitle.url = url;
583
+ subtitle.header = header;
584
+ const fullSub = {
585
+ ...subtitle,
586
+ provider: {
587
+ id: provider.id,
588
+ name: provider.name
589
+ }
590
+ };
591
+ subtitles.push(fullSub);
592
+ hookReg.run("subtitle", {
593
+ provider,
594
+ subtitle: fullSub,
595
+ id,
596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
597
+ });
598
+ },
599
+ /**
600
+ * Immediately aborts provider execution with a fatal error.
601
+ *
602
+ * @param error - The fatal error describing why the provider could not proceed.
603
+ * @returns An `ERR` result wrapping the given error.
604
+ */
605
+ fatal(error) {
606
+ const accumulatedError = new AggregateError([error, ...errors], error.message, { cause: error.cause });
607
+ const finalErr = new OMSSProviderError(accumulatedError.message, { cause: accumulatedError });
608
+ hookReg.run("error", {
609
+ provider,
610
+ error: finalErr,
611
+ id,
612
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
613
+ });
614
+ return ERR(finalErr);
615
+ },
616
+ /**
617
+ * Signals that the provider has finished emitting sources/subtitles
618
+ * and finalizes the result.
619
+ *
620
+ * @returns An `OK` result containing all accumulated sources,
621
+ * subtitles, and non-fatal errors for this execution.
622
+ */
623
+ done() {
624
+ const result = {
625
+ sources,
626
+ subtitles,
627
+ errors
628
+ };
629
+ hookReg.run("done", {
630
+ provider,
631
+ result,
632
+ id,
633
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
634
+ });
635
+ return OK(result);
636
+ }
637
+ };
638
+ }
639
+ //#endregion
640
+ //#region src/features/source/SourceCore.ts
641
+ /**
642
+ * Internal source gathering core.
643
+ *
644
+ * Handles OMSS ID parsing, provider lookup, resolver-level deduplication,
645
+ * provider execution, and final result aggregation.
646
+ *
647
+ * This class intentionally does not know about OMSS Hooks, middleware, or in-flight
648
+ * request sharing. Those concerns belong to SourceService.
649
+ */
650
+ var SourceCore = class {
651
+ #providerRegistry;
652
+ #extractorService;
653
+ #omssServer;
654
+ constructor(omssServer, providerRegistry, extractorService) {
655
+ this.#omssServer = omssServer;
656
+ this.#providerRegistry = providerRegistry;
657
+ this.#extractorService = extractorService;
658
+ }
659
+ /**
660
+ * Gather sources for a single OMSS ID.
661
+ *
662
+ * @param omssId - Raw OMSS identifier.
663
+ * @param opts - Optional source gathering parameters.
664
+ * @param providerHookRegistry - Hook registry for provider hooks.
665
+ * @param cleaningFunction - Optional custom function to clean url's and headers
666
+ * @returns Aggregated provider results or a source gathering error.
667
+ */
668
+ async getSources(omssId, opts, providerHookRegistry, cleaningFunction) {
669
+ const parsed = parseOMSSId(omssId);
670
+ if (!parsed.ok) return ERR(new OMSSSourceGatheringError(`Failed to parse OMSS id "${omssId}": ${parsed.error.message}`, { cause: parsed.error }));
671
+ const providers = opts.providerId ? this.#providerRegistry.getAll((p) => p.id === opts.providerId && p.resolver.namespace === parsed.value.namespace) : this.#providerRegistry.getAll((p) => p.resolver.namespace === parsed.value.namespace);
672
+ if (providers.length === 0) return ERR(new OMSSSourceGatheringError(`No providers found for namespace "${parsed.value.namespace}"` + (opts.providerId ? ` and provider "${opts.providerId}"` : "")));
673
+ const signal = opts.abortSignal ?? new AbortController().signal;
674
+ const ctx = {
675
+ server: this.#omssServer,
676
+ signal
677
+ };
678
+ /**
679
+ * Resolver-level deduplication: multiple providers that share the same
680
+ * resolver run that resolver only once, then share the result.
681
+ */
682
+ const resolverCache = /* @__PURE__ */ new Map();
683
+ /**
684
+ * Get resolver metadata for a provider, reusing the same resolver
685
+ * promise when multiple providers share the same resolver.
686
+ *
687
+ * @param provider - Provider whose resolver metadata should be loaded.
688
+ * @returns Resolver metadata or a source gathering error.
689
+ */
690
+ const getResolvedMeta = (provider) => {
691
+ const resolverKey = `${provider.resolver.namespace}:${provider.resolver.name}`;
692
+ let promise = resolverCache.get(resolverKey);
693
+ if (!promise) {
694
+ promise = (async () => {
695
+ if (signal.aborted) return ERR(new OMSSSourceGatheringError("Operation aborted"));
696
+ const result = await provider.resolver.resolve(parsed.value, ctx);
697
+ if (!result.ok) return ERR(new OMSSSourceGatheringError(`Resolver failed for ${resolverKey}: ${result.error.message}`, { cause: result.error }));
698
+ return OK(result.value);
699
+ })();
700
+ resolverCache.set(resolverKey, promise);
701
+ }
702
+ return promise;
703
+ };
704
+ /**
705
+ * Resolve sources for a single provider.
706
+ *
707
+ * @param provider - Provider to execute.
708
+ * @returns Provider result or a source gathering error.
709
+ */
710
+ const resolveForProvider = async (provider) => {
711
+ if (signal.aborted) return ERR(new OMSSSourceGatheringError("Operation aborted"));
712
+ if (!await provider.supportsId(parsed.value)) return OK({
713
+ sources: [],
714
+ subtitles: [],
715
+ errors: [new OMSSProviderError(`Provider "${provider.id}" did not support this id: "${parsed.value.raw}"`)]
716
+ });
717
+ if (signal.aborted) return ERR(new OMSSSourceGatheringError("Operation aborted"));
718
+ const metaResult = await getResolvedMeta(provider);
719
+ if (!metaResult.ok) return metaResult;
720
+ if (signal.aborted) return ERR(new OMSSSourceGatheringError("Operation aborted"));
721
+ const resultEmitter = createProviderResultEmitter(provider, providerHookRegistry.__getRegistry(), cleaningFunction, parsed.value);
722
+ return provider.getSources({
723
+ utils: {
724
+ omssId: parsed.value,
725
+ abortSignal: signal,
726
+ findExtractor: this.#extractorService.find
727
+ },
728
+ meta: metaResult.value
729
+ }, resultEmitter);
730
+ };
731
+ const settled = await Promise.allSettled(providers.map((provider) => resolveForProvider(provider)));
732
+ if (signal.aborted) return ERR(new OMSSSourceGatheringError("Operation aborted"));
733
+ const allSources = [];
734
+ const allSubtitles = [];
735
+ const unexpectedErrors = [];
736
+ const omssErrors = [];
737
+ let hasSuccess = false;
738
+ for (const item of settled) {
739
+ if (item.status === "rejected") {
740
+ omssErrors.push(new OMSSSourceGatheringError(`Provider execution failed: ${item.reason instanceof Error ? item.reason.message : String(item.reason)}`, { cause: item.reason instanceof Error ? item.reason : void 0 }));
741
+ unexpectedErrors.push(item.reason);
742
+ continue;
743
+ }
744
+ const res = item.value;
745
+ if (res.ok) {
746
+ hasSuccess = true;
747
+ allSources.push(...res.value.sources);
748
+ allSubtitles.push(...res.value.subtitles);
749
+ omssErrors.push(...res.value.errors);
750
+ continue;
751
+ }
752
+ omssErrors.push(res.error);
753
+ }
754
+ if (!hasSuccess) return ERR(new OMSSSourceGatheringError(`All providers failed for namespace "${parsed.value.namespace}" and id: "${parsed.value.raw}"`, { cause: new AggregateError([...omssErrors, ...unexpectedErrors], "Multiple failures detected") }));
755
+ return OK({
756
+ sources: allSources,
757
+ subtitles: allSubtitles,
758
+ errors: omssErrors
759
+ });
760
+ }
761
+ };
762
+ //#endregion
763
+ //#region src/utils/AsyncDeduper.ts
764
+ /**
765
+ * Generic helper for deduplicating concurrent asynchronous work by key.
766
+ *
767
+ * When the same key is requested multiple times while a request is still in
768
+ * flight, all callers receive the same Promise. Once the Promise settles, the
769
+ * key is removed automatically so the next request starts fresh.
770
+ *
771
+ * @typeParam TKey - Cache key type.
772
+ * @typeParam TValue - Promise resolution type.
773
+ */
774
+ var AsyncDeduper = class {
775
+ #entries = /* @__PURE__ */ new Map();
776
+ /**
777
+ * Returns the current in-flight Promise for a key, if one exists.
778
+ *
779
+ * @param key - In-flight request key.
780
+ * @returns Existing Promise or undefined.
781
+ */
782
+ get(key) {
783
+ return this.#entries.get(key);
784
+ }
785
+ /**
786
+ * Check whether a key currently has an in-flight Promise.
787
+ *
788
+ * @param key - In-flight request key.
789
+ * @returns True if the key is currently in flight.
790
+ */
791
+ has(key) {
792
+ return this.#entries.has(key);
793
+ }
794
+ /**
795
+ * Delete a key manually.
796
+ *
797
+ * @param key - In-flight request key.
798
+ * @returns True if an entry existed and was removed.
799
+ */
800
+ delete(key) {
801
+ return this.#entries.delete(key);
802
+ }
803
+ /**
804
+ * Clear all tracked in-flight requests.
805
+ */
806
+ clear() {
807
+ this.#entries.clear();
808
+ }
809
+ /**
810
+ * Run a Promise factory for a key, reusing an existing in-flight Promise
811
+ * when available.
812
+ *
813
+ * @param key - In-flight request key.
814
+ * @param factory - Factory that creates the Promise when no request exists yet.
815
+ * @returns Shared or newly created Promise.
816
+ */
817
+ run(key, factory) {
818
+ const existing = this.#entries.get(key);
819
+ if (existing) return existing;
820
+ const promise = factory().finally(() => {
821
+ this.#entries.delete(key);
822
+ });
823
+ this.#entries.set(key, promise);
824
+ return promise;
825
+ }
826
+ };
827
+ //#endregion
828
+ //#region src/utils/middleware.ts
829
+ /**
830
+ * Reusable typed middleware runner.
831
+ */
832
+ var MiddlewareRunner = class {
833
+ #handlers = {};
834
+ /**
835
+ * Register middleware for a specific operation.
836
+ *
837
+ * @param method - Operation name.
838
+ * @param handler - Middleware handler.
839
+ */
840
+ use(method, handler) {
841
+ (this.#handlers[method] ??= []).push(handler);
842
+ }
843
+ /**
844
+ * Run middleware chain for a specific operation.
845
+ *
846
+ * @param method - Operation name.
847
+ * @param context - Operation context payload.
848
+ * @param finalHandler - Final function to execute after middleware.
849
+ * @returns The operation result.
850
+ */
851
+ run(method, context, finalHandler) {
852
+ const handlers = this.#handlers[method] ?? [];
853
+ let index = -1;
854
+ const dispatch = (position) => {
855
+ if (position <= index) return Promise.reject(/* @__PURE__ */ new Error("next() called multiple times"));
856
+ index = position;
857
+ const handler = handlers[position];
858
+ if (!handler) return finalHandler();
859
+ return handler(context, () => dispatch(position + 1));
860
+ };
861
+ return dispatch(0);
862
+ }
863
+ };
864
+ //#endregion
865
+ //#region src/features/source/SourceService.ts
866
+ /**
867
+ * Public API for resolving sources for media.
868
+ *
869
+ * This service owns the public method surface, middleware execution,
870
+ * lifecycle hook dispatching, and request coalescing. The actual source
871
+ * gathering implementation lives in {@link SourceCore}.
872
+ */
873
+ var SourceService = class {
874
+ #hookRegistry;
875
+ #core;
876
+ /**
877
+ * Middleware runner for SourceService operations.
878
+ */
879
+ #middleware = new MiddlewareRunner();
880
+ /**
881
+ * Deduplicates concurrent getSources requests by request key.
882
+ */
883
+ #inFlight = new AsyncDeduper();
884
+ constructor(omssServer, providerRegistry, hookRegistry, extractorService) {
885
+ this.#hookRegistry = hookRegistry;
886
+ this.#core = new SourceCore(omssServer, providerRegistry, extractorService);
887
+ }
888
+ get cleaningFunction() {
889
+ return this.#cleaningFunction;
890
+ }
891
+ set cleaningFunction(fn) {
892
+ this.#cleaningFunction = fn;
893
+ }
894
+ /**
895
+ * Register middleware for a SourceService method.
896
+ *
897
+ * Middleware can be used for cross-cutting concerns such as caching,
898
+ * logging, tracing, or metrics.
899
+ *
900
+ * @param method - Middleware-enabled method name.
901
+ * @param handler - Middleware handler.
902
+ */
903
+ use(method, handler) {
904
+ this.#middleware.use(method, handler);
905
+ }
906
+ /**
907
+ * Fetch sources from all matching providers for an OMSS ID.
908
+ *
909
+ * This method is middleware-enabled. Concurrent requests for the same
910
+ * `omssId` and `providerId` share the same in-flight Promise until the
911
+ * request settles.
912
+ *
913
+ * @param omssId - OMSS identifier such as `"tmdb:12345"`.
914
+ * @param options - Optional source gathering parameters.
915
+ * @returns Aggregated provider results or a source gathering error.
916
+ */
917
+ async getSources(omssId, options = {}) {
918
+ return this.#middleware.run("getSources", {
919
+ omssId,
920
+ options
921
+ }, () => this.#internalGetSources(omssId, options));
922
+ }
923
+ /**
924
+ * Get and set the cleaning function for the source core.
925
+ */
926
+ #cleaningFunction = (obj) => obj;
927
+ /**
928
+ * Internal wrapper around source gathering.
929
+ *
930
+ * Runs lifecycle hooks and deduplicates concurrent requests before
931
+ * delegating to {@link SourceCore}.
932
+ *
933
+ * @param omssId - OMSS identifier.
934
+ * @param options - Optional source gathering parameters.
935
+ * @returns Aggregated provider results or a source gathering error.
936
+ */
937
+ async #internalGetSources(omssId, options) {
938
+ await this.#hookRegistry.run("beforeGetSources", {
939
+ omssId,
940
+ providerId: options.providerId
941
+ });
942
+ const inFlightKey = this.#getInFlightKey(omssId, options.providerId);
943
+ const result = await this.#inFlight.run(inFlightKey, () => this.#core.getSources(omssId, options, options.providerHookService ?? new HookService(), options.cleaningFunction ?? this.cleaningFunction));
944
+ if (result.ok) {
945
+ await this.#hookRegistry.run("afterGetSources", {
946
+ omssId,
947
+ providerId: options.providerId,
948
+ result: result.value
949
+ });
950
+ return this.#middleware.run("afterGetSources", {
951
+ omssId,
952
+ options,
953
+ result
954
+ }, () => Promise.resolve(result));
955
+ }
956
+ await this.#hookRegistry.run("getSourcesFailed", {
957
+ omssId,
958
+ providerId: options.providerId,
959
+ error: result.error
960
+ });
961
+ return result;
962
+ }
963
+ /**
964
+ * Build the stable in-flight key for a getSources request.
965
+ *
966
+ * @param omssId - OMSS identifier.
967
+ * @param providerId - Optional provider filter.
968
+ * @returns Unique in-flight request key.
969
+ */
970
+ #getInFlightKey(omssId, providerId) {
971
+ return `${omssId}|${providerId ?? ""}`;
972
+ }
973
+ };
974
+ //#endregion
975
+ //#region src/features/providers/ProviderRegistry.ts
976
+ /**
977
+ * Registry responsible for storing and managing OMSS Providers.
978
+ *
979
+ * Providers are stored by their unique {@link UnknownProvider.id}.
980
+ * This registry does not know about hooks.
981
+ */
982
+ var ProviderRegistry = class {
983
+ /**
984
+ * Internal map of registered providers, keyed by provider ID.
985
+ */
986
+ #providers = /* @__PURE__ */ new Map();
987
+ /**
988
+ * Adds a provider to the registry.
989
+ *
990
+ * @param provider - The provider instance to register.
991
+ * @returns `OK` if registration succeeded, `ERR` if the provider ID is already taken.
992
+ */
993
+ async add(provider) {
994
+ if (this.#providers.has(provider.id)) return ERR(new OMSSProviderError(`Provider "${provider.id}" is already registered`));
995
+ const valProvIdReq = validateSafeUniqueString(provider.id, "provider ID", OMSSProviderError);
996
+ if (!valProvIdReq.ok) return ERR(valProvIdReq.error);
997
+ const valResolverNamespaceReq = validateSafeUniqueString(provider.resolver.namespace, "resolver namespace for resolver with name: \"" + provider.resolver.name + "\" and id:", OMSSProviderError);
998
+ if (!valResolverNamespaceReq.ok) return ERR(valResolverNamespaceReq.error);
999
+ if (this.getAll().some((p) => p.resolver.namespace === provider.resolver.namespace && p.resolver !== provider.resolver)) return ERR(new OMSSProviderError(`Resolver namespace "${provider.resolver.namespace}" is already registered by another provider and another resolver. Use one resolver per namespace.`));
1000
+ if (provider.catalog) {
1001
+ const entries = await provider.catalog();
1002
+ const hasWildcard = entries.includes("*");
1003
+ const hasOther = entries.some((id) => id !== "*");
1004
+ if (hasWildcard && hasOther) return ERR(new OMSSProviderError(`Provider "${provider.id}" catalog contains "*" mixed with other IDs. Use either a single "*" or a list of valid IDs.`));
1005
+ for (const id of entries) if (!CATALOG_ENTRY.test(id)) return ERR(new OMSSProviderError(`Provider "${provider.id}" catalog contains an invalid entry "${id}". Entries must be non-empty strings with no whitespace, or "*".`));
1006
+ }
1007
+ this.#providers.set(provider.id, provider);
1008
+ return OK(provider);
1009
+ }
1010
+ /**
1011
+ * Retrieves a registered provider by its ID.
1012
+ *
1013
+ * @param id - The unique provider ID.
1014
+ * @returns The provider instance, or `undefined` if not found.
1015
+ */
1016
+ get(id) {
1017
+ return this.#providers.get(id);
1018
+ }
1019
+ /**
1020
+ * Returns all registered providers.
1021
+ */
1022
+ getAll(filter) {
1023
+ if (filter) return [...this.#providers.values()].filter(filter);
1024
+ return [...this.#providers.values()];
1025
+ }
1026
+ /**
1027
+ * Returns whether a provider with the given ID is registered.
1028
+ *
1029
+ * @param id - The unique provider ID.
1030
+ */
1031
+ has(id) {
1032
+ return this.#providers.has(id);
1033
+ }
1034
+ };
1035
+ //#endregion
1036
+ //#region src/features/providers/ProviderService.ts
1037
+ /**
1038
+ * The public API for managing OMSS Providers.
1039
+ */
1040
+ var ProviderService = class {
1041
+ #providerRegistry;
1042
+ #hookRegistry;
1043
+ #middleware = new MiddlewareRunner();
1044
+ #insideBeforeProviderRegister = false;
1045
+ constructor(providerRegistry, hookRegistry) {
1046
+ this.#providerRegistry = providerRegistry;
1047
+ this.#hookRegistry = hookRegistry;
1048
+ }
1049
+ /**
1050
+ * Adds middleware to the `register` pipeline.
1051
+ * Middlewares run in insertion order, after hooks and before the
1052
+ * actual registry `add()` call.
1053
+ */
1054
+ use(method, handler) {
1055
+ this.#middleware.use(method, handler);
1056
+ return this;
1057
+ }
1058
+ /**
1059
+ * Registers a provider into the system.
1060
+ */
1061
+ async register(provider) {
1062
+ if (this.#insideBeforeProviderRegister) return ERR(new OMSSProviderError("Providers cannot be registered during beforeProviderRegister"));
1063
+ this.#insideBeforeProviderRegister = true;
1064
+ try {
1065
+ await this.#hookRegistry.run("beforeProviderRegister", { provider });
1066
+ } finally {
1067
+ this.#insideBeforeProviderRegister = false;
1068
+ }
1069
+ const result = await this.#middleware.run("register", { provider }, () => this.#providerRegistry.add(provider));
1070
+ if (!result.ok) {
1071
+ await this.#hookRegistry.run("providerRegisterFailed", {
1072
+ provider,
1073
+ error: result.error
1074
+ });
1075
+ return ERR(result.error);
1076
+ }
1077
+ await this.#hookRegistry.run("afterProviderRegister", { provider });
1078
+ return result;
1079
+ }
1080
+ /**
1081
+ * Retrieves a registered provider by its ID.
1082
+ *
1083
+ * @param id - The provider ID to look up.
1084
+ * @returns The provider instance, or `undefined` if not found.
1085
+ */
1086
+ get(id) {
1087
+ return this.#providerRegistry.get(id);
1088
+ }
1089
+ /**
1090
+ * Returns all registered providers.
1091
+ * @param filter - Optional filter function to apply to providers.
1092
+ */
1093
+ getAll(filter) {
1094
+ return this.#providerRegistry.getAll(filter);
1095
+ }
1096
+ /**
1097
+ * Returns whether a provider with the given ID has been registered.
1098
+ *
1099
+ * @param id - The provider ID to check.
1100
+ */
1101
+ has(id) {
1102
+ return this.#providerRegistry.has(id);
1103
+ }
1104
+ /**
1105
+ * Returns a map of all namespaces and an array of all known identifiers for each namespace.
1106
+ * This list is BEST EFFORT ONLY. Do not rely on this. If any provider returns a `*` automatically, the namespace will support all identifiers (e.g. `"tmdb": ["*"], "imdb": ["tt37636", "..."]`.
1107
+ */
1108
+ async catalog() {
1109
+ const allProviders = this.getAll();
1110
+ const result = /* @__PURE__ */ new Map();
1111
+ await Promise.all(allProviders.map(async (provider) => {
1112
+ if (!provider.catalog) return;
1113
+ const namespace = provider.resolver.namespace;
1114
+ const entries = await provider.catalog();
1115
+ if (result.get(namespace)?.[0] === "*") return;
1116
+ if (entries.includes("*")) {
1117
+ result.set(namespace, ["*"]);
1118
+ return;
1119
+ }
1120
+ const existing = result.get(namespace) ?? [];
1121
+ const merged = Array.from(/* @__PURE__ */ new Set([...existing, ...entries]));
1122
+ result.set(namespace, merged);
1123
+ }));
1124
+ return OK(result);
1125
+ }
1126
+ /**
1127
+ * Returns the catalog for a single namespace.
1128
+ * Returns `undefined` if no provider in that namespace exposes a catalog.
1129
+ *
1130
+ * @param namespace - The resolver namespace to look up (e.g. `"tmdb"`).
1131
+ * @returns Merged list of IDs for the namespace, `["*"]` if any provider
1132
+ * signals wildcard support, or `undefined` if no catalog data exists.
1133
+ */
1134
+ async catalogForNamespace(namespace) {
1135
+ const providers = this.getAll((p) => p.resolver.namespace === namespace);
1136
+ if (providers.length === 0) return ERR(new OMSSProviderError(`No providers registered for namespace "${namespace}"`));
1137
+ const result = [];
1138
+ for (const provider of providers) {
1139
+ if (!provider.catalog) continue;
1140
+ const entries = await provider.catalog();
1141
+ if (entries.includes("*")) return OK(["*"]);
1142
+ for (const id of entries) if (!result.includes(id)) result.push(id);
1143
+ }
1144
+ return result.length > 0 ? OK(result) : ERR(new OMSSProviderError(`No catalog data available for namespace "${namespace}"`));
1145
+ }
1146
+ };
1147
+ //#endregion
1148
+ //#region src/features/extractors/ExtractorService.ts
1149
+ var ExtractorService = class {
1150
+ #extractorRegistry;
1151
+ #hookRegistry;
1152
+ #insideBeforeRegisterExtractor = false;
1153
+ constructor(extractorRegistry, hookRegistry) {
1154
+ this.#extractorRegistry = extractorRegistry;
1155
+ this.#hookRegistry = hookRegistry;
1156
+ }
1157
+ /**
1158
+ * Get all registered extractors (read-only).
1159
+ */
1160
+ get extractors() {
1161
+ return OK(this.#extractorRegistry.extractors);
1162
+ }
1163
+ /**
1164
+ * Find an extractor capable of handling the given URL.
1165
+ *
1166
+ * @param url - URL to search for.
1167
+ * @returns The first matching {@link Extractor} or an {@link OMSSExtractorError}.
1168
+ */
1169
+ async find(url) {
1170
+ await this.#hookRegistry.run("beforeFindExtractor", { url });
1171
+ const extractors = this.#extractorRegistry.extractors;
1172
+ const results = await Promise.all(extractors.map((extractor) => extractor.matcher(url)));
1173
+ for (let i = 0; i < extractors.length; i++) if (results[i].ok) {
1174
+ const extractor = extractors[i];
1175
+ await this.#hookRegistry.run("afterFindExtractor", {
1176
+ url,
1177
+ extractor
1178
+ });
1179
+ return OK(extractor);
1180
+ }
1181
+ const error = new OMSSExtractorError(`No extractor found for URL "${url}"`);
1182
+ await this.#hookRegistry.run("findExtractorFailed", {
1183
+ url,
1184
+ error
1185
+ });
1186
+ return ERR(error);
1187
+ }
1188
+ /**
1189
+ * Register an extractor.
1190
+ *
1191
+ * @param extractor - Extractor to register.
1192
+ */
1193
+ async register(extractor) {
1194
+ if (this.#insideBeforeRegisterExtractor) return ERR(new OMSSExtractorError("Extractors cannot be registered during beforeRegisterExtractor"));
1195
+ this.#insideBeforeRegisterExtractor = true;
1196
+ try {
1197
+ await this.#hookRegistry.run("beforeRegisterExtractor", { extractor });
1198
+ } finally {
1199
+ this.#insideBeforeRegisterExtractor = false;
1200
+ }
1201
+ try {
1202
+ this.#extractorRegistry.add(extractor);
1203
+ } catch (error) {
1204
+ const extractorError = error instanceof OMSSExtractorError ? error : new OMSSExtractorError(error instanceof Error ? error.message : String(error));
1205
+ await this.#hookRegistry.run("extractorRegisterFailed", {
1206
+ extractor,
1207
+ error: extractorError
1208
+ });
1209
+ return ERR(extractorError);
1210
+ }
1211
+ await this.#hookRegistry.run("afterRegisterExtractor", { extractor });
1212
+ return OK();
1213
+ }
1214
+ /**
1215
+ * Remove every registered extractor.
1216
+ */
1217
+ reset() {
1218
+ this.#extractorRegistry.reset();
1219
+ return OK();
1220
+ }
1221
+ /**
1222
+ * Determine whether an extractor has already been registered.
1223
+ *
1224
+ * @param extractor - Extractor to check.
1225
+ */
1226
+ has(extractor) {
1227
+ return OK(this.#extractorRegistry.has(extractor));
1228
+ }
1229
+ /**
1230
+ * Remove an extractor.
1231
+ *
1232
+ * @param extractor - Extractor to remove.
1233
+ * @returns Whether the extractor was removed.
1234
+ */
1235
+ remove(extractor) {
1236
+ return OK(this.#extractorRegistry.remove(extractor));
1237
+ }
1238
+ };
1239
+ //#endregion
1240
+ //#region src/features/extractors/ExtractorRegistry.ts
1241
+ /**
1242
+ * Extractor Registry
1243
+ *
1244
+ * Manages all extractors.
1245
+ */
1246
+ var ExtractorRegistry = class {
1247
+ /**
1248
+ * Array of extractors.
1249
+ */
1250
+ #extractors = Array();
1251
+ /**
1252
+ * Get all extractors.
1253
+ */
1254
+ get extractors() {
1255
+ return this.#extractors;
1256
+ }
1257
+ /**
1258
+ * Clear all extractors.
1259
+ */
1260
+ reset() {
1261
+ this.#extractors.length = 0;
1262
+ }
1263
+ /**
1264
+ * Add an extractor.
1265
+ * @param extractor - Extractor to add.
1266
+ */
1267
+ add(extractor) {
1268
+ if (this.#extractors.some((e) => e === extractor)) return;
1269
+ this.#extractors.push(extractor);
1270
+ }
1271
+ /**
1272
+ * Check if an extractor is already registered.
1273
+ * @param extractor - Extractor to check.
1274
+ */
1275
+ has(extractor) {
1276
+ return this.#extractors.includes(extractor);
1277
+ }
1278
+ /**
1279
+ * Remove an extractor.
1280
+ * @param extractor - Extractor to remove.
1281
+ * @returns True if the extractor was removed, false otherwise.
1282
+ */
1283
+ remove(extractor) {
1284
+ if (!this.has(extractor)) return false;
1285
+ this.#extractors.splice(this.#extractors.indexOf(extractor), 1);
1286
+ return true;
1287
+ }
1288
+ };
1289
+ //#endregion
1290
+ //#region src/core/server.ts
1291
+ /**
1292
+ * Core server class for OMSS.
1293
+ */
1294
+ var OMSSServer = class {
1295
+ hooks;
1296
+ plugins;
1297
+ providers;
1298
+ sources;
1299
+ extractors;
1300
+ #config;
1301
+ /**
1302
+ * Creates a new OMSSServer instance.
1303
+ *
1304
+ * @param config - Immutable server configuration
1305
+ */
1306
+ constructor(config) {
1307
+ this.#config = config;
1308
+ const hooksRegistry = new HookRegistry();
1309
+ const extractorRegistry = new ExtractorRegistry();
1310
+ const pluginRegistry = new PluginRegistry(this);
1311
+ const providerRegistry = new ProviderRegistry();
1312
+ this.hooks = new HookService(hooksRegistry);
1313
+ this.extractors = new ExtractorService(extractorRegistry, hooksRegistry);
1314
+ this.plugins = new PluginService(this, pluginRegistry, hooksRegistry);
1315
+ this.providers = new ProviderService(providerRegistry, hooksRegistry);
1316
+ this.sources = new SourceService(this, providerRegistry, hooksRegistry, this.extractors);
1317
+ }
1318
+ /**
1319
+ * Get the OMSS Config from the constructor
1320
+ * @returns the initialised OMSS Config
1321
+ */
1322
+ get config() {
1323
+ return this.#config;
1324
+ }
1325
+ /**
1326
+ * Decorate the OMSSServer instance with a new property.
1327
+ * @param name - The name of the property to be decorated.
1328
+ * @param value - The value to be assigned to the property.
1329
+ * @param deps - An array of dependency names.
1330
+ * @returns The name of the decorated property in the {@link Result} object.
1331
+ */
1332
+ decorate(name, value, deps = []) {
1333
+ if (Object.hasOwn(this, name)) return ERR(new OMSSServerError(`Decorator "${name}" already exists`, { cause: { existing: this[name] } }));
1334
+ for (const dep of deps) if (!this.hasDecorator(dep)) return ERR(new OMSSServerError(`"${name}" depends on "${dep}", which does not exist`));
1335
+ Object.defineProperty(this, name, {
1336
+ value,
1337
+ writable: false,
1338
+ configurable: false,
1339
+ enumerable: true
1340
+ });
1341
+ return OK(name);
1342
+ }
1343
+ /**
1344
+ * Check if a decorator with the given name exists.
1345
+ * @param name - The name of the decorator to check.
1346
+ * @returns True if the decorator exists, false otherwise.
1347
+ */
1348
+ hasDecorator(name) {
1349
+ return Object.hasOwn(this, name);
1350
+ }
1351
+ /**
1352
+ * Get a decorated property by its name.
1353
+ * @param name - The name of the property to retrieve.
1354
+ * @returns The decorated property value in the {@link Result} object.
1355
+ */
1356
+ getDecorator(name) {
1357
+ if (!Object.hasOwn(this, name)) return ERR(new OMSSServerError(`Decorator "${name}" not found`));
1358
+ return OK(this[name]);
1359
+ }
1360
+ };
1361
+ //#endregion
1362
+ //#region src/features/providers/BaseProvider.ts
1363
+ /**
1364
+ * Base class for all providers.
1365
+ */
1366
+ var BaseProvider = class {};
1367
+ //#endregion
1368
+ //#region src/features/resolvers/BaseResolver.ts
1369
+ /**
1370
+ * Base class for all OMSS resolvers.
1371
+ *
1372
+ * Resolvers convert an OMSS ID into usable metadata for providers.
1373
+ */
1374
+ var BaseResolver = class {};
1375
+ //#endregion
1376
+ export { BaseProvider, BaseResolver, ERR, HookRegistry, HookService, OK, OMSSError, OMSSExtractorError, OMSSPluginError, OMSSProviderError, OMSSResolverError, OMSSServer, OMSSServerError, OMSSSourceGatheringError, PluginState, SAFE_UNIQUE_STRING, parseOMSSId, validateSafeUniqueString };
1377
+
1378
+ //# sourceMappingURL=index.mjs.map