diff --git a/index.ts b/index.ts index 9dd8708..6c7180f 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export type { LocalModuleInstance as Module } from "./module.ts"; +export type { ModuleInstance as Module } from "./module.ts"; export type { Transformer } from "./transform.ts"; import mixin, { applyTransforms } from "./mixins.js"; diff --git a/module.ts b/module.ts index 0b76c34..8972982 100644 --- a/module.ts +++ b/module.ts @@ -48,7 +48,7 @@ export interface Metadata { const getLoadableChildrenInstances = () => RootModule.INSTANCE.getChildren().map((module) => module.getEnabledInstance()).filter( (instance) => instance?.canLoad(), - ) as LocalModuleInstance[]; + ) as ModuleInstance[]; export const enableAllLoadableMixins = () => Promise.all(getLoadableChildrenInstances().map((instance) => instance.loadMixins())); @@ -56,17 +56,19 @@ export const enableAllLoadableMixins = () => export const enableAllLoadable = () => Promise.all(getLoadableChildrenInstances().map((instance) => instance.load())); -export abstract class Module< - C extends Module, - I extends ModuleInstance> = ModuleInstance>, +export abstract class ModuleBase< + C extends ModuleBase, + I extends ModuleInstanceBase> = ModuleInstanceBase>, > { public instances = new Map(); constructor( - public parent: Module> | null, + public parent: ModuleBase> | null, protected children: Record, private identifier: ModuleIdentifier, - ) {} + ) { + this.parent?.setChild(this.identifier, this); + } public getIdentifier(): ModuleIdentifier { return this.identifier; @@ -79,10 +81,10 @@ export abstract class Module< } public async getChildOrNew(identifier: ModuleIdentifier): Promise { - return this.getChild(identifier) ?? await this.newChild(identifier, { enabled: "", v: {} }); + return this.getChild(identifier) ?? this.newChild(identifier, { enabled: "", v: {} }); } - public setChild(identifier: ModuleIdentifier, child: C) { + private setChild(identifier: ModuleIdentifier, child: C) { this.children[identifier] = child; } @@ -94,7 +96,7 @@ export abstract class Module< return Object.values(this.children); } - public getDescendants(identifier: ModuleIdentifier): Array>> { + public getDescendants(identifier: ModuleIdentifier): Array>> { if (identifier === this.identifier) { return [this]; } @@ -103,8 +105,8 @@ export abstract class Module< ); } - public *getAllDescendantsByBreadth(): Generator>> { - const i: Array>> = [this]; + public *getAllDescendantsByBreadth(): Generator>> { + const i: Array>> = [this]; while (i.length) { const e = i.shift()!; @@ -123,28 +125,20 @@ export abstract class Module< return [...ancestry, reducedIdentifier]; } - public abstract newInstance(version: Truthy, store: _Store): Promise; + public abstract newInstance(version: Truthy, store: _Store, local: boolean): Promise; - public async init(versions: _Module["v"]) { + public async init(versions: _Module["v"], local: boolean) { await Promise.all( Object.entries(versions) - .map(([version, store]) => this.newInstance(version, store)), + .map(([version, store]) => this.newInstance(version, store, local)), ); - this.parent!.setChild(this.identifier, this); - return this; } } -export class RootModule extends Module { - override newChild(identifier: ModuleIdentifier, module: _Module): Promise { - const localModule = new LocalModule(this, {}, identifier, module.enabled); - - return localModule.init(module.v); - } - - override newInstance(version: Truthy, store: _Store): Promise { +export class RootModule extends ModuleBase { + override newInstance(): Promise { throw new Error("RootModule can't have instances"); } public static INSTANCE = new RootModule(); @@ -153,65 +147,52 @@ export class RootModule extends Module { super(null, {}, ""); Object.freeze(this.instances); } -} - -export class RemoteModule extends Module { - static fromModule( - parent: Module, - identifier: ModuleIdentifier, - module: _Module, - ): Promise { - const remoteModule = new RemoteModule(parent, {}, identifier); - return remoteModule.init(module.v); - } - - override newChild(identifier: ModuleIdentifier, module: _Module) { - return RemoteModule.fromModule(this, identifier, module); - } - - override async newInstance(version: Truthy, { artifacts, checksum }: _Store) { - const remoteModuleInstance = new RemoteModuleInstance(this, version, null, artifacts, checksum); - - this.instances.set(version, remoteModuleInstance); - - return remoteModuleInstance; + override newChild(identifier: ModuleIdentifier, module: _Module, local = false) { + return Module.prototype.newChild.call(this, identifier, module, local); } } -export class LocalModule extends Module { +export class Module extends ModuleBase { constructor( - parent: RootModule, - children: Record, + parent: RootModule | Module, + children: Record, identifier: ModuleIdentifier, public enabled: Version, ) { super(parent, children, identifier); } - override newChild(identifier: ModuleIdentifier, module: _Module) { - return RemoteModule.fromModule(this, identifier, module); + override newChild(identifier: ModuleIdentifier, module: _Module, local = false) { + if (this.getChild(identifier)) { + throw new Error(`Module ${identifier} already exists`); + } + return new Module(this, {}, identifier, module.enabled).init(module.v, local); } - override async newInstance(version: Truthy, { artifacts, installed, checksum }: _Store) { - const localModuleInstance = new LocalModuleInstance(this, version, null, artifacts, checksum, installed); + override async newInstance( + version: Truthy, + { artifacts, installed, checksum }: _Store, + local = false, + ) { + const instance = new ModuleInstance(this, version, null, artifacts, checksum, local, installed); - this.instances.set(version, localModuleInstance); + this.instances.set(version, instance); - if (localModuleInstance.isEnabled()) { - await localModuleInstance.onEnable(); + if (instance.isEnabled()) { + await instance.onEnable(); } - return localModuleInstance; + return instance; } - public canEnable(instance: LocalModuleInstance) { + public canEnable(instance: ModuleInstance) { const enabledInstance = this.getEnabledInstance(); - return !instance.isEnabled() && instance.isInstalled() && + return instance.canDelete() && (!enabledInstance || this.canDisable(enabledInstance)); } - public enable(instance: LocalModuleInstance) { + public enable(instance: ModuleInstance) { if (!this.canEnable(instance)) { return Promise.resolve(false); } @@ -226,7 +207,7 @@ export class LocalModule extends Module { }); } - public canDisable(instance: LocalModuleInstance) { + public canDisable(instance: ModuleInstance) { return instance.isEnabled() && !instance.isLoaded(); } @@ -250,7 +231,7 @@ export class LocalModule extends Module { return this.enabled; } - public getEnabledInstance(): LocalModuleInstance | undefined { + public getEnabledInstance(): ModuleInstance | undefined { return this.getEnabledVersion() ? this.instances.get(this.getEnabledVersion()!)! : undefined; } } @@ -259,7 +240,7 @@ export interface MixinLoader { awaitedMixins: Promise[]; } -export abstract class ModuleInstance = Module> { +export abstract class ModuleInstanceBase = ModuleBase> { public getName() { return this.metadata?.name; } @@ -299,7 +280,7 @@ export abstract class ModuleInstance = Module> { public metadata: Metadata | null, public artifacts: Array, public checksum: string, - ) {} + ) { } // ? public updateMetadata(metadata: Metadata) { @@ -309,23 +290,7 @@ export abstract class ModuleInstance = Module> { abstract getMetadataURL(): string | null; } -export class RemoteModuleInstance extends ModuleInstance { - public getMetadataURL() { - return this.getRemoteMetadataURL(); - } - - public async add() { - if (!(await ModuleManager.add(this))) { - return null; - } - - const localModule = await RootModule.INSTANCE.getChildOrNew(this.getModuleIdentifier()); - const store: _Store = { installed: false, artifacts: this.artifacts, checksum: this.checksum }; - return await localModule.newInstance(this.version, store); - } -} - -export class LocalModuleInstance extends ModuleInstance implements MixinLoader { +export class ModuleInstance extends ModuleInstanceBase implements MixinLoader { public async loadProviders() { const vault = await fetchJson<_Vault>(this.getRelPath("vault.json")!).catch(() => null); const provider = vault?.modules ?? {}; @@ -350,22 +315,27 @@ export class LocalModuleInstance extends ModuleInstance implements private jsIndex: any; public transition = new Transition(); - private dependants = new Set(); + private dependants = new Set(); public isLoaded() { return this.loaded; } + public isLocal() { + return this.added; + } + public isInstalled() { return this.installed; } constructor( - module: LocalModule, + module: Module, version: Truthy, metadata: Metadata | null, artifacts: Array, checksum: string, + private added: boolean, private installed: boolean, ) { super(module, version, metadata, artifacts, checksum); @@ -533,15 +503,14 @@ export class LocalModuleInstance extends ModuleInstance implements resolve(); } - // ? As is, this always returns true. Recur Impl for easier future modification private canUnloadRecur() { - if (this.loaded) { - for (const dependant of this.dependants) { - if (!dependant.canUnloadRecur()) { - return false; - } - } - } + // if (this.loaded) { + // for (const dependant of this.dependants) { + // if (!dependant.canUnloadRecur()) { + // return false; + // } + // } + // } return true; } @@ -581,8 +550,34 @@ export class LocalModuleInstance extends ModuleInstance implements return false; } + public canAdd() { + return !this.added; + } + + public async add() { + await this.transition.block(); + + if (!this.canAdd()) { + return null; + } + + const resolve = this.transition.extend(); + + if (!(await ModuleManager.add(this))) { + return null; + } + + const module = await RootModule.INSTANCE.getChildOrNew(this.getModuleIdentifier()); + const instance = new ModuleInstance(module, this.getVersion(), this.metadata, this.artifacts, this.checksum, true, false); + module.instances.set(instance.getVersion(), instance); + this.added = true; + resolve(); + + return this; + } + public canInstallRemove() { - return !this.installed; + return this.added && !this.installed; } public async install() { @@ -605,7 +600,7 @@ export class LocalModuleInstance extends ModuleInstance implements } public isEnabled() { - return this.getVersion() === this.module.getEnabledVersion(); + return this.isInstalled() && (this.getVersion() === this.module.getEnabledVersion()); } public async onEnable() { @@ -618,12 +613,12 @@ export class LocalModuleInstance extends ModuleInstance implements } public canLoad() { - return this.canDelete() && this.isEnabled(); + return this.isEnabled() && !this.loaded; } public async load() { await this.transition.block(); - if (this.loaded) { + if (!this.canLoad()) { return false; } try { @@ -638,7 +633,7 @@ export class LocalModuleInstance extends ModuleInstance implements } public canUnload() { - return this.loaded && !this.isEnabled(); + return this.loaded; } public async unload() { @@ -658,7 +653,7 @@ export class LocalModuleInstance extends ModuleInstance implements } public canDelete() { - return this.installed && !this.loaded; + return this.installed && !this.isEnabled(); } public async delete() { @@ -681,19 +676,26 @@ export class LocalModuleInstance extends ModuleInstance implements } public async remove() { + await this.transition.block(); + if (!this.canInstallRemove()) { return null; } + const resolve = this.transition.extend(); + if (!(await ModuleManager.remove(this))) { return null; } this.module.instances.delete(this.getVersion()); if (this.module.instances.size === 0) { + this.module.parent!.removeChild(this.getModuleIdentifier()); this.module.parent = null; - RootModule.INSTANCE.removeChild(this.module.getIdentifier()); } + this.added = false; + resolve(); + return this; } } @@ -704,14 +706,24 @@ export const INTERNAL_MIXIN_LOADER: MixinLoader = { export const INTERNAL_TRANSFORMER = createTransformer(INTERNAL_MIXIN_LOADER); -const vaults = [ +const localModules = [ await fetchJson<_Vault>("/modules/vault.json"), await fetchJson<_Vault>("https://raw.githubusercontent.com/spicetify/modules/main/vault.json"), +].reduceRight<_Vault["modules"]>((acc, vault) => deepMerge(acc, vault.modules), {}); + +const remoteModules = [ await fetchJson<_Vault>("https://raw.githubusercontent.com/spicetify/pkgs/main/vault.json"), -]; -const provider = vaults.reduce<_Vault["modules"]>((acc, vault) => deepMerge(acc, vault.modules), {}); +].reduceRight<_Vault["modules"]>((acc, vault) => deepMerge(acc, vault.modules), {}); + await Promise.all( - Object.keys(provider).map(async (identifier) => - RootModule.INSTANCE.newChild(identifier, provider[identifier]) + Object.keys(localModules).map((identifier) => + RootModule.INSTANCE.newChild(identifier, localModules[identifier], true) ), ); +requestIdleCallback(() => + Promise.all( + Object.keys(remoteModules).map((identifier) => { + RootModule.INSTANCE.newChild(identifier, remoteModules[identifier]); + }), + ) +); diff --git a/protocol.ts b/protocol.ts index 4b90a65..025452c 100644 --- a/protocol.ts +++ b/protocol.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import type { LocalModuleInstance, Module, RemoteModuleInstance } from "./module.ts"; +import type { ModuleInstance, ModuleBase } from "./module.ts"; import { stringifyUrlSearchParams } from "./util.js"; const workerProtocol = "https://bespoke.delusoire.workers.dev/protocol/"; @@ -71,26 +71,26 @@ const sendProtocolMessage = async (action: string, options: Record) { + async disable(module: ModuleBase) { return await sendProtocolMessage("enable", { id: `${module.getIdentifier()}@` }); }, - async delete(instance: LocalModuleInstance) { + async delete(instance: ModuleInstance) { return await sendProtocolMessage("delete", { id: instance.getIdentifier() }); }, - async remove(instance: LocalModuleInstance) { + async remove(instance: ModuleInstance) { return await sendProtocolMessage("remove", { id: instance.getIdentifier() }); }, }; diff --git a/transform.ts b/transform.ts index 7cc1d9f..ecc4c7c 100644 --- a/transform.ts +++ b/transform.ts @@ -17,7 +17,7 @@ export class SourceFile { static SOURCES = new Map(); - private constructor(private path: string) {} + private constructor(private path: string) { } static from(path: string) { return SourceFile.SOURCES.get(path) ?? new SourceFile(path); @@ -30,7 +30,7 @@ export class SourceFile { return this.path; } const content = await fetchText(this.path); - const modifiedContent = trs.reduce((p, [, transform]) => transform(p, this.path), content); + const modifiedContent = trs.reduce((p, [, transform]) => transform(p, this.path), content!); const ext = this.path.slice(this.path.lastIndexOf(".")); // @ts-ignore const type: string | undefined = MimeTypes[ext]; @@ -54,18 +54,18 @@ export type MixinProps = { export type Transformer = ReturnType; export const createTransformer = (module: MixinLoader) => -( - transform: (emit: Thunk) => (input: string, path: string) => string, - { then = () => {}, glob = /(?:)/, noAwait = false }: MixinProps, -) => { - const { promise, resolve } = Promise.withResolvers(); + ( + transform: (emit: Thunk) => (input: string, path: string) => string, + { then = () => { }, glob = /(?:)/, noAwait = false }: MixinProps, + ) => { + const { promise, resolve } = Promise.withResolvers(); - transforms.push([glob, transform(resolve)]); + transforms.push([glob, transform(resolve)]); - promise.then(then); - // @ts-ignore - promise.transform = transform; - noAwait || module.awaitedMixins.push(promise as Promise); -}; + promise.then(then); + // @ts-ignore + promise.transform = transform; + noAwait || module.awaitedMixins.push(promise as Promise); + }; export const transforms = new Array<[RegExp, (input: string, path: string) => string]>(); diff --git a/util.ts b/util.ts index 0af777e..ff22a93 100644 --- a/util.ts +++ b/util.ts @@ -19,8 +19,9 @@ export function findBy(...tests: Array>) { return (xs: A[]) => xs.find(testFn)!; } -export const fetchText = (path: string) => fetch(path).then((res) => res.text()); -export const fetchJson = (path: string) => fetch(path).then((res) => res.json()) as Promise; +export const fetchText = (path: string) => fetch(path).then((res) => res.text()).catch(() => null); +export const fetchJson = (path: string) => + fetch(path).then((res) => res.json()).catch(() => null) as Promise; // assumption: str[start] === pair[0] export const findMatchingPos = (