From 112a0b67de43ed1f006c737f0800640a46c5eaf7 Mon Sep 17 00:00:00 2001 From: Izaak Kuipers Date: Tue, 24 Feb 2026 14:03:24 +0100 Subject: [PATCH 1/2] ConfigurationBuilder --- .../apppermissions/AppPermissions.svelte | 2 +- .../AppPermissions/PermissionRow.svelte | 2 +- src/apps/components/iconpicker/runtime.ts | 2 +- .../wallpaper/Wallpaper/DesktopIcon.svelte | 2 +- src/apps/components/wallpaper/runtime.ts | 81 ++--------- .../Virtual/MyArcOS/RecentFiles.svelte | 4 +- src/apps/user/iconeditor/runtime.ts | 8 +- src/apps/user/mediaplayer/runtime.ts | 35 ++--- src/interfaces/config.ts | 5 + src/interfaces/permission.ts | 6 +- src/interfaces/services/IconService.ts | 4 +- src/interfaces/services/MigrationSvc.ts | 2 - src/interfaces/services/RecentFilesSvc.ts | 2 - src/interfaces/services/TrashSvc.ts | 4 +- src/ts/config.ts | 135 ++++++++++++++++++ src/ts/permissions/index.ts | 79 ++++------ .../services/FileAssocSvc/index.ts | 50 ++----- .../servicehost/services/IconService/index.ts | 47 ++---- .../services/MigrationSvc/index.ts | 51 ++----- .../services/RecentFilesSvc/index.ts | 50 ++----- src/ts/servicehost/services/TrashSvc/index.ts | 31 ++-- 21 files changed, 270 insertions(+), 332 deletions(-) create mode 100644 src/interfaces/config.ts create mode 100644 src/ts/config.ts diff --git a/src/apps/components/apppermissions/AppPermissions.svelte b/src/apps/components/apppermissions/AppPermissions.svelte index 599b9349..06ae103b 100644 --- a/src/apps/components/apppermissions/AppPermissions.svelte +++ b/src/apps/components/apppermissions/AppPermissions.svelte @@ -8,7 +8,7 @@ const { process }: { process: AppPermissionsRuntime } = $props(); const { targetApp } = process; - const { Configuration } = Permissions!; + const { Storage: Configuration } = Permissions!; let permissionId = $state(""); diff --git a/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte b/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte index 11d3379f..6f103934 100644 --- a/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte +++ b/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte @@ -4,7 +4,7 @@ import { onMount } from "svelte"; const { id, permissionId }: { id: PermissionString; permissionId: string } = $props(); - const { Configuration } = Permissions!; + const { Storage: Configuration } = Permissions!; const options = ["Unset", "Allow", "Deny"] as const; type Option = (typeof options)[number]; diff --git a/src/apps/components/iconpicker/runtime.ts b/src/apps/components/iconpicker/runtime.ts index 8bf958c5..71c2aa16 100755 --- a/src/apps/components/iconpicker/runtime.ts +++ b/src/apps/components/iconpicker/runtime.ts @@ -37,7 +37,7 @@ export class IconPickerRuntime extends AppProcess { const iconService = Daemon?.serviceHost?.getService("IconService"); if (!iconService) return false; - this.store = iconService.Configuration(); + this.store = iconService.Icons(); this.groups = iconService.getGroupedIcons(); } diff --git a/src/apps/components/wallpaper/Wallpaper/DesktopIcon.svelte b/src/apps/components/wallpaper/Wallpaper/DesktopIcon.svelte index 01234e5f..93175f34 100755 --- a/src/apps/components/wallpaper/Wallpaper/DesktopIcon.svelte +++ b/src/apps/components/wallpaper/Wallpaper/DesktopIcon.svelte @@ -40,7 +40,7 @@ let movingX = $state(); let movingY = $state(); - const { userPreferences, Configuration, selected, orphaned } = process; + const { userPreferences, Positions: Configuration, selected, orphaned } = process; async function updatePos() { const pos = $Configuration[`icon$${identifier}`] as { diff --git a/src/apps/components/wallpaper/runtime.ts b/src/apps/components/wallpaper/runtime.ts index 5510a5dd..088187f5 100755 --- a/src/apps/components/wallpaper/runtime.ts +++ b/src/apps/components/wallpaper/runtime.ts @@ -1,11 +1,10 @@ import { AppProcess } from "$ts/apps/process"; +import { ConfigurationBuilder } from "$ts/config"; import { Daemon } from "$ts/daemon"; import { Env, Fs, SysDispatch } from "$ts/env"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { MessageBox } from "$ts/util/dialog"; import { getItemNameFromPath, join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import type { AppContextMenu, AppProcessData } from "$types/app"; import type { DirectoryReadReturn } from "$types/fs"; @@ -23,7 +22,14 @@ export class WallpaperRuntime extends AppProcess { orphaned = Store([]); loading = Store(false); directory: string; - Configuration = Store({}); + Positions = Store({}); + Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.Positions) + .WritesTo(this.CONFIG_PATH) + .WithDefaults({}) + .WithCooldown(500) + .Build(); public contextMenu: AppContextMenu = WallpaperContextMenu(this); @@ -45,18 +51,7 @@ export class WallpaperRuntime extends AppProcess { } async start() { - const migrated = await this.migrateDesktopIcons(); - if (!migrated) await this.loadConfiguration(); - - let firstSub = false; - - this.Configuration.subscribe((v) => { - if (!firstSub) { - firstSub = true; - return; - } - this.writeConfiguration(v); - }); + await this.Configuration.initialize(); } async render() { @@ -105,7 +100,7 @@ export class WallpaperRuntime extends AppProcess { findAndDeleteOrphans(contents: DirectoryReadReturn | undefined) { const orphaned = this.orphaned(); - const config = this.Configuration(); + const config = this.Positions(); let orphanedCount = 0; for (const id of Object.keys(config)) { @@ -123,7 +118,7 @@ export class WallpaperRuntime extends AppProcess { } } - if (orphanedCount) this.Configuration.set(config); + if (orphanedCount) this.Positions.set(config); this.orphaned.set(orphaned); } @@ -133,7 +128,7 @@ export class WallpaperRuntime extends AppProcess { if (!wrapper) return { x: 0, y: 0 }; return new Promise((r) => { - this.Configuration.update((v) => { + this.Positions.update((v) => { function resolve(x: number, y: number) { r({ x, y }); v[`icon$${identifier}`] = { x, y }; @@ -247,55 +242,5 @@ export class WallpaperRuntime extends AppProcess { prog.mutDone(+1); } - //#endregion - //#region CONFIGURATION - - async loadConfiguration() { - this.Log(`Loading configuration`); - - try { - const contents = await Fs.readFile(this.CONFIG_PATH); - if (!contents) return await this.writeConfiguration({}); - - const json = tryJsonParse(arrayBufferToText(contents)); - if (!json || typeof json === "string") return await this.writeConfiguration({}); - - this.Configuration.set(json); - } catch {} - } - - async writeConfiguration(data: DesktopIcons) { - this.Log(`Writing configuration`); - - await Fs.writeFile(this.CONFIG_PATH, textToBlob(JSON.stringify(data, null, 2))); - - return data; - } - - // 7.0.5 -> 7.0.6+ - // Migration of desktop icons from the preferences to a dedicated file in U:/System - async migrateDesktopIcons() { - this.Log(`migrateDesktopIcons`); - - const migrationPath = join(UserPaths.Migrations, "DeskIconMig-706.lock"); - const pref = this.userPreferences().appPreferences.desktopIcons; - const migration = await Fs.stat(migrationPath); - - if (pref && !migration) { - await this.writeConfiguration(pref); - this.Configuration.set(pref); - - this.userPreferences.update((v) => { - delete v.appPreferences.desktopIcons; - return v; - }); - - await Fs.writeFile(migrationPath, textToBlob(`${Date.now()}`)); - return true; - } - - return false; - } - //#endregion } diff --git a/src/apps/user/filemanager/FileManager/Virtual/MyArcOS/RecentFiles.svelte b/src/apps/user/filemanager/FileManager/Virtual/MyArcOS/RecentFiles.svelte index 3c3e223b..c5593113 100644 --- a/src/apps/user/filemanager/FileManager/Virtual/MyArcOS/RecentFiles.svelte +++ b/src/apps/user/filemanager/FileManager/Virtual/MyArcOS/RecentFiles.svelte @@ -8,7 +8,7 @@ const { process }: { process: FileManagerRuntime } = $props(); const { userPreferences } = process; const service = Daemon.serviceHost?.getService("RecentFilesSvc"); - const Configuration = service?.Configuration; + const Configuration = service?.Recents; let selected = $state(""); @@ -26,7 +26,7 @@ { caption: "Clear recents", icon: "x", - action: () => service.Configuration.set([]), + action: () => service.Recents.set([]), }, ], process, diff --git a/src/apps/user/iconeditor/runtime.ts b/src/apps/user/iconeditor/runtime.ts index 39eafc9d..6b8c044c 100644 --- a/src/apps/user/iconeditor/runtime.ts +++ b/src/apps/user/iconeditor/runtime.ts @@ -24,7 +24,7 @@ export class IconEditorRuntime extends AppProcess { async start() { this.iconService = Daemon?.serviceHost?.getService("IconService"); this.setGroups(); - this.icons.set({ ...(this.iconService?.Configuration() || {}) }); + this.icons.set({ ...(this.iconService?.Icons() || {}) }); this.icons.subscribe(() => { this.hasChanges.set(true); this.updateFiltered(); @@ -49,7 +49,7 @@ export class IconEditorRuntime extends AppProcess { ); if (saveChanges) { - this.iconService?.Configuration.set({ ...this.icons() }); + this.iconService?.Icons.set({ ...this.icons() }); this.hasChanges.set(false); } @@ -60,7 +60,7 @@ export class IconEditorRuntime extends AppProcess { revert() { this.Log(`Reverting changes`); - this.icons.set({ ...(this.iconService?.Configuration() || {}) }); + this.icons.set({ ...(this.iconService?.Icons() || {}) }); this.setGroups(); this.selectedIcon.set(""); this.selectedGroup.set(""); @@ -88,7 +88,7 @@ export class IconEditorRuntime extends AppProcess { async save() { this.Log(`Saving changes`); - this.iconService?.Configuration.set({ ...this.icons() }); + this.iconService?.Icons.set({ ...this.icons() }); this.hasChanges.set(false); await this.closeWindow(); diff --git a/src/apps/user/mediaplayer/runtime.ts b/src/apps/user/mediaplayer/runtime.ts index c0609cd9..35778b52 100755 --- a/src/apps/user/mediaplayer/runtime.ts +++ b/src/apps/user/mediaplayer/runtime.ts @@ -19,6 +19,7 @@ import { MediaPlayerAltMenu } from "./altmenu"; import TrayPopup from "./MediaPlayer/TrayPopup.svelte"; import { LoopMode, type AudioFileMetadata, type MetadataConfiguration, type PlayerState } from "./types"; import { CommandResult } from "$ts/result"; +import { ConfigurationBuilder } from "$ts/config"; export class MediaPlayerRuntime extends AppProcess { private readonly METADATA_PATH = join(UserPaths.Configuration, "MediaPlayer", "Metadata.json"); @@ -38,6 +39,12 @@ export class MediaPlayerRuntime extends AppProcess { CurrentCoverUrl = Store(); LoadingMetadata = Store(false); mediaSpecificAccentColor = Store(""); + Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.MetadataConfiguration) + .WritesTo(this.METADATA_PATH) + .WithDefaults({}) + .Build(); override contextMenu: AppContextMenu = { player: [ @@ -111,14 +118,8 @@ export class MediaPlayerRuntime extends AppProcess { protected async start(): Promise { await Fs.createDirectory(getParentDirectory(this.METADATA_PATH)); await Fs.createDirectory(this.COVERIMAGES_PATH); - await this.readConfiguration(); + await this.Configuration.initialize(); - let firstSub = false; - this.MetadataConfiguration.subscribe((v) => { - if (!firstSub) return (firstSub = true); - - this.writeConfiguration(v); - }); this.CurrentMediaMetadata.subscribe((v) => { if (!v?.title) return; @@ -588,26 +589,6 @@ export class MediaPlayerRuntime extends AppProcess { //#endregion //#region METADATA - async readConfiguration() { - try { - const content = await Fs.readFile(this.METADATA_PATH); - if (!content) throw new Error("Failed to read file contents"); - - const json = tryJsonParse(arrayBufferToText(content)); - if (!json || typeof json === "string") throw new Error("File contents could not be parsed as JSON"); - - this.MetadataConfiguration.set(json); - } catch { - return await this.writeConfiguration({}); - } - } - - async writeConfiguration(configuration: MetadataConfiguration) { - this.Log(`writeConfiguration`); - - await Fs.writeFile(this.METADATA_PATH, textToBlob(JSON.stringify(configuration, null, 2)), undefined, false); - } - async normalizeMetadata(meta: IAudioMetadata): Promise { this.Log(`normalizeMetadata`); diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts new file mode 100644 index 00000000..d6e079dd --- /dev/null +++ b/src/interfaces/config.ts @@ -0,0 +1,5 @@ +export interface IConfigurator { + readConfiguration(): Promise; + writeConfiguration(configuration?: T): Promise; + initialize(): Promise; +} diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index 794d5b76..534950b2 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -8,14 +8,14 @@ import type { } from "$types/fs"; import type { PermissionStorage } from "$types/permission"; import type { ReadableStore } from "$types/writable"; +import type { IConfigurator } from "./config"; import type { IFilesystemDrive } from "./fs"; import type { IProcess } from "./process"; export interface IPermissionHandler { _criticalProcess: boolean; - Configuration: ReadableStore; - readConfiguration(): Promise; - writeConfiguration(config: PermissionStorage): Promise; + Storage: ReadableStore; + Configuration: IConfigurator; hasPermission(process: IProcess, permission: PermissionString): boolean; grantPermission(process: IProcess, permission: PermissionString): void; revokePermission(process: IProcess, permission: PermissionString): void; diff --git a/src/interfaces/services/IconService.ts b/src/interfaces/services/IconService.ts index a8b354e1..2ac9557e 100644 --- a/src/interfaces/services/IconService.ts +++ b/src/interfaces/services/IconService.ts @@ -7,10 +7,8 @@ export interface IIconService extends IBaseService { FILE_CACHE: Record; ICON_TYPES: string[]; DEFAULT_ICON: string; - Configuration: ReadableStore>; + Icons: ReadableStore>; start(): Promise; - loadConfiguration(): Promise>; - writeConfiguration(config: Record): Promise>; defaultConfiguration(): Record; getIcon(id: string, noCache?: boolean): Promise; getIconCached(id: string): string; diff --git a/src/interfaces/services/MigrationSvc.ts b/src/interfaces/services/MigrationSvc.ts index 9256a950..b7ca40b7 100644 --- a/src/interfaces/services/MigrationSvc.ts +++ b/src/interfaces/services/MigrationSvc.ts @@ -7,6 +7,4 @@ export interface IMigrationService extends IBaseService { MIGRATIONS: IMigrationNodeConstructor[]; runMigrations(cb?: MigrationStatusCallback): Promise>; runMigration(migration: IMigrationNodeConstructor, cb?: MigrationStatusCallback): Promise; - loadConfiguration(): Promise>; - writeConfiguration(config: Record): Promise>; } diff --git a/src/interfaces/services/RecentFilesSvc.ts b/src/interfaces/services/RecentFilesSvc.ts index a5dbdafb..893b04f1 100644 --- a/src/interfaces/services/RecentFilesSvc.ts +++ b/src/interfaces/services/RecentFilesSvc.ts @@ -1,8 +1,6 @@ import type { IBaseService } from "$interfaces/service"; export interface IRecentFilesService extends IBaseService { - loadConfiguration(): Promise; - writeConfiguration(configuration: string[]): Promise; addToRecents(path: string): boolean; removeFromRecents(path: string): boolean; getRecents(): string[]; diff --git a/src/interfaces/services/TrashSvc.ts b/src/interfaces/services/TrashSvc.ts index b52c9149..fd4fbbbe 100644 --- a/src/interfaces/services/TrashSvc.ts +++ b/src/interfaces/services/TrashSvc.ts @@ -1,3 +1,4 @@ +import type { IConfigurator } from "$interfaces/config"; import type { IBaseService } from "$interfaces/service"; import type { TrashIndexNode } from "$types/trash"; import type { ReadableStore } from "$types/writable"; @@ -5,9 +6,8 @@ import type { ReadableStore } from "$types/writable"; export interface ITrashCanService extends IBaseService { INDEX_PATH: string; IndexBuffer: ReadableStore>; + Configuration: IConfigurator>; start(): Promise; - readIndex(): Promise>; - writeIndex(index: Record): Promise>; moveToTrash(path: string, dispatch?: boolean): Promise; restoreTrashItem(uuid: string): Promise; getIndex(): Record; diff --git a/src/ts/config.ts b/src/ts/config.ts new file mode 100644 index 00000000..f0ec1ce8 --- /dev/null +++ b/src/ts/config.ts @@ -0,0 +1,135 @@ +import type { IConfigurator } from "$interfaces/config"; +import type { IProcess } from "$interfaces/process"; +import { LogLevel } from "$types/logging"; +import type { ReadableStore } from "$types/writable"; +import { Fs } from "./env"; +import { Log } from "./logging"; +import { arrayBufferToText, textToBlob } from "./util/convert"; +import { tryJsonParse } from "./util/json"; + +export class ConfigurationBuilder { + #store?: ReadableStore; + #savePath?: string; + #defaults?: T; + #process?: IProcess; + #cooldown?: number; + #built = false; + + ReadsFrom(store: ReadableStore) { + if (this.#store) throw new Error("ConfigurationBuilder store can only be initialized once"); + if (this.#built) throw new Error("ConfigurationBuilder has to be fully configured before building"); + + this.#store = store; + return this; + } + + WritesTo(path: string) { + if (this.#savePath) throw new Error("ConfigurationBuilder savePath can only be set once"); + if (this.#built) throw new Error("ConfigurationBuilder has to be fully configured before building"); + + this.#savePath = path; + return this; + } + + WithDefaults(defaults: T) { + if (this.#defaults) throw new Error("ConfigurationBuilder defaults can only be set once"); + if (this.#built) throw new Error("ConfigurationBuilder has to be fully configured before building"); + + this.#defaults = defaults; + return this; + } + + ForProcess(process: IProcess) { + if (this.#process) throw new Error("ConfigurationBuilder process can only be set once"); + if (this.#built) throw new Error("ConfigurationBuilder has to be fully configured before building"); + + this.#process = process; + return this; + } + + WithCooldown(cooldown: number) { + if (this.#cooldown !== undefined) throw new Error("ConfigurationBuilder: cooldown can only be set once"); + if (this.#built) throw new Error("ConfigurationBuilder has to be fully configured before building"); + + this.#cooldown = cooldown; + return this; + } + + Build(): IConfigurator { + if (this.#built) throw new Error("ConfigurationBuilder can only be built once"); + + const store = this.#store; + const savePath = this.#savePath; + const defaults = this.#defaults; + const process = this.#process; + const cooldown = this.#cooldown; + let initialRun = true; + + if (!savePath) throw new Error("savePath is required. Use WritesTo to set"); + if (!store) throw new Error("store is required. Use ReadsFrom to set"); + + this.Log(`Building new configurator for ${savePath} (process ${process?.name ?? ""})`); + + const Log = (m: string, level = LogLevel.info) => this.Log(`Configurator: ${m}`, level); + + class Configurator implements IConfigurator { + timeout?: NodeJS.Timeout; + + async readConfiguration(): Promise { + if (process?._disposed) return defaults!; + + Log("Reading configuration"); + + const content = await Fs.readFile(savePath!); + if (!content) return await this.writeConfiguration(defaults); + + try { + const obj = tryJsonParse(arrayBufferToText(content)); + + return obj; + } catch { + return await this.writeConfiguration(defaults); + } + } + + async writeConfiguration(configuration: T | undefined = defaults): Promise { + if (process?._disposed) return configuration!; + if (!configuration) throw new Error("Need a configuration to write"); + + if (initialRun) { + initialRun = false; + return configuration; + } + + Log("Writing configuration"); + + await Fs.writeFile(savePath!, textToBlob(JSON.stringify(configuration, null, 2))); + + return configuration; + } + + async initialize() { + if (process?._disposed) return; + + Log("Initializing"); + + store!.set((await this.readConfiguration())!); + store!.subscribe((v) => { + clearTimeout(this.timeout); + + this.timeout = setTimeout(() => { + this.writeConfiguration(v); + }, cooldown); + }); + } + } + + const configurator = new Configurator(); + + return configurator; + } + + private Log(message: string, level = LogLevel.info) { + Log(`ConfigurationBuilder::${this.#process?.pid ?? "NO_PROC"}`, message, level); + } +} diff --git a/src/ts/permissions/index.ts b/src/ts/permissions/index.ts index d989f6d9..594a0a5a 100644 --- a/src/ts/permissions/index.ts +++ b/src/ts/permissions/index.ts @@ -2,12 +2,11 @@ import type { IPermissionedFilesystemInteractor, IPermissionHandler } from "$int import type { IProcess } from "$interfaces/process"; import { AppProcess } from "$ts/apps/process"; import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; +import { ConfigurationBuilder } from "$ts/config"; import { Daemon } from "$ts/daemon"; -import { Fs, USERFS_UUID } from "$ts/env"; +import { USERFS_UUID } from "$ts/env"; import { Process } from "$ts/kernel/mods/stack/process/instance"; import { sliceIntoChunks } from "$ts/util"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import { ElevationLevel } from "$types/elevation"; import type { PermissionStorage, SudoPermissions } from "$types/permission"; @@ -29,10 +28,14 @@ export class PermissionHandler extends Process implements IPermissionHandler { private PERMISSION_ID_REGEX = /^([0-9A-Z]{4}-){3}[0-9A-Z]{4}$/gm; #PERMISSION_FILE = "U:/System/Permissions.json"; #PERMISSION_EXPIRY = 1000 * 60 * 10; // 10 minutes - public Configuration = Store(DefaultPermissionStorage); + public Storage = Store(DefaultPermissionStorage); + Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.Storage) + .WritesTo(this.#PERMISSION_FILE) + .WithCooldown(100) + .Build(); private SudoConfiguration = Store({}); - private FirstSubDone = false; - private configurationWriteTimeout?: NodeJS.Timeout; #permissionedFilesystemInteractors: Record = {}; get #PERMISSION_EXPIRY_DYN() { @@ -50,35 +53,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { } protected async start(): Promise { - await this.readConfiguration(); - - this.Configuration.subscribe((v) => { - if (!this.FirstSubDone) return (this.FirstSubDone = true); - - clearTimeout(this.configurationWriteTimeout); - - this.configurationWriteTimeout = setTimeout(() => { - this.writeConfiguration(v); - }, 100); - }); - } - - //#endregion - - //#region CONFIGURATION - - async readConfiguration() { - const content = await Fs.readFile(this.#PERMISSION_FILE); - if (!content) return this.writeConfiguration(this.Configuration()); - - const json = tryJsonParse(arrayBufferToText(content)); - if (typeof json === "string") return this.writeConfiguration(DefaultPermissionStorage); - - this.Configuration.set(json); - } - - async writeConfiguration(config: PermissionStorage) { - await Fs.writeFile(this.#PERMISSION_FILE, textToBlob(JSON.stringify(config, null, 2)), undefined, false); + await this.Configuration.initialize(); } //#endregion @@ -89,14 +64,14 @@ export class PermissionHandler extends Process implements IPermissionHandler { this.validatePermissionString(permission); const id = this.getPermissionId(process); - return this.Configuration().allowed[id]?.includes(permission); + return this.Storage().allowed[id]?.includes(permission); } hasPermissionById(permissionId: string, permission: PermissionString) { this.validatePermissionString(permission); this.validatePermissionId(permissionId); - return this.Configuration().allowed[permissionId]?.includes(permission); + return this.Storage().allowed[permissionId]?.includes(permission); } grantPermission(process: IProcess, permission: PermissionString) { @@ -105,7 +80,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (this.hasPermission(process, permission)) this.throwError("PERMERR_ALREADY_OWNED", id, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.allowed[id] ||= []; v.allowed[id].push(permission); @@ -119,7 +94,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (this.hasPermissionById(permissionId, permission)) this.throwError("PERMERR_ALREADY_OWNED", permissionId, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.allowed[permissionId] ||= []; v.allowed[permissionId].push(permission); @@ -131,7 +106,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { this.validatePermissionString(permission); const id = this.getPermissionId(process); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.allowed[id] ||= []; v.allowed[id].splice(v.allowed[id].indexOf(permission), 1); @@ -143,7 +118,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { this.validatePermissionString(permission); this.validatePermissionId(permissionId); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.allowed[permissionId] ||= []; v.allowed[permissionId].splice(v.allowed[permissionId].indexOf(permission), 1); @@ -158,14 +133,14 @@ export class PermissionHandler extends Process implements IPermissionHandler { this.validatePermissionString(permission); const id = this.getPermissionId(process); - return this.Configuration().denied[id]?.includes(permission); + return this.Storage().denied[id]?.includes(permission); } isDeniedById(permissionId: string, permission: PermissionString) { this.validatePermissionId(permissionId); this.validatePermissionString(permission); - return this.Configuration().denied[permissionId]?.includes(permission); + return this.Storage().denied[permissionId]?.includes(permission); } denyPermission(process: IProcess, permission: PermissionString) { @@ -176,7 +151,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (this.isDenied(process, permission)) this.throwError("PERMERR_ALREADY_DENIED", id, permission); if (this.hasPermission(process, permission)) this.revokePermission(process, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.denied[id] ||= []; v.denied[id].push(permission); @@ -191,7 +166,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (this.isDeniedById(permissionId, permission)) this.throwError("PERMERR_ALREADY_DENIED", permissionId, permission); if (this.hasPermissionById(permissionId, permission)) this.revokePermissionById(permissionId, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.denied[permissionId] ||= []; v.denied[permissionId].push(permission); @@ -206,7 +181,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (!this.isDenied(process, permission)) this.throwError("PERMERR_NOT_DENIED", id, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.denied[id] ||= []; v.denied[id].splice(v.denied[id].indexOf(permission), 1); @@ -220,7 +195,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { if (!this.isDeniedById(permissionId, permission)) this.throwError("PERMERR_NOT_DENIED", permissionId, permission); - this.Configuration.update((v) => { + this.Storage.update((v) => { v.denied[permissionId] ||= []; v.denied[permissionId].splice(v.denied[permissionId].indexOf(permission), 1); @@ -308,7 +283,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { resetPermissionsById(permissionId: string) { this.validatePermissionId(permissionId); - this.Configuration.update((v) => { + this.Storage.update((v) => { delete v.allowed[permissionId]; delete v.denied[permissionId]; delete v.registration[permissionId]; @@ -363,7 +338,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { //#region REGISTRATION setRegistration(clientId: string, appId: string) { - this.Configuration.update((v) => { + this.Storage.update((v) => { v.registration[clientId] = appId; return v; @@ -371,7 +346,7 @@ export class PermissionHandler extends Process implements IPermissionHandler { } removeRegistration(clientId: string) { - this.Configuration.update((v) => { + this.Storage.update((v) => { delete v.registration[clientId]; return v; @@ -379,14 +354,14 @@ export class PermissionHandler extends Process implements IPermissionHandler { } removeApplication(appId: string) { - const clients = Object.entries(this.Configuration().registration) + const clients = Object.entries(this.Storage().registration) .filter(([_, v]) => v === appId) .map(([k]) => k); for (const clientId of clients) { this.removeRegistration(clientId); - this.Configuration.update((v) => { + this.Storage.update((v) => { delete v.allowed[clientId]; delete v.denied[clientId]; diff --git a/src/ts/servicehost/services/FileAssocSvc/index.ts b/src/ts/servicehost/services/FileAssocSvc/index.ts index b23335bf..a63dad7a 100644 --- a/src/ts/servicehost/services/FileAssocSvc/index.ts +++ b/src/ts/servicehost/services/FileAssocSvc/index.ts @@ -1,13 +1,11 @@ import type { IFileAssocService } from "$interfaces/services/FileAssocSvc"; +import { ConfigurationBuilder } from "$ts/config"; import { Daemon } from "$ts/daemon"; -import { Fs } from "$ts/env"; import type { ServiceHost } from "$ts/servicehost"; import { BaseService } from "$ts/servicehost/base"; import { ApplicationStorage } from "$ts/servicehost/services/AppStorage"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { getItemNameFromPath, join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import type { ExpandedFileAssociationInfo, FileAssociationConfig } from "$types/assoc"; import type { Service } from "$types/service"; @@ -15,7 +13,13 @@ import { DefaultFileDefinitions } from "./store"; export class FileAssocService extends BaseService implements IFileAssocService { private CONFIG_PATH = join(UserPaths.System, "FileAssociations.json"); - private Configuration = Store(); + private Associations = Store(); + private Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.Associations) + .WritesTo(this.CONFIG_PATH) + .WithDefaults(this.defaultFileAssociations()) + .Build(); //#region LIFECYCLE @@ -27,44 +31,18 @@ export class FileAssocService extends BaseService implements IFileAssocService { async start() { this.initBroadcast?.("Starting file associations"); - await this.loadConfiguration(); + await this.Configuration.initialize(); } //#endregion - - private async loadConfiguration() { - if (this._disposed) return; - - this.Log("Loading configuration"); - const contents = await Fs.readFile(this.CONFIG_PATH); - - const json = contents ? tryJsonParse(arrayBufferToText(contents)) : undefined; - - if (!json || typeof json === "string") return await this.writeConfiguration(this.defaultFileAssociations()); - - this.Configuration.set(json); - } - - private async writeConfiguration(configuration: FileAssociationConfig) { - if (this._disposed) return configuration; - this.Log("Writing configuration"); - - await Fs.writeFile(this.CONFIG_PATH, textToBlob(JSON.stringify(configuration, null, 2))); - - this.Configuration.set(configuration); - - return configuration; - } - public async updateConfiguration( callback: (config: FileAssociationConfig) => FileAssociationConfig | Promise ) { if (this._disposed) return; - const result = await callback(this.Configuration()); + const result = await callback(this.Associations()); - this.Configuration.set(result); - await this.writeConfiguration(result); + this.Associations.set(result); } public defaultFileAssociations(): FileAssociationConfig { @@ -98,7 +76,7 @@ export class FileAssocService extends BaseService implements IFileAssocService { if (this._disposed || !path) return; const storage = this.host.getService("AppStorage"); - const config = this.Configuration(); + const config = this.Associations(); const associations = config?.associations; const definitions = config?.definitions; const split = path.split("."); @@ -130,7 +108,7 @@ export class FileAssocService extends BaseService implements IFileAssocService { } getUnresolvedAssociationIcon(path: string): string { - const config = this.Configuration(); + const config = this.Associations(); const associations = config?.associations; const definitions = config?.definitions; const split = path.split("."); @@ -144,7 +122,7 @@ export class FileAssocService extends BaseService implements IFileAssocService { } getConfiguration() { - return this.Configuration(); + return this.Associations(); } } diff --git a/src/ts/servicehost/services/IconService/index.ts b/src/ts/servicehost/services/IconService/index.ts index d7968baa..551b8696 100644 --- a/src/ts/servicehost/services/IconService/index.ts +++ b/src/ts/servicehost/services/IconService/index.ts @@ -1,13 +1,12 @@ import type { IIconService } from "$interfaces/services/IconService"; +import { ConfigurationBuilder } from "$ts/config"; import { Daemon } from "$ts/daemon"; import { Fs } from "$ts/env"; import { getAllImages, getGroupedIcons, iconIdFromPath, maybeIconId } from "$ts/images"; import type { ServiceHost } from "$ts/servicehost"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import type { App } from "$types/app"; import type { Service } from "$types/service"; @@ -17,7 +16,15 @@ export class IconService extends BaseService implements IIconService { FILE_CACHE: Record = {}; // R ICON_TYPES = ["fs", "builtin", "app"]; DEFAULT_ICON = ""; - Configuration = Store>({}); + Icons = Store>({}); + Configuration = new ConfigurationBuilder>() + .ForProcess(this) + .ReadsFrom(this.Icons) + .WritesTo(this.PATH) + .WithDefaults({}) + .WithCooldown(50) + .Build(); + //#region LIFECYCLE constructor(pid: number, parentPid: number, name: string, host: ServiceHost, initBroadcast?: (msg: string) => void) { @@ -28,15 +35,10 @@ export class IconService extends BaseService implements IIconService { async start() { this.initBroadcast?.("Starting icon service"); - this.Configuration.set(await this.loadConfiguration()); + await this.Configuration.initialize(); await this.cacheEverything(); - let initialDone = false; - - this.Configuration.subscribe((v) => { - if (!initialDone) return (initialDone = true); - - this.writeConfiguration(v); + this.Icons.subscribe(() => { this.cacheEverything(); }); } @@ -44,25 +46,6 @@ export class IconService extends BaseService implements IIconService { //#endregion //#region CONFIGURATION - async loadConfiguration() { - this.Log(`Loading configuration`); - const config = tryJsonParse>(arrayBufferToText((await Fs.readFile(this.PATH))!)); - - if (!config || typeof config === "string") { - return await this.writeConfiguration(this.defaultConfiguration()); - } - - return config; - } - - async writeConfiguration(config: Record) { - this.Log(`Writing configuration: ${Object.keys(config).length} icons`); - - await Fs.writeFile(this.PATH, textToBlob(JSON.stringify(config, null, 2))); - - return config; - } - defaultConfiguration() { const icons = getAllImages(); const config: Record = {}; @@ -78,7 +61,7 @@ export class IconService extends BaseService implements IIconService { async getIcon(id: string, noCache = false): Promise { if (!id) return this.DEFAULT_ICON; - const icon = id.startsWith("@") ? id : this.Configuration()[id]; + const icon = id.startsWith("@") ? id : this.Icons()[id]; if (!icon) return this.DEFAULT_ICON; try { @@ -125,7 +108,7 @@ export class IconService extends BaseService implements IIconService { // - Izaak Kuipers, September 25th 2025 if (id.startsWith("@local:")) return this.DEFAULT_ICON; - const icon = id.startsWith("@") ? id : this.Configuration()[id]; + const icon = id.startsWith("@") ? id : this.Icons()[id]; if (!icon) return this.DEFAULT_ICON; try { @@ -168,7 +151,7 @@ export class IconService extends BaseService implements IIconService { } async cacheEverything() { - const icons = this.Configuration(); + const icons = this.Icons(); const promises = []; const known: string[] = []; diff --git a/src/ts/servicehost/services/MigrationSvc/index.ts b/src/ts/servicehost/services/MigrationSvc/index.ts index 9c8d8845..b12ecd80 100644 --- a/src/ts/servicehost/services/MigrationSvc/index.ts +++ b/src/ts/servicehost/services/MigrationSvc/index.ts @@ -1,12 +1,10 @@ import type { IMigrationNodeConstructor } from "$interfaces/migration"; import type { IMigrationService } from "$interfaces/services/MigrationSvc"; -import { Fs } from "$ts/env"; +import { ConfigurationBuilder } from "$ts/config"; import type { ServiceHost } from "$ts/servicehost"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import { LogLevel } from "$types/logging"; import type { MigrationResult, MigrationStatusCallback } from "$types/migrations"; @@ -16,13 +14,20 @@ import { FileAssociationsMigration } from "./nodes/FileAssociations"; import { IconConfigurationMigration } from "./nodes/IconConfiguration"; export class MigrationService extends BaseService implements IMigrationService { - private Configuration = Store>({}); + private Index = Store>({}); private CONFIG_PATH = join(UserPaths.Migrations, "Index.json"); + private Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.Index) + .WritesTo(this.CONFIG_PATH) + .WithDefaults({}) + .WithCooldown(100) + .Build(); public MIGRATIONS: IMigrationNodeConstructor[] = [FileAssociationsMigration, IconConfigurationMigration, AppShortcutsMigration]; public get Config() { - return this.Configuration(); + return this.Index(); } //#region LIFECYCLE @@ -34,16 +39,8 @@ export class MigrationService extends BaseService implements IMigrationService { } protected async start(): Promise { - let initialDone = false; - - this.Configuration.set(await this.loadConfiguration()); - this.Configuration.subscribe((v) => { - if (!initialDone) return (initialDone = true); - - this.writeConfiguration(v); - }); - this.initBroadcast?.("Running migrations"); + await this.Configuration.initialize(); await this.runMigrations(this.initBroadcast); } @@ -51,7 +48,7 @@ export class MigrationService extends BaseService implements IMigrationService { async runMigrations(cb?: MigrationStatusCallback): Promise> { this.Log("runMigrations: now running versional migrations"); - const config = this.Configuration(); + const config = this.Index(); const results: Record = {}; cb?.("Running migrations"); @@ -84,7 +81,7 @@ export class MigrationService extends BaseService implements IMigrationService { results[migration.name] = result; if (result.result === "err_ok" || result.result === "err_sameVersion") - this.Configuration.update((v) => { + this.Index.update((v) => { v[migration.name] = migration.version; return v; }); @@ -108,33 +105,13 @@ export class MigrationService extends BaseService implements IMigrationService { const result = await instance._runMigration(cb); if (result.result === "err_ok" || result.result === "err_sameVersion") - this.Configuration.update((v) => { + this.Index.update((v) => { v[migration.name] = migration.version; return v; }); return result; } - - async loadConfiguration() { - this.Log(`Loading configuration`); - - const config = tryJsonParse>(arrayBufferToText((await Fs.readFile(this.CONFIG_PATH))!)); - - if (!config || typeof config === "string") { - return await this.writeConfiguration({}); - } - - return config; - } - - async writeConfiguration(config: Record) { - this.Log(`Writing configuration: ${Object.keys(config).length} migrations`); - - await Fs.writeFile(this.CONFIG_PATH, textToBlob(JSON.stringify(config, null, 2))); - - return config; - } } export const migrationService: Service = { diff --git a/src/ts/servicehost/services/RecentFilesSvc/index.ts b/src/ts/servicehost/services/RecentFilesSvc/index.ts index 82f06b58..4fce7d1a 100644 --- a/src/ts/servicehost/services/RecentFilesSvc/index.ts +++ b/src/ts/servicehost/services/RecentFilesSvc/index.ts @@ -1,17 +1,23 @@ import type { IRecentFilesService } from "$interfaces/services/RecentFilesSvc"; +import { ConfigurationBuilder } from "$ts/config"; import { Fs } from "$ts/env"; import type { ServiceHost } from "$ts/servicehost"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; import { Store } from "$ts/writable"; import type { Service } from "$types/service"; export class RecentFilesService extends BaseService implements IRecentFilesService { - Configuration = Store([]); + Recents = Store([]); readonly CONFIG_PATH = join(UserPaths.System, "RecentFiles.json"); + private Configuration = new ConfigurationBuilder() + .ForProcess(this) + .ReadsFrom(this.Recents) + .WritesTo(this.CONFIG_PATH) + .WithDefaults([]) + .WithCooldown(100) + .Build(); //#region LIFECYCLE @@ -23,36 +29,10 @@ export class RecentFilesService extends BaseService implements IRecentFilesServi protected async start(): Promise { this.initBroadcast?.("Starting recent files service"); - await this.loadConfiguration(); - - let firstDone = false; - this.Configuration.subscribe((v) => { - if (!firstDone) return (firstDone = true); - - this.writeConfiguration(v); - }); + await this.Configuration.initialize(); } //#endregion LIFECYCLE - //#region CONFIGURATION - - async loadConfiguration() { - try { - const content = tryJsonParse(arrayBufferToText((await Fs.readFile(this.CONFIG_PATH))!)); - - if (!content || typeof content === "string") throw ""; - - this.Configuration.set(content as string[]); - } catch { - await this.writeConfiguration([]); - } - } - - async writeConfiguration(configuration: string[]) { - await Fs.writeFile(this.CONFIG_PATH, textToBlob(JSON.stringify(configuration, null, 2))); - } - - //#endregion //#region EXTERNAL API addToRecents(path: string) { @@ -66,11 +46,11 @@ export class RecentFilesService extends BaseService implements IRecentFilesServi if (path.startsWith(UserPaths.System) || path.startsWith(UserPaths.Applications) || path.startsWith("T:/")) return false; // Remove if the path already exists in the list so that it'll be moved to the top - if (this.Configuration().includes(path)) { + if (this.Recents().includes(path)) { this.removeFromRecents(path); } - this.Configuration.update((v) => { + this.Recents.update((v) => { v = [path].concat(v); // Push path to start of array return v; }); @@ -79,9 +59,9 @@ export class RecentFilesService extends BaseService implements IRecentFilesServi } removeFromRecents(path: string) { - if (!this.Configuration().includes(path)) return false; + if (!this.Recents().includes(path)) return false; - this.Configuration.update((v) => { + this.Recents.update((v) => { v.splice(v.indexOf(path), 1); return v; }); @@ -90,7 +70,7 @@ export class RecentFilesService extends BaseService implements IRecentFilesServi } getRecents() { - return this.Configuration(); + return this.Recents(); } //#endregion diff --git a/src/ts/servicehost/services/TrashSvc/index.ts b/src/ts/servicehost/services/TrashSvc/index.ts index 85491d20..aa030833 100644 --- a/src/ts/servicehost/services/TrashSvc/index.ts +++ b/src/ts/servicehost/services/TrashSvc/index.ts @@ -1,10 +1,10 @@ import type { ITrashCanService } from "$interfaces/services/TrashSvc"; +import { ConfigurationBuilder } from "$ts/config"; import { Daemon } from "$ts/daemon"; import { Env, Fs, SysDispatch } from "$ts/env"; import type { ServiceHost } from "$ts/servicehost"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; -import { arrayBufferToText, textToBlob } from "$ts/util/convert"; import { getItemNameFromPath, getParentDirectory, join } from "$ts/util/fs"; import { UUID } from "$ts/util/uuid"; import { Store } from "$ts/writable"; @@ -14,6 +14,12 @@ import type { TrashIndexNode } from "$types/trash"; export class TrashCanService extends BaseService implements ITrashCanService { INDEX_PATH = join(UserPaths.System, `TrashIndex.json`); IndexBuffer = Store>({}); + Configuration = new ConfigurationBuilder>() + .ForProcess(this) + .ReadsFrom(this.IndexBuffer) + .WritesTo(this.INDEX_PATH) + .WithDefaults({}) + .Build(); //#region LIFECYCLE @@ -25,32 +31,11 @@ export class TrashCanService extends BaseService implements ITrashCanService { async start() { this.initBroadcast?.("Starting trash service"); - this.IndexBuffer.set(await this.readIndex()); - this.IndexBuffer.subscribe((v) => this.writeIndex(v)); + await this.Configuration.initialize(); } //#endregion - async readIndex(): Promise> { - const content = await Fs.readFile(this.INDEX_PATH); - - if (!content) return await this.writeIndex({}); - - try { - const parsed = JSON.parse(arrayBufferToText(content)!); - - return parsed as Record; - } catch { - return await this.writeIndex({}); - } - } - - async writeIndex(index: Record) { - await Fs.writeFile(this.INDEX_PATH, textToBlob(JSON.stringify(index, null, 2))); - - return index; - } - async moveToTrash(path: string, dispatch = false): Promise { if (Daemon?.preferences().globalSettings.disableTrashCan) { await Fs.deleteItem(path); From 7b07bf629d935598a026180565c7987314b2829a Mon Sep 17 00:00:00 2001 From: Izaak Kuipers Date: Tue, 24 Feb 2026 14:08:04 +0100 Subject: [PATCH 2/2] Fix some of the svelte references --- .../components/apppermissions/AppPermissions.svelte | 4 ++-- .../apppermissions/AppPermissions/PermissionRow.svelte | 4 ++-- .../components/wallpaper/Wallpaper/DesktopIcon.svelte | 10 +++++----- .../FileManager/Virtual/MyArcOS/RecentFiles.svelte | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/apps/components/apppermissions/AppPermissions.svelte b/src/apps/components/apppermissions/AppPermissions.svelte index 06ae103b..ccf12f3f 100644 --- a/src/apps/components/apppermissions/AppPermissions.svelte +++ b/src/apps/components/apppermissions/AppPermissions.svelte @@ -8,12 +8,12 @@ const { process }: { process: AppPermissionsRuntime } = $props(); const { targetApp } = process; - const { Storage: Configuration } = Permissions!; + const { Storage } = Permissions!; let permissionId = $state(""); onMount(() => { - Configuration.subscribe((permissions) => { + Storage.subscribe((permissions) => { const reg = permissions?.registration ?? {}; permissionId = Object.keys(reg).find((key) => reg[key] === $targetApp.id) ?? ""; }); diff --git a/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte b/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte index 6f103934..824a051f 100644 --- a/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte +++ b/src/apps/components/apppermissions/AppPermissions/PermissionRow.svelte @@ -4,7 +4,7 @@ import { onMount } from "svelte"; const { id, permissionId }: { id: PermissionString; permissionId: string } = $props(); - const { Storage: Configuration } = Permissions!; + const { Storage } = Permissions!; const options = ["Unset", "Allow", "Deny"] as const; type Option = (typeof options)[number]; @@ -12,7 +12,7 @@ let option = $state