From 881608f7deba725deef59be94012ca42ad141d3c Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Tue, 7 Jan 2025 16:09:06 +0100 Subject: [PATCH] refactor file feature --- src/features/file/context.tsx | 174 +++++++++++++++++++++++++++++ src/features/file/index.tsx | 175 +----------------------------- src/features/file/parser/index.ts | 1 + src/routes/(editor)/index.tsx | 6 +- 4 files changed, 179 insertions(+), 177 deletions(-) create mode 100644 src/features/file/context.tsx create mode 100644 src/features/file/parser/index.ts diff --git a/src/features/file/context.tsx b/src/features/file/context.tsx new file mode 100644 index 0000000..f70d979 --- /dev/null +++ b/src/features/file/context.tsx @@ -0,0 +1,174 @@ +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 { json } from "./parser"; + +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): Promise; + close(): Promise; + 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); + }, + + async close() { + setState('root', undefined); + + await internal.remove(ROOT); + }, + + 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); + } +}; \ No newline at end of file diff --git a/src/features/file/index.tsx b/src/features/file/index.tsx index b6d7117..c3159f1 100644 --- a/src/features/file/index.tsx +++ b/src/features/file/index.tsx @@ -1,177 +1,4 @@ -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): Promise; - close(): Promise; - 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); - }, - - async close() { - setState('root', undefined); - - await internal.remove(ROOT); - }, - - 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 { useFiles, FilesProvider, load } from './context'; export { Grid } from './grid'; export type { Entry } from './grid'; \ No newline at end of file diff --git a/src/features/file/parser/index.ts b/src/features/file/parser/index.ts new file mode 100644 index 0000000..219bb36 --- /dev/null +++ b/src/features/file/parser/index.ts @@ -0,0 +1 @@ +export * as json from './json'; \ No newline at end of file diff --git a/src/routes/(editor)/index.tsx b/src/routes/(editor)/index.tsx index b245105..fc2a866 100644 --- a/src/routes/(editor)/index.tsx +++ b/src/routes/(editor)/index.tsx @@ -1,16 +1,16 @@ import { useNavigate } from "@solidjs/router"; import { createEffect } from "solid-js"; -import { useFiles } from "~/features/file"; +import { isServer } from "solid-js/web"; +import { useFiles } from "~/features/file/context"; export default function Index() { const navigate = useNavigate(); const files = useFiles(); createEffect(() => { - const loading = files.loading(); const root = files.root(); - if (loading) { + if (isServer) { return; }