feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

228
src/presets/user-presets.ts Normal file
View File

@@ -0,0 +1,228 @@
import type { UserExportPreset } from './preset.types';
import {
parseUserPresetJson,
serializeUserPreset,
validateUserPreset,
} from './preset-validation';
export interface UserPresetPersistence {
load(): Promise<readonly UserExportPreset[]>;
save(presets: readonly UserExportPreset[]): Promise<void>;
}
export class UserPresetRegistry {
readonly #persistence: UserPresetPersistence;
#presets = new Map<string, UserExportPreset>();
constructor(persistence: UserPresetPersistence) {
this.#persistence = persistence;
}
async initialize(): Promise<void> {
const loaded = await this.#persistence.load();
const validated = loaded.map((preset) => {
const result = validateUserPreset(preset);
if (!result.valid || !result.preset) {
throw new TypeError(
`Stored preset is invalid: ${result.errors.join('; ')}`
);
}
return result.preset;
});
this.#presets = new Map(validated.map((preset) => [preset.id, preset]));
}
list(): readonly UserExportPreset[] {
return Object.freeze(
[...this.#presets.values()].sort((left, right) =>
left.name.localeCompare(right.name)
)
);
}
get(id: string): UserExportPreset | undefined {
return this.#presets.get(id);
}
async save(preset: UserExportPreset): Promise<void> {
const result = validateUserPreset(preset);
if (!result.valid || !result.preset) {
throw new TypeError(`Invalid preset: ${result.errors.join('; ')}`);
}
this.#presets.set(result.preset.id, result.preset);
await this.#persist();
}
async clone(
id: string,
newId: string,
newName: string
): Promise<UserExportPreset> {
const original = this.#presets.get(id);
if (!original) {
throw new RangeError(`Unknown preset: ${id}`);
}
const cloned = { ...original, id: newId, name: newName };
await this.save(cloned);
return this.#presets.get(newId) as UserExportPreset;
}
async rename(id: string, name: string): Promise<void> {
const preset = this.#presets.get(id);
if (!preset) {
throw new RangeError(`Unknown preset: ${id}`);
}
await this.save({ ...preset, name });
}
async delete(id: string): Promise<boolean> {
const deleted = this.#presets.delete(id);
if (deleted) {
await this.#persist();
}
return deleted;
}
async reset(): Promise<void> {
this.#presets.clear();
await this.#persist();
}
export(id: string): string {
const preset = this.#presets.get(id);
if (!preset) {
throw new RangeError(`Unknown preset: ${id}`);
}
return serializeUserPreset(preset);
}
async import(json: string): Promise<UserExportPreset> {
const preset = parseUserPresetJson(json);
await this.save(preset);
return preset;
}
async #persist(): Promise<void> {
await this.#persistence.save(this.list());
}
}
export class MemoryUserPresetPersistence implements UserPresetPersistence {
#presets: readonly UserExportPreset[] = [];
async load(): Promise<readonly UserExportPreset[]> {
return this.#presets;
}
async save(presets: readonly UserExportPreset[]): Promise<void> {
this.#presets = structuredClone(presets);
}
}
export interface IndexedDbUserPresetPersistenceOptions {
readonly databaseName?: string;
readonly storeName?: string;
readonly indexedDB?: IDBFactory;
}
/**
* Stores only validated typed preset documents. A dedicated database avoids
* version races with durable job metadata stores that may be opened first.
*/
export class IndexedDbUserPresetPersistence implements UserPresetPersistence {
readonly #databaseName: string;
readonly #storeName: string;
readonly #factory: IDBFactory;
#database?: Promise<IDBDatabase>;
constructor(options: IndexedDbUserPresetPersistenceOptions = {}) {
const factory = options.indexedDB ?? globalThis.indexedDB;
if (!factory) {
throw new Error('IndexedDB is unavailable');
}
this.#factory = factory;
this.#databaseName = options.databaseName ?? 'av-tools-user-presets-v1';
this.#storeName = options.storeName ?? 'presets';
}
async load(): Promise<readonly UserExportPreset[]> {
const request = (await this.#store('readonly')).get('all');
const value = await requestResult<unknown>(request);
if (value === undefined) {
return Object.freeze([]);
}
if (!Array.isArray(value)) {
throw new TypeError('Stored user presets are invalid');
}
return Object.freeze(
value.map((preset) => {
const result = validateUserPreset(preset);
if (!result.valid || !result.preset) {
throw new TypeError(
`Stored preset is invalid: ${result.errors.join('; ')}`
);
}
return result.preset;
})
);
}
async save(presets: readonly UserExportPreset[]): Promise<void> {
const validated = presets.map((preset) => {
const result = validateUserPreset(preset);
if (!result.valid || !result.preset) {
throw new TypeError(`Invalid preset: ${result.errors.join('; ')}`);
}
return result.preset;
});
const store = await this.#store('readwrite');
const completion = transactionComplete(store.transaction);
await requestResult<IDBValidKey>(
store.put(structuredClone(validated), 'all')
);
await completion;
}
async #store(mode: IDBTransactionMode): Promise<IDBObjectStore> {
const database = await (this.#database ??= this.#open());
return database
.transaction(this.#storeName, mode)
.objectStore(this.#storeName);
}
#open(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = this.#factory.open(this.#databaseName, 1);
request.onupgradeneeded = () => {
if (!request.result.objectStoreNames.contains(this.#storeName)) {
request.result.createObjectStore(this.#storeName);
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () =>
reject(request.error ?? new Error('Could not open preset storage'));
request.onblocked = () =>
reject(new Error('Preset storage upgrade is blocked'));
});
}
}
function requestResult<T>(request: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () =>
reject(request.error ?? new Error('Preset storage request failed'));
});
}
function transactionComplete(transaction: IDBTransaction): Promise<void> {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () =>
reject(
transaction.error ?? new Error('Preset storage transaction failed')
);
transaction.onabort = () =>
reject(transaction.error ?? new Error('Preset storage was aborted'));
});
}