diff --git a/packages/core/src/Application.ts b/packages/core/src/Application.ts index 6986660..3b05d1e 100644 --- a/packages/core/src/Application.ts +++ b/packages/core/src/Application.ts @@ -1,8 +1,16 @@ import { Container, interfaces } from 'inversify'; import { IApplication } from './IApplication'; import { IModule } from './Modules/IModule'; +import { IApplicationRegistration } from './IApplicationRegistration'; + +export class Application implements IApplication, IApplicationRegistration { + public registerConstant(service: T, identifier: symbol) { + if (this.container.isBound(identifier)) { + this.container.unbind(identifier); + } + this.container.bind(identifier).toConstantValue(service); + } -export class Application implements IApplication { public register(service: interfaces.Newable, identifier: symbol) { if (this.container.isBound(identifier)) { this.container.unbind(identifier); @@ -10,12 +18,12 @@ export class Application implements IApplication { this.container.bind(identifier).to(service); } - getService(identifier: symbol): T | undefined { - return this.container.get(identifier); + public use(module: IModule): void { + module(this); } - use(module: IModule): void { - module(this); + public getService(identifier: symbol): T | undefined { + return this.container.get(identifier); } /** diff --git a/packages/core/src/IApplication.ts b/packages/core/src/IApplication.ts index 593a0ef..16b58d0 100644 --- a/packages/core/src/IApplication.ts +++ b/packages/core/src/IApplication.ts @@ -1,27 +1,11 @@ -import { interfaces } from 'inversify'; -import { IModule } from './Modules/IModule'; - /** * The main application holding everything together. */ export interface IApplication { - /** - * Registers a new service. - * @param service service to register. - * @param identifier symbol for the service. - */ - register(service: interfaces.ServiceIdentifier, identifier: symbol): void; - /** * Gets a service identified by its symbol. * @param identifier symbol for the service. * @returns The requested service. */ getService(identifier: symbol): T | undefined; - - /** - * Registers a {@link IModule}. - * @param module {@link IModule} to use in the {@link IApplication}. - */ - use(module: IModule): void; } diff --git a/packages/core/src/IApplicationRegistration.ts b/packages/core/src/IApplicationRegistration.ts new file mode 100644 index 0000000..f9b0e4c --- /dev/null +++ b/packages/core/src/IApplicationRegistration.ts @@ -0,0 +1,24 @@ +import { IModule } from './Modules'; +import { interfaces } from 'inversify'; + +export interface IApplicationRegistration { + /** + * Registers a new service. + * @param service service to register. + * @param identifier symbol for the service. + */ + register(service: interfaces.ServiceIdentifier, identifier: symbol): void; + + /** + * Registers a new constant. + * @param service service to register. + * @param identifier symbol for the service. + */ + registerConstant(service: T, identifier: symbol): void; + + /** + * Registers a {@link IModule}. + * @param module {@link IModule} to use in the {@link IApplication}. + */ + use(module: IModule): void; +} diff --git a/packages/core/src/Modules/CoreModule.ts b/packages/core/src/Modules/CoreModule.ts index 8bb5fc2..750e121 100644 --- a/packages/core/src/Modules/CoreModule.ts +++ b/packages/core/src/Modules/CoreModule.ts @@ -1,9 +1,11 @@ -import { IKeyValueStoreSymbol, IObjectStoreSymbol } from 'ipmc-interfaces'; +import { IIndexManagerSymbol, IKeyValueStoreSymbol, IObjectStoreSymbol } from 'ipmc-interfaces'; import { MemoryKeyValueStore } from '../Services/MemoryKeyValueStore'; import { ObjectStore } from '../Services/ObjectStore'; import { IModule } from './IModule'; +import { IndexManager } from '../Services/IndexManager'; export const CoreModule: IModule = (app) => { app.register(MemoryKeyValueStore, IKeyValueStoreSymbol); app.register(ObjectStore, IObjectStoreSymbol); + app.register(IndexManager, IIndexManagerSymbol); }; diff --git a/packages/core/src/Modules/IModule.ts b/packages/core/src/Modules/IModule.ts index 0a534df..da60661 100644 --- a/packages/core/src/Modules/IModule.ts +++ b/packages/core/src/Modules/IModule.ts @@ -1,7 +1,7 @@ -import { IApplication } from '../IApplication'; +import { IApplicationRegistration } from '../IApplicationRegistration'; /** * A Module for an {@link IApplication}. * @param app the instance of an {@link IApplication}. */ -export type IModule = (app: IApplication) => void; +export type IModule = (app: IApplicationRegistration) => void; diff --git a/packages/core/src/Services/IndexManager.ts b/packages/core/src/Services/IndexManager.ts new file mode 100644 index 0000000..bfe3f01 --- /dev/null +++ b/packages/core/src/Services/IndexManager.ts @@ -0,0 +1,92 @@ +import { Signal } from '@preact/signals-core'; +import { inject, injectable, postConstruct, preDestroy } from 'inversify'; +import { IIndexManager, IIpfsService, IIpfsServiceSymbol, ILibrary, ILibraryIndex, IObjectStore, IObjectStoreSymbol, IProfile, IProfileSymbol, ITask, isMovieLibrary, isSeriesLibrary } from 'ipmc-interfaces'; +import { MovieIndexFetcher, SeriesIndexFetcher } from './Indexer'; + +@injectable() +export class IndexManager implements IIndexManager { + public constructor( + @inject(IProfileSymbol) private readonly profile: IProfile, + @inject(IIpfsServiceSymbol) private readonly ipfs: IIpfsService, + @inject(IObjectStoreSymbol) private readonly objectStore: IObjectStore, + ) { + for (const lib of this.profile.libraries) { + this.libraries.set(lib.name, new Signal(lib)); + const indexSignal = new Signal | undefined>(this.objectStore.get(this.getIndexStorageKey(lib.name))); + this.indexes.set(lib.name, indexSignal); + indexSignal.subscribe((newState) => { + if (newState !== undefined) { + this.objectStore.set(this.getIndexStorageKey(lib.name), newState); + } + }); + } + } + + @postConstruct() + public start(): void { + this.triggerUpdate(); + this.timer = setInterval(() => { + this.triggerUpdate(); + }, 15 * 60 * 1000); + } + + @preDestroy() + public stop(): void { + clearInterval(this.timer); + } + + public indexes = new Map | undefined>>(); + + public tasks = new Signal([]); + + private getIndexStorageKey(name: string) { + return `${this.profile.id}_index_${name}`; + } + + private triggerUpdate(): void { + for (const library of this.libraries.values()) { + const lib = library.value; + if (!this.updates.has(lib.name)) { + const task = { + title: 'Updating library ' + lib.name, + }; + this.tasks.value = [...this.tasks.value, task]; + this.updates.set(lib.name, this.updateLibrary(lib).finally(() => { + this.updates.delete(lib.name); + this.tasks.value = this.tasks.value.filter(t => t != task); + })); + } + } + } + + private async updateLibrary(library: ILibrary): Promise { + const index = this.indexes.get(library.name); + if (library.upstream != undefined && index != undefined) { + try { + const cid = await this.ipfs.resolve(library.upstream); + const indexer = isMovieLibrary(library) ? new MovieIndexFetcher(this.ipfs, library) : isSeriesLibrary(library) ? new SeriesIndexFetcher(this.ipfs, library) : undefined; + if (index.value?.cid != cid || indexer?.version !== index.value?.indexer) { + if (indexer == undefined) { + throw new Error(`Unknown library type [${library.type}]`); + } + + const newIndex = await indexer.fetchIndex(); + + index.value = { + cid: cid, + indexer: indexer.version, + index: newIndex, + }; + } + } catch (ex) { + console.error(ex); + } + } + } + + private libraries = new Map>(); + + private updates = new Map>(); + + private timer: any; +} diff --git a/packages/core/src/Services/Indexer/IIndexFetcher.ts b/packages/core/src/Services/Indexer/IIndexFetcher.ts index 38f278a..fb1b1ad 100644 --- a/packages/core/src/Services/Indexer/IIndexFetcher.ts +++ b/packages/core/src/Services/Indexer/IIndexFetcher.ts @@ -1,3 +1,4 @@ export interface IIndexFetcher { fetchIndex(): Promise; + version: string; } diff --git a/packages/core/src/Services/Indexer/MovieIndexFetcher.ts b/packages/core/src/Services/Indexer/MovieIndexFetcher.ts index 4f8c35f..2f201e0 100644 --- a/packages/core/src/Services/Indexer/MovieIndexFetcher.ts +++ b/packages/core/src/Services/Indexer/MovieIndexFetcher.ts @@ -6,6 +6,8 @@ export class MovieIndexFetcher implements IIndexFetcher { constructor(private readonly node: IIpfsService, private readonly lib: IGenericLibrary) { } + public version = '0'; + public async fetchIndex(): Promise { const files = (await this.node.ls(this.lib.root.toString())).filter(f => f.type == 'dir'); const index = []; diff --git a/packages/core/src/Services/Indexer/SeriesIndexFetcher.ts b/packages/core/src/Services/Indexer/SeriesIndexFetcher.ts index 05230ee..af6424a 100644 --- a/packages/core/src/Services/Indexer/SeriesIndexFetcher.ts +++ b/packages/core/src/Services/Indexer/SeriesIndexFetcher.ts @@ -6,6 +6,8 @@ export class SeriesIndexFetcher implements IIndexFetcher { constructor(private readonly node: IIpfsService, private readonly lib: IGenericLibrary) { } + public version = '0'; + public async fetchIndex(): Promise { const files = (await this.node.ls(this.lib.root.toString())).filter(f => f.type == 'dir'); const index = []; diff --git a/packages/core/src/Services/Indexer/index.ts b/packages/core/src/Services/Indexer/index.ts index a4da321..e582c30 100644 --- a/packages/core/src/Services/Indexer/index.ts +++ b/packages/core/src/Services/Indexer/index.ts @@ -1,2 +1,3 @@ +export { type IIndexFetcher } from './IIndexFetcher'; export { MovieIndexFetcher } from './MovieIndexFetcher'; export { SeriesIndexFetcher } from './SeriesIndexFetcher'; diff --git a/packages/core/src/Services/ProfileManager/BaseProfileManager.ts b/packages/core/src/Services/ProfileManager/BaseProfileManager.ts deleted file mode 100644 index d3842f8..0000000 --- a/packages/core/src/Services/ProfileManager/BaseProfileManager.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Signal } from "@preact/signals-react"; -import { IIpfsService, ILibrary, IProfile, isMovieLibrary, isSeriesLibrary, ITask } from 'ipmc-interfaces'; -import { MovieIndexFetcher, SeriesIndexFetcher } from '../Indexer'; -import { IProfileManager, ProfileManagerState } from 'ipmc-interfaces'; - -export abstract class BaseProfileManager implements IProfileManager { - protected constructor(public readonly profile: TProfile) { - for (const lib of this.profile.libraries) { - this.libraries.set(lib.name, new Signal(lib)); - } - } - - public start(): Promise { - return this.startNode() - .then(() => { - this.triggerUpdate(); - this.timer = setInterval(() => { - this.triggerUpdate(); - }, 15 * 60 * 1000); - this.state.value = ProfileManagerState.Running; - }); - } - - public stop(): Promise { - clearInterval(this.timer); - return this.stopNode() - .then(() => { - this.state.value = ProfileManagerState.Stopped; - }); - } - - public libraries = new Map>(); - - public state = new Signal(ProfileManagerState.Stopped); - - public tasks = new Signal([]); - - - protected abstract startNode(): Promise; - - protected abstract stopNode(): Promise; - - public ipfs: IIpfsService | undefined; - - - private triggerUpdate(): void { - for (const library of this.libraries.values()) { - const lib = library.value; - if (!this.updates.has(lib.name)) { - const task = { - title: 'Updating library ' + lib.name, - }; - this.tasks.value = [...this.tasks.value, task]; - this.updates.set(lib.name, this.updateLibrary(lib).finally(() => { - this.updates.delete(lib.name); - this.tasks.value = this.tasks.value.filter(t => t != task); - })); - } - } - } - - private async updateLibrary(library: ILibrary): Promise { - if (library.upstream != undefined) { - try { - const cid = await this.ipfs!.resolve(library.upstream); - if (library.root != cid) { - const lib = this.libraries.get(library.name); - if (lib != undefined) { - lib.value = { - ...lib.value, root: cid - }; - } - } - } catch (ex) { - console.error(ex); - } - } - - if (library.index?.cid !== library.root || library.index?.cid !== undefined) { - const lib = this.libraries.get(library.name); - if (lib != undefined) { - const indexer = isMovieLibrary(lib.value) ? new MovieIndexFetcher(this.ipfs!, lib.value) : isSeriesLibrary(lib.value) ? new SeriesIndexFetcher(this.ipfs!, lib.value) : undefined; - if (indexer == undefined) { - throw new Error(`Unknown library type [${library.type}]`); - } - - const index = await indexer.fetchIndex(); - - //@ts-ignore - lib.value = { - ...lib.value, index: { - values: index, - cid: lib.value.root, - } - }; - } - } - } - - private updates = new Map>(); - - private timer: any; -} diff --git a/packages/core/src/Services/ProfileManager/RemoteProfileManager.ts b/packages/core/src/Services/ProfileManager/RemoteProfileManager.ts deleted file mode 100644 index 4a6ddc3..0000000 --- a/packages/core/src/Services/ProfileManager/RemoteProfileManager.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { IFileInfo, IRemoteProfile } from "ipmc-interfaces"; -import { BaseProfileManager } from "./BaseProfileManager"; -import { create } from 'kubo-rpc-client'; - -export class RemoteProfileManager extends BaseProfileManager { - constructor(profile: IRemoteProfile) { - super(profile); - } - - protected async startNode() { - const node = create({ url: this.profile.url }); - const connString = (await node.config.get('Addresses.Gateway')) as string; - const port = connString.substring(connString.lastIndexOf('/') + 1); - const id = (await node.id()).id.toString(); - - this.ipfs = { - async ls(cid: string) { - const files: IFileInfo[] = []; - for await (const file of node.ls(cid)) { - files.push({ - type: file.type, - name: file.name, - cid: file.cid.toString(), - }); - } - return files; - }, - stop() { - return Promise.resolve(); - }, - toUrl(cid: string) { - return `http://127.0.0.1:${port}/ipfs/${cid}`; - }, - id() { - return id; - }, - async peers() { - return (await node.swarm.peers()).map(p => p.addr.toString() + '/' + p.peer.toString()); - }, - async resolve(name) { - let result = ''; - for await (const res of node.name.resolve(name.at(0) == '/' ? name : '/ipns/' + name)) { - result = res; - } - - return result; - }, - async isPinned(cid) { - for await (const res of node.pin.ls({ - paths: cid, - })) { - if (res.cid.toString() == cid) { - return true; - } - } - return false; - }, - async addPin(cid) { - await node.pin.add(cid); - }, - async rmPin(cid) { - await node.pin.rm(cid); - }, - }; - } - - protected stopNode() { - return Promise.resolve(); - } -} diff --git a/packages/core/src/Services/ProfileManager/SimpleProfileManager.ts b/packages/core/src/Services/ProfileManager/SimpleProfileManager.ts deleted file mode 100644 index 66fa958..0000000 --- a/packages/core/src/Services/ProfileManager/SimpleProfileManager.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IIpfsService, IProfile } from 'ipmc-interfaces'; -import { BaseProfileManager } from './BaseProfileManager'; - -export class SimpleProfileManager extends BaseProfileManager { - constructor(ipfs: IIpfsService, profile: IProfile) { - super(profile); - this.ipfs = ipfs; - } - - protected startNode(): Promise { - return Promise.resolve(); - } - - protected stopNode(): Promise { - return this.ipfs?.stop() ?? Promise.resolve(); - } -} diff --git a/packages/core/src/Services/ProfileManager/index.ts b/packages/core/src/Services/ProfileManager/index.ts deleted file mode 100644 index 9791a28..0000000 --- a/packages/core/src/Services/ProfileManager/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { BaseProfileManager } from './BaseProfileManager'; -export { RemoteProfileManager } from './RemoteProfileManager'; -export { SimpleProfileManager } from './SimpleProfileManager'; diff --git a/packages/core/src/Services/index.ts b/packages/core/src/Services/index.ts index fd24320..0d6b510 100644 --- a/packages/core/src/Services/index.ts +++ b/packages/core/src/Services/index.ts @@ -1,5 +1,4 @@ export * from './Indexer'; -export * from './ProfileManager'; export { HotkeyService } from './HotkeyService'; export { LocalStorageKeyValueStore } from './LocalStorageKeyValueStore'; export { MemoryKeyValueStore } from './MemoryKeyValueStore'; diff --git a/packages/core/src/util/createRemoteIpfs.ts b/packages/core/src/util/createRemoteIpfs.ts new file mode 100644 index 0000000..0985316 --- /dev/null +++ b/packages/core/src/util/createRemoteIpfs.ts @@ -0,0 +1,59 @@ +import { IFileInfo, IIpfsService } from 'ipmc-interfaces'; +import { create } from 'kubo-rpc-client'; + +export async function createRemoteIpfs(url?: string): Promise { + const node = create({ url }); + const connString = (await node.config.get('Addresses.Gateway')) as string; + const port = connString.substring(connString.lastIndexOf('/') + 1); + const id = (await node.id()).id.toString(); + + return { + async ls(cid: string) { + const files: IFileInfo[] = []; + for await (const file of node.ls(cid)) { + files.push({ + type: file.type, + name: file.name, + cid: file.cid.toString(), + }); + } + return files; + }, + stop() { + return Promise.resolve(); + }, + toUrl(cid: string) { + return `http://127.0.0.1:${port}/ipfs/${cid}`; + }, + id() { + return id; + }, + async peers() { + return (await node.swarm.peers()).map(p => p.addr.toString() + '/' + p.peer.toString()); + }, + async resolve(name) { + let result = ''; + for await (const res of node.name.resolve(name.at(0) == '/' ? name : '/ipns/' + name)) { + result = res; + } + + return result; + }, + async isPinned(cid) { + for await (const res of node.pin.ls({ + paths: cid, + })) { + if (res.cid.toString() == cid) { + return true; + } + } + return false; + }, + async addPin(cid) { + await node.pin.add(cid); + }, + async rmPin(cid) { + await node.pin.rm(cid); + }, + }; +} diff --git a/packages/core/src/util/index.ts b/packages/core/src/util/index.ts index 9a421e6..2f98aae 100644 --- a/packages/core/src/util/index.ts +++ b/packages/core/src/util/index.ts @@ -1 +1,5 @@ +<<<<<<< HEAD export { createFilter } from './createFilter'; +======= +export { createRemoteIpfs } from './createRemoteIpfs'; +>>>>>>> b91302fed0641a56d3300fbe3e23c06e5913baf1 diff --git a/packages/desktop/src/renderer/src/App.tsx b/packages/desktop/src/renderer/src/App.tsx index 7eba760..56635b0 100644 --- a/packages/desktop/src/renderer/src/App.tsx +++ b/packages/desktop/src/renderer/src/App.tsx @@ -1,10 +1,14 @@ -import { IpmcApp } from "ipmc-ui"; +import { IpmcLauncher, ThemeContextProvider } from "ipmc-ui"; function App(): JSX.Element { - return ; + return ( + + + + ); } export default App; diff --git a/packages/interfaces/src/MetaData/ILibraryIndex.ts b/packages/interfaces/src/MetaData/ILibraryIndex.ts new file mode 100644 index 0000000..12a495f --- /dev/null +++ b/packages/interfaces/src/MetaData/ILibraryIndex.ts @@ -0,0 +1,5 @@ +export interface ILibraryIndex { + indexer: string; + cid: string, + index: TValues[]; +} diff --git a/packages/interfaces/src/MetaData/Library/LibraryType.ts b/packages/interfaces/src/MetaData/Library/LibraryType.ts new file mode 100644 index 0000000..874ccc6 --- /dev/null +++ b/packages/interfaces/src/MetaData/Library/LibraryType.ts @@ -0,0 +1,6 @@ +export enum LibraryType { + Movies, + Series, + Music, + Books, +} diff --git a/packages/interfaces/src/MetaData/Library/index.ts b/packages/interfaces/src/MetaData/Library/index.ts index 922fcba..fd54bc5 100644 --- a/packages/interfaces/src/MetaData/Library/index.ts +++ b/packages/interfaces/src/MetaData/Library/index.ts @@ -3,3 +3,4 @@ export type { ILibrary, IMovieLibrary } from './ILibrary'; export { isMovieLibrary, isMusicLibrary, isSeriesLibrary } from './ILibrary'; export type { IMovieMetaData } from './IMovieMetaData'; export type { IEpisodeMetaData, ISeriesMetaData, ISeasonMetaData } from './ISeriesMetaData'; +export { LibraryType } from './LibraryType'; diff --git a/packages/interfaces/src/MetaData/index.ts b/packages/interfaces/src/MetaData/index.ts index bfabf0a..1ce6e3d 100644 --- a/packages/interfaces/src/MetaData/index.ts +++ b/packages/interfaces/src/MetaData/index.ts @@ -1,10 +1,7 @@ export * from './Features'; export * from './Library'; -export type { IFileInfo } from './IFileInfo'; -export { isIFileInfo } from './IFileInfo'; -export type { IFolderFile } from './IFolderFile'; -export { isIFolderFile } from './IFolderFile'; -export type { IImageFile } from './IImageFile'; -export { isIImageFile } from './IImageFile'; -export type { IVideoFile } from './IVideoFile'; -export { isIVideoFile } from './IVideoFile'; +export { type IFileInfo, isIFileInfo } from './IFileInfo'; +export { type IFolderFile, isIFolderFile } from './IFolderFile'; +export { type IImageFile, isIImageFile } from './IImageFile'; +export { type ILibraryIndex } from './ILibraryIndex'; +export { type IVideoFile, isIVideoFile } from './IVideoFile'; diff --git a/packages/interfaces/src/Profile/IProfile.ts b/packages/interfaces/src/Profile/IProfile.ts index af7f89a..824cf68 100644 --- a/packages/interfaces/src/Profile/IProfile.ts +++ b/packages/interfaces/src/Profile/IProfile.ts @@ -1,6 +1,8 @@ import { IInternalProfile, isInternalProfile } from "./IInternalProfile"; import { IRemoteProfile, isRemoteProfile } from "./IRemoteProfile"; +export const IProfileSymbol = Symbol.for('IProfile'); + export type IProfile = IInternalProfile | IRemoteProfile; export function isIProfile(item: any): item is IProfile { diff --git a/packages/interfaces/src/Profile/index.ts b/packages/interfaces/src/Profile/index.ts index 650f053..e8ad959 100644 --- a/packages/interfaces/src/Profile/index.ts +++ b/packages/interfaces/src/Profile/index.ts @@ -1,6 +1,4 @@ -export type { IBaseProfile } from './IBaseProfile'; -export type { IInternalProfile } from './IInternalProfile'; -export { isInternalProfile } from './IInternalProfile'; -export type { IProfile } from './IProfile'; -export type { IRemoteProfile } from './IRemoteProfile'; -export { isRemoteProfile } from './IRemoteProfile'; +export { type IBaseProfile } from './IBaseProfile'; +export { type IInternalProfile, isInternalProfile } from './IInternalProfile'; +export { type IProfile, isIProfile, IProfileSymbol } from './IProfile'; +export { type IRemoteProfile, isRemoteProfile } from './IRemoteProfile'; diff --git a/packages/interfaces/src/Services/IIndexManager.ts b/packages/interfaces/src/Services/IIndexManager.ts new file mode 100644 index 0000000..ea046d3 --- /dev/null +++ b/packages/interfaces/src/Services/IIndexManager.ts @@ -0,0 +1,11 @@ +import { ReadonlySignal } from '@preact/signals-core'; +import { ILibraryIndex } from '../MetaData'; + +export const IIndexManagerSymbol = Symbol.for('IIndexManager'); + +export interface IIndexManager { + /** + * Currently available indexes. + */ + indexes: Map | undefined>>; +} diff --git a/packages/interfaces/src/Services/IIpfsService.ts b/packages/interfaces/src/Services/IIpfsService.ts index f3d033b..4a79f73 100644 --- a/packages/interfaces/src/Services/IIpfsService.ts +++ b/packages/interfaces/src/Services/IIpfsService.ts @@ -1,5 +1,7 @@ import { IFileInfo } from '../MetaData'; +export const IIpfsServiceSymbol = Symbol.for('IIpfsService'); + export interface IIpfsService { /** * Checks whether a cid is pinned. diff --git a/packages/interfaces/src/Services/IProfileManager.ts b/packages/interfaces/src/Services/IProfileManager.ts deleted file mode 100644 index 518a5b2..0000000 --- a/packages/interfaces/src/Services/IProfileManager.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ReadonlySignal } from '@preact/signals-react'; -import { ILibrary } from '../MetaData'; -import { ITask } from '../ITask'; -import { IProfile } from '../Profile'; -import { IIpfsService } from './IIpfsService'; - -/** - * Manages a profile. - */ -export interface IProfileManager { - /** - * Starts the service. - */ - start(): Promise; - - /** - * Stops the service. - */ - stop(): Promise; - - /** - * Currently available libraries. - */ - libraries: Map>; - - /** - * Current status of the service. - */ - state: ReadonlySignal; - - /** - * Current tasks of the service. - */ - tasks: ReadonlySignal; - - /** - * The profile of the service. - */ - profile: IProfile; - - /** - * Ipfs service of the profile if started. - */ - ipfs: undefined | IIpfsService; -} - -export enum ProfileManagerState { - Running, - Stopped, - Error, -} diff --git a/packages/interfaces/src/Services/index.ts b/packages/interfaces/src/Services/index.ts index 891d744..8a3a441 100644 --- a/packages/interfaces/src/Services/index.ts +++ b/packages/interfaces/src/Services/index.ts @@ -1,7 +1,7 @@ export { type IConfigurationService } from './IConfigurationService'; export { IHotkeyServiceSymbol, type IHotkeyService, type IHotkey } from './IHotkeyService'; -export { type IIpfsService } from './IIpfsService'; +export { type IIndexManager, IIndexManagerSymbol } from './IIndexManager'; +export { type IIpfsService, IIpfsServiceSymbol } from './IIpfsService'; export { type IKeyValueStore, IKeyValueStoreSymbol } from './IKeyValueStore'; export { type INodeService } from './INodeService'; export { type IObjectStore, IObjectStoreSymbol } from './IObjectStore'; -export { type IProfileManager, ProfileManagerState } from './IProfileManager'; diff --git a/packages/ui/src/IpmcApp.tsx b/packages/ui/src/IpmcApp.tsx index 9bf1974..492b323 100644 --- a/packages/ui/src/IpmcApp.tsx +++ b/packages/ui/src/IpmcApp.tsx @@ -1,30 +1,33 @@ import React from 'react'; import { LibraryManager } from "./components/pages/LibraryManager"; -import { AppContextProvider } from './context/AppContext'; -import { IProfileInit, ProfileContextProvider } from "./context/ProfileContext"; -import { Route, Router, Switch } from 'wouter'; -import { useHashLocation } from "wouter/use-hash-location"; -import { BrowserModule, CoreModule } from 'ipmc-core'; +import { Route, Switch } from 'wouter'; +import { BrowserModule, CoreModule, IModule } from 'ipmc-core'; +import { AppBar } from './components/organisms/AppBar'; +import { AppContextProvider } from './context'; // Setup translations import './i18n'; -export function IpmcApp(props: IProfileInit) { +export interface IIpmcAppProps { + setup?: IModule; +} + +export function IpmcApp(props: IIpmcAppProps) { + const { setup } = props; + return ( { app.use(CoreModule); app.use(BrowserModule); + setup && app.use(setup); }}> - - - - - - - 404: No such page! - - - + + + + + + 404: No such page! + ); } diff --git a/packages/ui/src/IpmcLauncher.tsx b/packages/ui/src/IpmcLauncher.tsx new file mode 100644 index 0000000..db8cc67 --- /dev/null +++ b/packages/ui/src/IpmcLauncher.tsx @@ -0,0 +1,118 @@ +import React, { PropsWithChildren } from 'react'; +import { useComputed, useSignal } from '@preact/signals-react'; +import { useTranslation } from './hooks'; +import { IConfigurationService, IIpfsService, IIpfsServiceSymbol, INodeService, IProfile, IProfileSymbol, isInternalProfile, isRemoteProfile } from 'ipmc-interfaces'; +import { createRemoteIpfs } from 'ipmc-core'; +import { IpmcApp } from './IpmcApp'; +import { Alert, Box, Button, ButtonGroup, Stack } from '@mui/material'; +import { ProfileSelector } from './components/molecules/ProfileSelector'; +import { LoadScreen } from './components/molecules/LoadScreen'; + +enum LoadState { + Idle, + Ready, + Starting, + Stopping, + Error, +} + +export interface IIpmcLauncherProps { + configService: IConfigurationService; + nodeService: INodeService; +} + +export const IReturnToLauncherActionSymbol = Symbol.for('IReturnToLauncherAction'); + +export type IReturnToLauncherAction = () => void; + +export function IpmcLauncher(props: PropsWithChildren) { + const _t = useTranslation(); + + const state = useSignal(LoadState.Idle); + const node = useSignal(undefined); + const profile = useSignal(undefined); + + async function start(name: string) { + if (name == undefined) return; + + const currentProfile = props.configService.getProfile(name); + if (currentProfile != undefined) { + profile.value = currentProfile; + try { + state.value = LoadState.Starting; + + if (isRemoteProfile(currentProfile)) { + node.value = await createRemoteIpfs(currentProfile.url); + } + if (isInternalProfile(currentProfile)) { + node.value = await props.nodeService.create(currentProfile); + } + + state.value = LoadState.Ready; + } catch (ex) { + console.error(ex); + state.value = LoadState.Error; + } + } + } + + async function stop() { + if (node.value != undefined) { + state.value = LoadState.Stopping; + try { + await node.value!.stop(); + node.value = undefined; + profile.value = undefined; + } catch (ex) { + console.error(ex); + state.value = LoadState.Error; + } + } + state.value = LoadState.Idle; + } + + const content = useComputed(() => { + const ipfs = node.value; + const currentState = state.value; + const currentProfile = profile.value; + + switch (currentState) { + case LoadState.Error: + return ( + + An error occured. + + + + + ); + case LoadState.Idle: + return ( + + + + ); + case LoadState.Starting: + case LoadState.Stopping: + return ( + + ); + case LoadState.Ready: + return ( + { + app.registerConstant(ipfs!, IIpfsServiceSymbol); + app.registerConstant(currentProfile!, IProfileSymbol); + app.registerConstant(stop, IReturnToLauncherActionSymbol); + }} /> + ); + default: + return (<>); + } + }); + + return useComputed(() => ( + + {content} + + )); +} diff --git a/packages/ui/src/components/atoms/FileInfoDisplay.tsx b/packages/ui/src/components/atoms/FileInfoDisplay.tsx index 6380586..995e574 100644 --- a/packages/ui/src/components/atoms/FileInfoDisplay.tsx +++ b/packages/ui/src/components/atoms/FileInfoDisplay.tsx @@ -1,12 +1,12 @@ import { Box, Stack, Typography } from '@mui/material'; -import { IFileInfo, isPosterFeature } from 'ipmc-interfaces'; +import { IFileInfo, IIpfsService, IIpfsServiceSymbol, isPosterFeature } from 'ipmc-interfaces'; import React from 'react'; -import { useProfile } from '../../context/ProfileContext'; import { useTitle } from '../../hooks/useTitle'; +import { useService } from '../../context/AppContext'; export function FileInfoDisplay(props: { file: IFileInfo; }) { const { file } = props; - const { ipfs } = useProfile(); + const ipfs = useService(IIpfsServiceSymbol); const name = useTitle(file); const title = ( {name} diff --git a/packages/ui/src/components/organisms/AppBar.tsx b/packages/ui/src/components/organisms/AppBar.tsx index 2a82711..6f9e74c 100644 --- a/packages/ui/src/components/organisms/AppBar.tsx +++ b/packages/ui/src/components/organisms/AppBar.tsx @@ -2,33 +2,27 @@ import { Box, Button, Toolbar } from "@mui/material"; import React from "react"; import { ConnectionStatus } from "../molecules/ConnectionStatus"; import { LanguageSelector } from "../molecules/LanguageSelector"; -import { Signal, useComputed } from '@preact/signals-react'; import { ThemeToggle } from '../atoms/ThemeToggle'; import { useTranslation } from '../../hooks/useTranslation'; -import { IIpfsService, IProfile } from 'ipmc-interfaces'; +import { IIpfsService, IIpfsServiceSymbol, IProfile, IProfileSymbol } from 'ipmc-interfaces'; import { useTheme } from '../../context/ThemeContext'; +import { useOptionalService, useService } from '../../context'; +import { IReturnToLauncherAction, IReturnToLauncherActionSymbol } from '../../IpmcLauncher'; -export function AppBar(props: { - shutdownProfile: () => void; - ipfs: Signal; - profile: Signal; -}) { - const { shutdownProfile, ipfs, profile } = props; +export function AppBar() { + const shutdownProfile = useOptionalService(IReturnToLauncherActionSymbol); + const ipfs = useService(IIpfsServiceSymbol); + const profile = useService(IProfileSymbol); const _t = useTranslation(); const { darkMode } = useTheme(); - const status = useComputed(() => ipfs.value != undefined && ()); - const logout = useComputed(() => ipfs.value != undefined && (<> - - {profile.value?.name} - )); - return ( - {logout} + {shutdownProfile && } + {profile.name} - {status} + { darkMode.value = !darkMode.value; }} /> diff --git a/packages/ui/src/components/organisms/Library.tsx b/packages/ui/src/components/organisms/Library.tsx index 62c01d6..7e86700 100644 --- a/packages/ui/src/components/organisms/Library.tsx +++ b/packages/ui/src/components/organisms/Library.tsx @@ -4,23 +4,20 @@ import { FileView } from './FileView'; import { ReadonlySignal, useComputed, useSignal } from '@preact/signals-react'; import { LoadScreen } from '../molecules/LoadScreen'; import { Grid } from '@mui/material'; -import { IFileInfo } from 'ipmc-interfaces'; +import { IFileInfo, IIndexManager, IIndexManagerSymbol } from 'ipmc-interfaces'; import { Display } from '../pages/LibraryManager'; -import { useProfile } from '../../context/ProfileContext'; -import { useWatcher } from '../../hooks'; import { ErrorBoundary } from '../atoms/ErrorBoundary'; import { createFilter } from 'ipmc-core'; +import { useService } from '../../context/AppContext'; export function Library(props: { display: ReadonlySignal; query: ReadonlySignal; library: string; }) { - const { display, library, query } = props; - const { profile } = useProfile(); - + const { display, library } = props; + const index = useService(IIndexManagerSymbol).indexes.get(library)!; const selected = useSignal(undefined); - const index = useWatcher<{ cid: string; values: IFileInfo[]; } | undefined>(profile.libraries.get(library)?.value.index as { cid: string; values: IFileInfo[]; } | undefined); const detail = useComputed(() => selected.value !== undefined ? ( @@ -42,7 +39,7 @@ export function Library(props: { ) : ( <> - {(q === undefined ? i.values : i.values.filter(createFilter(q))).map(v => ( + {(q === undefined ? i.index : i.index.filter(createFilter(q))).map(v => ( profile.tasks.value.length > 0 ? ( + /*const status = useComputed(() => profile.tasks.value.length > 0 ? ( {profile.tasks.value.map(t => ( @@ -21,10 +17,10 @@ export function LibraryHomeScreen() { ))} - ) : undefined); + ) : undefined);*/ return ( {_t('Home')} - {status} + {/*status*/} ); } diff --git a/packages/ui/src/components/organisms/VideoPlayer.tsx b/packages/ui/src/components/organisms/VideoPlayer.tsx index 181fff2..738ac5d 100644 --- a/packages/ui/src/components/organisms/VideoPlayer.tsx +++ b/packages/ui/src/components/organisms/VideoPlayer.tsx @@ -1,10 +1,10 @@ import React, { useRef } from 'react'; -import { IVideoFile } from 'ipmc-interfaces'; -import { useProfile } from '../../context/ProfileContext'; +import { IIpfsService, IIpfsServiceSymbol, IVideoFile } from 'ipmc-interfaces'; import { useHotkey } from '../../hooks'; +import { useService } from '../../context'; export function VideoPlayer(props: { file: IVideoFile; }) { - const { ipfs } = useProfile(); + const ipfs = useService(IIpfsServiceSymbol); const videoRef = useRef(null); useHotkey({ key: 'F' }, () => { diff --git a/packages/ui/src/components/pages/LibraryManager.tsx b/packages/ui/src/components/pages/LibraryManager.tsx index 6b1ef57..7d7211a 100644 --- a/packages/ui/src/components/pages/LibraryManager.tsx +++ b/packages/ui/src/components/pages/LibraryManager.tsx @@ -5,15 +5,15 @@ import MusicNoteIcon from '@mui/icons-material/MusicNote'; import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Paper, Stack } from '@mui/material'; import { Signal, useComputed, useSignal } from "@preact/signals-react"; +import { ILibrary, IProfile, IProfileSymbol } from "ipmc-interfaces"; import React from "react"; -import { LibraryAppBar } from "../organisms/LibraryAppBar"; -import { useProfile } from "../../context/ProfileContext"; -import { LibraryHomeScreen } from '../organisms/LibraryHomeScreen'; +import { Route, useLocation } from 'wouter'; +import { useService } from '../../context/AppContext'; import { useTranslation } from '../../hooks/useTranslation'; -import { Library } from '../organisms/Library'; import { ErrorBoundary } from '../atoms/ErrorBoundary'; -import { Route, useLocation, useRoute } from 'wouter'; -import { useWatcher } from '../../hooks'; +import { Library } from '../organisms/Library'; +import { LibraryAppBar } from "../organisms/LibraryAppBar"; +import { LibraryHomeScreen } from '../organisms/LibraryHomeScreen'; const icons = { movie: , @@ -22,15 +22,13 @@ const icons = { } as { [key: string]: any; }; export function LibraryManager() { - const { profile } = useProfile(); - const [, params] = useRoute<{ library: string; }>('/:library'); - const [, setLocation] = useLocation(); + const profile = useService(IProfileSymbol); const _t = useTranslation(); - - const libraries = profile.profile.libraries; - const library = useWatcher(() => params?.library !== undefined ? profile.libraries.get(params?.library) : undefined); + const libraries = profile.libraries; + const library = useSignal(undefined); const display = useSignal(Display.Poster); const query = useSignal(''); + const [_, setLocation] = useLocation(); const sidebar = useComputed(() => ( @@ -49,7 +47,7 @@ export function LibraryManager() { {libraries.map((lib) => ( { setLocation('/' + lib.name); }}> diff --git a/packages/ui/src/context/AppContext.tsx b/packages/ui/src/context/AppContext.tsx index b891161..d1e2ded 100644 --- a/packages/ui/src/context/AppContext.tsx +++ b/packages/ui/src/context/AppContext.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { Application, IApplication } from 'ipmc-core'; +import { Application, IApplication, IModule } from 'ipmc-core'; import { PropsWithChildren, createContext } from 'react'; import { useComputed, useSignal, useSignalEffect } from '@preact/signals-react'; import { LoadScreen } from '../components/molecules/LoadScreen'; @@ -7,7 +7,7 @@ import { ThemeContextProvider } from './ThemeContext'; const AppContext = createContext({} as IApplication); -export function AppContextProvider(props: PropsWithChildren<{ setup: (app: IApplication) => void; }>) { +export function AppContextProvider(props: PropsWithChildren<{ setup: IModule; }>) { const application = useSignal(undefined); useSignalEffect(() => { @@ -29,7 +29,6 @@ export function AppContextProvider(props: PropsWithChildren<{ setup: (app: IAppl ); } - export function useApp() { return useContext(AppContext); } @@ -38,3 +37,13 @@ export function useService(identifier: symbol): T { const app = useApp(); return app.getService(identifier)!; } + +export function useOptionalService(identifier: symbol): T | undefined { + const app = useApp(); + try { + return app.getService(identifier)!; + } catch (ex) { + return undefined; + } +} + diff --git a/packages/ui/src/context/ProfileContext.tsx b/packages/ui/src/context/ProfileContext.tsx deleted file mode 100644 index 37be4ad..0000000 --- a/packages/ui/src/context/ProfileContext.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Alert, Box, Button, ButtonGroup, Stack } from "@mui/material"; -import { useComputed, useSignal } from "@preact/signals-react"; -import { - IIpfsService, - isInternalProfile, - isRemoteProfile, - IProfileManager, - IConfigurationService, - INodeService, -} from 'ipmc-interfaces'; -import React, { createContext, PropsWithChildren, useContext } from 'react'; -import { useTranslation } from '../hooks/useTranslation'; -import { SimpleProfileManager, RemoteProfileManager } from 'ipmc-core'; -import { LoadScreen } from "../components/molecules/LoadScreen"; -import { ProfileSelector } from "../components/molecules/ProfileSelector"; -import { AppBar } from "../components/organisms/AppBar"; - -export interface IProfileContext { - config: IConfigurationService; - ipfs: IIpfsService; - profile: IProfileManager; -} - -export interface IProfileInit { - configService: IConfigurationService; - nodeService: INodeService; -} - -const ProfileContext = createContext({} as IProfileContext); - -enum LoadState { - Idle, - Ready, - Starting, - Stopping, - Error, -} - -export function ProfileContextProvider(props: PropsWithChildren) { - const _t = useTranslation(); - - const profileManager = useSignal(undefined); - const state = useSignal(LoadState.Idle); - - async function start(name: string) { - if (name == undefined) return; - - const currentProfile = props.configService.getProfile(name); - if (currentProfile != undefined) { - try { - state.value = LoadState.Starting; - - let manager: IProfileManager | undefined; - if (isRemoteProfile(currentProfile)) { - manager = new RemoteProfileManager(currentProfile); - } - if (isInternalProfile(currentProfile)) { - manager = new SimpleProfileManager(await props.nodeService.create(currentProfile), currentProfile); - } - - if (manager) { - await manager.start(); - profileManager.value = manager; - state.value = LoadState.Ready; - } else { - state.value = LoadState.Error; - } - } catch (ex) { - console.error(ex); - state.value = LoadState.Error; - } - } - } - - async function stop() { - if (node.value != undefined) { - state.value = LoadState.Stopping; - try { - await profileManager.value!.stop(); - profileManager.value = undefined; - } catch (ex) { - console.error(ex); - state.value = LoadState.Error; - } - } - state.value = LoadState.Idle; - } - - const content = useComputed(() => { - const manager = profileManager.value; - const currentState = state.value; - - switch (currentState) { - case LoadState.Error: - return ( - - An error occured. - - - - - ); - case LoadState.Idle: - return ( - - - - ); - case LoadState.Starting: - case LoadState.Stopping: - return ( - - ); - case LoadState.Ready: - return ( - - {props.children} - - ); - default: - return (<>); - } - }); - - const profile = useComputed(() => profileManager.value?.profile); - const node = useComputed(() => profileManager.value?.ipfs); - - return useComputed(() => ( - - - {content} - - )); -} - -export function useProfile() { - return useContext(ProfileContext); -} diff --git a/packages/ui/src/context/index.ts b/packages/ui/src/context/index.ts new file mode 100644 index 0000000..3e45e71 --- /dev/null +++ b/packages/ui/src/context/index.ts @@ -0,0 +1,2 @@ +export { AppContextProvider, useApp, useService, useOptionalService } from './AppContext'; +export { type IThemeContext, ThemeContextProvider, useTheme } from './ThemeContext'; diff --git a/packages/ui/src/hooks/useFileUrl.ts b/packages/ui/src/hooks/useFileUrl.ts index 78ae34c..fa70769 100644 --- a/packages/ui/src/hooks/useFileUrl.ts +++ b/packages/ui/src/hooks/useFileUrl.ts @@ -1,7 +1,8 @@ -import { useProfile } from "../context/ProfileContext"; +import { IIpfsService, IIpfsServiceSymbol } from 'ipmc-interfaces'; +import { useService } from '../context/AppContext'; export function useFileUrl(cid?: string, fallback?: string): string | undefined { - const { ipfs } = useProfile(); + const ipfs = useService(IIpfsServiceSymbol); return cid ? ipfs.toUrl(cid) : fallback ?? undefined; } diff --git a/packages/ui/src/hooks/usePinManager.ts b/packages/ui/src/hooks/usePinManager.ts index cabb8fe..cd5facd 100644 --- a/packages/ui/src/hooks/usePinManager.ts +++ b/packages/ui/src/hooks/usePinManager.ts @@ -1,5 +1,6 @@ import { ReadonlySignal, useSignal, useSignalEffect } from '@preact/signals-react'; -import { useProfile } from '../context/ProfileContext'; +import { useService } from '../context/AppContext'; +import { IIpfsService, IIpfsServiceSymbol } from 'ipmc-interfaces'; export enum PinStatus { Unknown, @@ -10,7 +11,7 @@ export enum PinStatus { } export function usePinManager(cid: string): [ReadonlySignal, setState: (pin: boolean) => void] { - const { ipfs } = useProfile(); + const ipfs = useService(IIpfsServiceSymbol); const status = useSignal(PinStatus.Unknown); function setState(pin: boolean) { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d2672c1..01944c5 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1 +1,3 @@ export { IpmcApp } from './IpmcApp'; +export { IpmcLauncher } from './IpmcLauncher'; +export { ThemeContextProvider } from './context'; diff --git a/packages/webui/src/App.tsx b/packages/webui/src/App.tsx index 4f15425..9143d53 100644 --- a/packages/webui/src/App.tsx +++ b/packages/webui/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { IFileInfo } from 'ipmc-interfaces'; -import { IpmcApp } from 'ipmc-ui'; +import { IpmcLauncher, ThemeContextProvider } from 'ipmc-ui'; import { webSockets } from '@libp2p/websockets'; import { webTransport } from '@libp2p/webtransport'; @@ -18,114 +18,118 @@ import { unixfs } from '@helia/unixfs'; import { ipns } from '@helia/ipns'; export function App() { - return p == name)) { - window.localStorage.setItem('profiles', JSON.stringify([...profiles, name])); - } - }, - }} - nodeService={{ - async create(profile) { - const datastore = new IDBDatastore(`${profile?.name ?? 'default'}/data`); - await datastore.open(); - const blockstore = new IDBBlockstore(`${profile?.name ?? 'default'}/data`); - await blockstore.open(); - - const helia = await createHelia({ - start: true, - datastore, - blockstore, - libp2p: { - ...(profile?.swarmKey ? { - connectionProtector: preSharedKey({ - psk: new TextEncoder().encode(profile.swarmKey), - }), - } : {}), - transports: [ - webSockets(), - webTransport(), - ], - peerDiscovery: [ - bootstrap({ - list: profile?.bootstrap ?? [ - // a list of bootstrap peer multiaddrs to connect to on node startup - '/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', - '/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', - '/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa' - ] - }) - ], - connectionEncryption: [noise()], - }, - blockBrokers: [ - bitswap(), - ...(profile?.swarmKey == undefined ? [trustlessGateway()] : []) - ], - }); - - const fs = unixfs(helia); - - return ({ - async ls(cid: string) { - const files: IFileInfo[] = []; - for await (const file of fs.ls(CID.parse(cid))) { - files.push({ - type: file.type == 'directory' ? 'dir' : 'file', - name: file.name, - cid: file.cid.toString(), - }); - } - return files; - }, - async stop() { - await helia.stop(); - await blockstore.close(); - await datastore.close(); - }, - toUrl(cid: string) { - return `TODO ${cid}`; - }, - peers() { - return Promise.resolve(helia.libp2p.getConnections().map(p => p.remoteAddr.toString())); + return ( + + p == name)) { + window.localStorage.setItem('profiles', JSON.stringify([...profiles, name])); } }, - isPinned(cid) { - return helia.pins.isPinned(CID.parse(cid)); - }, - async addPin(cid) { - for await (const block of helia.pins.add(CID.parse(cid))) { - console.log(`Pin progress ${cid}: ${block.toString()}`); - } - }, - async rmPin(cid) { - for await (const block of helia.pins.rm(CID.parse(cid))) { - console.log(`Pin progress ${cid}: ${block.toString()}`); - } + }} + nodeService={{ + async create(profile) { + const datastore = new IDBDatastore(`${profile?.name ?? 'default'}/data`); + await datastore.open(); + const blockstore = new IDBBlockstore(`${profile?.name ?? 'default'}/data`); + await blockstore.open(); + + const helia = await createHelia({ + start: true, + datastore, + blockstore, + libp2p: { + ...(profile?.swarmKey ? { + connectionProtector: preSharedKey({ + psk: new TextEncoder().encode(profile.swarmKey), + }), + } : {}), + transports: [ + webSockets(), + webTransport(), + ], + peerDiscovery: [ + bootstrap({ + list: profile?.bootstrap ?? [ + // a list of bootstrap peer multiaddrs to connect to on node startup + '/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa' + ] + }) + ], + connectionEncryption: [noise()], + }, + blockBrokers: [ + bitswap(), + ...(profile?.swarmKey == undefined ? [trustlessGateway()] : []) + ], + }); + + const fs = unixfs(helia); + + return ({ + async ls(cid: string) { + const files: IFileInfo[] = []; + for await (const file of fs.ls(CID.parse(cid))) { + files.push({ + type: file.type == 'directory' ? 'dir' : 'file', + name: file.name, + cid: file.cid.toString(), + }); + } + return files; + }, + async stop() { + await helia.stop(); + await blockstore.close(); + await datastore.close(); + }, + toUrl(cid: string) { + return `TODO ${cid}`; + }, + peers() { + return Promise.resolve(helia.libp2p.getConnections().map(p => p.remoteAddr.toString())); + }, + id() { + return helia.libp2p.peerId.toString(); + }, + async resolve(name) { + try { + return (await ipns(helia).resolve(peerIdFromString(name))).cid.toString(); + } catch (ex) { + return (await ipns(helia).resolveDNSLink(name)).cid.toString(); + } + }, + isPinned(cid) { + return helia.pins.isPinned(CID.parse(cid)); + }, + async addPin(cid) { + for await (const block of helia.pins.add(CID.parse(cid))) { + console.log(`Pin progress ${cid}: ${block.toString()}`); + } + }, + async rmPin(cid) { + for await (const block of helia.pins.rm(CID.parse(cid))) { + console.log(`Pin progress ${cid}: ${block.toString()}`); + } + }, + }); }, - }); - }, - }} - />; + }} + /> + + ); };