From 7ffdb2f51bf3124b2d64a5a02fbdd00e14594507 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Fri, 7 Feb 2025 11:04:17 +1100 Subject: [PATCH] made more parts reactive, fixing a bug with newly created files --- src/features/file/helpers.ts | 102 ++++++++++++++++++++++++++++++----- src/routes/(editor)/edit.tsx | 50 +++-------------- 2 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/features/file/helpers.ts b/src/features/file/helpers.ts index 5aeca4d..ad3a31c 100644 --- a/src/features/file/helpers.ts +++ b/src/features/file/helpers.ts @@ -1,8 +1,9 @@ -import { Accessor, createResource, InitializedResource, onCleanup } from "solid-js"; +import { Accessor, createEffect, createResource, createSignal, InitializedResource, onCleanup, Resource } from "solid-js"; import { json } from "./parser"; import { filter } from "~/utilities"; interface Files extends Record { } +interface Contents extends Map> { } export const read = (file: File): Promise | undefined> => { switch (file.type) { @@ -12,12 +13,10 @@ export const read = (file: File): Promise | undefined> => { } }; -export const readFiles = (directory: Accessor): InitializedResource => { - const [value, { refetch }] = createResource(async (_, { value: prev }) => { - prev ??= {}; - +export const readFiles = (directory: Accessor): Accessor => { + return createPolled(directory, async (directory, prev) => { const next: Files = Object.fromEntries(await Array.fromAsync( - filter(directory().values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')), + filter(directory.values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')), async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }] )); @@ -37,15 +36,92 @@ export const readFiles = (directory: Accessor): Initi } return prev; - }, { initialValue: {} }) + }, { interval: 1000, initialValue: {} }); +}; - const interval = setInterval(() => { - refetch(); - }, 1000); +const LAST_MODIFIED = Symbol('lastModified'); +export const contentsOf = (directory: Accessor): Accessor => { + return createPolled(directory, async (directory, prev) => { + const files = await Array.fromAsync(walk(directory)); - onCleanup(() => { - clearInterval(interval); + const next = async () => new Map(await Promise.all(files.map(async ({ id, file }) => { + const entries = (await read(file))!; + entries[LAST_MODIFIED] = file.lastModified; + + return [id, entries] as const; + }))); + + if (files.length !== prev.size) { + return next(); + } + + if (files.every(({ id }) => prev.has(id)) === false) { + return next(); + } + + if (files.every(({ id, file }) => prev.get(id)![LAST_MODIFIED] === file.lastModified) === false) { + return next(); + } + + return prev; + }, { interval: 1000, initialValue: new Map() }); +}; + +function createPolled(source: Accessor, callback: (source: S, prev: T) => T | Promise, options: { interval: number, initialValue: T }): Accessor { + const { interval, initialValue } = options; + const [value, setValue] = createSignal(initialValue); + const tick = createTicker(interval); + + createEffect(() => { + tick(); + const s = source(); + + (async () => { + const prev = value(); + const next: T = await callback(s, prev); + + setValue(() => next); + })(); }); return value; -}; \ No newline at end of file +}; + +function createTicker(interval: number): Accessor { + const [tick, update] = createSignal(true); + + const intervalId = setInterval(() => { + update(v => !v); + }, interval); + + onCleanup(() => { + clearInterval(intervalId); + }); + + return tick; +} + +async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], file: File }, void, never> { + for await (const handle of directory.values()) { + if (handle.kind === 'directory') { + yield* walk(handle, [...path, handle.name]); + + continue; + } + + if (!handle.name.endsWith('.json')) { + continue; + } + + const id = await handle.getUniqueId(); + const file = await handle.getFile(); + + yield { id, handle, path, file }; + } +}; + +declare global { + interface Map { + [LAST_MODIFIED]: number; + } +} \ No newline at end of file diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index d9b7063..8076c91 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -1,5 +1,5 @@ import { Component, createEffect, createMemo, createResource, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js"; -import { Created, filter, MutarionKind, Mutation, splitAt } from "~/utilities"; +import { Created, MutarionKind, Mutation, splitAt } from "~/utilities"; import { Sidebar } from "~/components/sidebar"; import { Menu } from "~/features/menu"; import { Grid, read, readFiles, TreeProvider, Tree, useFiles } from "~/features/file"; @@ -14,32 +14,10 @@ import { makePersisted } from "@solid-primitives/storage"; import { writeClipboard } from "@solid-primitives/clipboard"; import { destructure } from "@solid-primitives/destructure"; import css from "./edit.module.css"; +import { contentsOf } from "~/features/file/helpers"; const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches; -async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], lang: string, entries: Map }, void, never> { - for await (const handle of directory.values()) { - if (handle.kind === 'directory') { - yield* walk(handle, [...path, handle.name]); - - continue; - } - - if (!handle.name.endsWith('.json')) { - continue; - } - - const id = await handle.getUniqueId(); - const file = await handle.getFile(); - const lang = file.name.split('.').at(0)!; - const entries = await load(file); - - if (entries !== undefined) { - yield { id, handle, path, lang, entries }; - } - } -}; - interface Entries extends Map> { }; export default function Edit(props: ParentProps) { @@ -74,25 +52,17 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { const { t } = useI18n(); const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => { - const [api, setApi] = createSignal<(GridApi & { addLocale(locale: string): void })>(); + const [api, setApi] = createSignal(); const [entries, setEntries] = createSignal(new Map()); - const [files, setFiles] = createSignal>(new Map()); - - (async () => { - const files = await Array.fromAsync( - filter(handle.values(), entry => entry.kind === 'file'), - async file => [file.name.split('.').at(0)!, { handle: file, id: await file.getUniqueId() }] as const - ); - - setFiles(new Map(files)); - })(); + const __files = readFiles(() => handle); + const files = createMemo(() => new Map(Object.entries(__files()).map(([id, { file, handle }]) => [file.name.split('.').at(0)!, { handle, id }]))); return ({ key, handle, api, setApi, entries, setEntries, files }); })); const [active, setActive] = makePersisted(createSignal(), { name: 'edit__aciveTab' }); - const [contents, setContents] = createSignal>>(new Map()); const [newKeyPrompt, setNewKeyPrompt] = createSignal(); const [newLanguagePrompt, setNewLanguagePrompt] = createSignal(); + const contents = contentsOf(() => props.root); const tab = createMemo(() => { const name = active(); @@ -225,14 +195,6 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return existingFiles.concat(newFiles); }); - createEffect(() => { - const directory = props.root; - - (async () => { - setContents(new Map(await Array.fromAsync(walk(directory), ({ id, entries }) => [id, entries] as const))) - })(); - }); - const commands = { open: createCommand('page.edit.command.open', async () => { const directory = await window.showDirectoryPicker({ mode: 'readwrite' });