import Dexie, { EntityTable } from "dexie"; import { Accessor, createContext, createMemo, onMount, ParentComponent, useContext } from "solid-js"; import { createStore } from "solid-js/store"; import { isServer } from "solid-js/web"; import * as json from './parser/json'; const ROOT = '__root__'; interface FileEntity { key: string; handle: FileSystemDirectoryHandle; } type Store = Dexie & { files: EntityTable; }; interface InternalFilesContextType { onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any): void; set(key: string, handle: FileSystemDirectoryHandle): Promise; get(key: string): Promise; remove(...keys: string[]): Promise; keys(): Promise; entries(): Promise; list(): Promise; } interface FilesContextType { readonly files: Accessor, readonly root: Accessor, readonly loading: Accessor, open(directory: FileSystemDirectoryHandle): void; get(key: string): Accessor set(key: string, handle: FileSystemDirectoryHandle): Promise; remove(key: string): Promise; } const FilesContext = createContext(); const clientContext = (): InternalFilesContextType => { const db = new Dexie('Files') as Store; db.version(1).stores({ files: 'key, handle' }); return { onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) { const callHook = (key: string, handle: FileSystemDirectoryHandle) => { if (!key || key === ROOT) { return; } setTimeout(() => hook(key, handle), 1); }; db.files.hook('creating', (_: string, { key, handle }: FileEntity) => { callHook(key, handle); }); db.files.hook('deleting', (_: string, { key, handle }: FileEntity = { key: undefined!, handle: undefined! }) => callHook(key, handle)); db.files.hook('updating', (_1: Object, _2: string, { key, handle }: FileEntity) => callHook(key, handle)); }, async set(key: string, handle: FileSystemDirectoryHandle) { await db.files.put({ key, handle }); }, async get(key: string) { return (await db.files.get(key))?.handle; }, async remove(...keys: string[]) { await Promise.all(keys.map(key => db.files.delete(key))); }, async keys() { return (await db.files.where('key').notEqual(ROOT).toArray()).map(f => f.key); }, async entries() { return await db.files.where('key').notEqual(ROOT).toArray(); }, async list() { const files = await db.files.where('key').notEqual(ROOT).toArray(); return files.map(f => f.handle) }, } }; const serverContext = (): InternalFilesContextType => ({ onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) { }, set(key: string, handle: FileSystemDirectoryHandle) { return Promise.resolve(); }, get(key: string) { return Promise.resolve(undefined); }, remove(...keys: string[]) { return Promise.resolve(undefined); }, keys() { return Promise.resolve([]); }, entries() { return Promise.resolve([]); }, list() { return Promise.resolve([]); }, }); export const FilesProvider: ParentComponent = (props) => { const internal = isServer ? serverContext() : clientContext(); const [state, setState] = createStore<{ loading: boolean, openedFiles: FileEntity[], root: FileSystemDirectoryHandle | undefined }>({ loading: true, openedFiles: [], root: undefined }); internal.onChange(async () => { setState('openedFiles', await internal.entries()); }); onMount(() => { (async () => { const [root, openedFiles] = await Promise.all([ internal.get(ROOT), internal.entries(), ]); setState(prev => ({ ...prev, loading: false, root, openedFiles })); })(); }); const context: FilesContextType = { files: createMemo(() => state.openedFiles), root: createMemo(() => state.root), loading: createMemo(() => state.loading), async open(directory: FileSystemDirectoryHandle) { await internal.remove(...(await internal.keys())); setState('root', directory); await internal.set(ROOT, directory); }, get(key: string): Accessor { return createMemo(() => state.openedFiles.find(entity => entity.key === key)?.handle); }, async set(key: string, handle: FileSystemDirectoryHandle) { await internal.set(key, handle); }, async remove(key: string) { await internal.remove(key); }, }; return {props.children}; } export const useFiles = () => useContext(FilesContext)!; export const load = (file: File): Promise | undefined> => { switch (file.type) { case 'application/json': return json.load(file.stream()) default: return Promise.resolve(undefined); } }; export { Grid } from './grid'; export type { Entry } from './grid';