add file system observer #38
4 changed files with 208 additions and 6 deletions
|
@ -1,6 +1,11 @@
|
||||||
import { Accessor, createEffect, from, createSignal } from "solid-js";
|
import { Accessor, createEffect, from, createSignal } from "solid-js";
|
||||||
import { json } from "./parser";
|
import { json } from "./parser";
|
||||||
import { filter } from "~/utilities";
|
import { filter } from "~/utilities";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
import { installIntoGlobal } from 'iterator-helpers-polyfill';
|
||||||
|
import { debounce } from "@solid-primitives/scheduled";
|
||||||
|
|
||||||
|
installIntoGlobal();
|
||||||
|
|
||||||
interface Files extends Record<string, { handle: FileSystemFileHandle, file: File }> { }
|
interface Files extends Record<string, { handle: FileSystemFileHandle, file: File }> { }
|
||||||
interface Contents extends Map<string, Map<string, string>> { }
|
interface Contents extends Map<string, Map<string, string>> { }
|
||||||
|
@ -13,8 +18,16 @@ export const read = (file: File): Promise<Map<string, string> | undefined> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Files> => {
|
export const readFiles = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Files> => {
|
||||||
return createPolled<FileSystemDirectoryHandle, Files>(directory, async (directory, prev) => {
|
return (!isServer && 'FileSystemObserver' in window) ? readFiles__observer(directory) : readFiles__polled(directory)
|
||||||
|
};
|
||||||
|
|
||||||
|
const readFiles__polled = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Files> => {
|
||||||
|
return createPolled<FileSystemDirectoryHandle | undefined, Files>(directory, async (directory, prev) => {
|
||||||
|
if (!directory) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
const next: Files = Object.fromEntries(await Array.fromAsync(
|
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 }]
|
async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }]
|
||||||
|
@ -39,9 +52,130 @@ export const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Acces
|
||||||
}, { interval: 1000, initialValue: {} });
|
}, { interval: 1000, initialValue: {} });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readFiles__observer = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Files> => {
|
||||||
|
const [files, setFiles] = createSignal<Files>({});
|
||||||
|
|
||||||
|
const observer = new FileSystemObserver(debounce(async records => {
|
||||||
|
for (const record of records) {
|
||||||
|
switch (record.type) {
|
||||||
|
case 'modified': {
|
||||||
|
if (record.changedHandle.kind === 'file') {
|
||||||
|
const handle = record.changedHandle as FileSystemFileHandle;
|
||||||
|
const id = await handle.getUniqueId();
|
||||||
|
const file = await handle.getFile();
|
||||||
|
|
||||||
|
setFiles(prev => ({ ...prev, [id]: { file, handle } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.log(record);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10));
|
||||||
|
|
||||||
|
createEffect<FileSystemDirectoryHandle | undefined>((last = undefined) => {
|
||||||
|
if (last) {
|
||||||
|
observer.unobserve(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = directory();
|
||||||
|
|
||||||
|
if (!dir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe(dir);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setFiles(Object.fromEntries(
|
||||||
|
await dir.values()
|
||||||
|
.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json'))
|
||||||
|
.map(async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }] as const)
|
||||||
|
.toArray()
|
||||||
|
));
|
||||||
|
})();
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HANDLE = Symbol('handle');
|
||||||
const LAST_MODIFIED = Symbol('lastModified');
|
const LAST_MODIFIED = Symbol('lastModified');
|
||||||
export const contentsOf = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Contents> => {
|
export const contentsOf = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Contents> => {
|
||||||
return createPolled<FileSystemDirectoryHandle, Contents>(directory, async (directory, prev) => {
|
return (!isServer && 'FileSystemObserver' in window) ? contentsOf__observer(directory) : contentsOf__polled(directory)
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentsOf__observer = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Contents> => {
|
||||||
|
const [contents, setContents] = createSignal<Contents>(new Map);
|
||||||
|
|
||||||
|
const observer = new FileSystemObserver(debounce(async records => {
|
||||||
|
for (const record of records) {
|
||||||
|
switch (record.type) {
|
||||||
|
case 'modified': {
|
||||||
|
if (record.changedHandle.kind === 'file') {
|
||||||
|
const handle = record.changedHandle as FileSystemFileHandle;
|
||||||
|
const id = await handle.getUniqueId();
|
||||||
|
const file = await handle.getFile();
|
||||||
|
const entries = (await read(file))!;
|
||||||
|
entries[LAST_MODIFIED] = file.lastModified;
|
||||||
|
|
||||||
|
setContents(prev => new Map([...prev, [id, entries]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.log(record);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10));
|
||||||
|
|
||||||
|
createEffect<FileSystemDirectoryHandle | undefined>((last = undefined) => {
|
||||||
|
if (last) {
|
||||||
|
observer.unobserve(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = directory();
|
||||||
|
|
||||||
|
if (!dir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe(dir);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setContents(new Map(await walk(dir).map(async ({ id, file }) => {
|
||||||
|
const entries = (await read(file))!;
|
||||||
|
entries[LAST_MODIFIED] = file.lastModified;
|
||||||
|
|
||||||
|
return [id, entries] as const;
|
||||||
|
}).toArray()));
|
||||||
|
})();
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
});
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentsOf__polled = (directory: Accessor<FileSystemDirectoryHandle | undefined>): Accessor<Contents> => {
|
||||||
|
return createPolled<FileSystemDirectoryHandle | undefined, Contents>(directory, async (directory, prev) => {
|
||||||
|
if (!directory) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
const files = await Array.fromAsync(walk(directory));
|
const files = await Array.fromAsync(walk(directory));
|
||||||
|
|
||||||
const next = async () => new Map(await Promise.all(files.map(async ({ id, file }) => {
|
const next = async () => new Map(await Promise.all(files.map(async ({ id, file }) => {
|
||||||
|
@ -116,6 +250,41 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []):
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Map<K, V> {
|
interface Map<K, V> {
|
||||||
|
[HANDLE]: FileSystemFileHandle;
|
||||||
[LAST_MODIFIED]: number;
|
[LAST_MODIFIED]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileSystemObserverCallback = (
|
||||||
|
records: FileSystemChangeRecord[],
|
||||||
|
observer: FileSystemObserver
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
interface FileSystemObserverObserveOptions {
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileSystemChangeType = 'appeared' | 'disappeared' | 'modified' | 'moved' | 'unknown' | 'errored';
|
||||||
|
|
||||||
|
interface FileSystemChangeRecord {
|
||||||
|
readonly changedHandle: FileSystemHandle;
|
||||||
|
readonly relativePathComponents: ReadonlyArray<string>;
|
||||||
|
readonly type: FileSystemChangeType;
|
||||||
|
readonly relativePathMovedFrom?: ReadonlyArray<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileSystemObserver {
|
||||||
|
observe(
|
||||||
|
handle: FileSystemHandle,
|
||||||
|
options?: FileSystemObserverObserveOptions
|
||||||
|
): Promise<void>;
|
||||||
|
unobserve(handle: FileSystemHandle): void;
|
||||||
|
disconnect(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileSystemObserverConstructor {
|
||||||
|
new(callback: FileSystemObserverCallback): FileSystemObserver;
|
||||||
|
readonly prototype: FileSystemObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
var FileSystemObserver: FileSystemObserverConstructor;
|
||||||
}
|
}
|
|
@ -13,9 +13,8 @@ import { useI18n } from "~/features/i18n";
|
||||||
import { makePersisted } from "@solid-primitives/storage";
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
import { writeClipboard } from "@solid-primitives/clipboard";
|
import { writeClipboard } from "@solid-primitives/clipboard";
|
||||||
import { destructure } from "@solid-primitives/destructure";
|
import { destructure } from "@solid-primitives/destructure";
|
||||||
import css from "./edit.module.css";
|
|
||||||
import { contentsOf } from "~/features/file/helpers";
|
import { contentsOf } from "~/features/file/helpers";
|
||||||
import { createHtmlParser, createMarkdownParser, createSource } from "~/features/source";
|
import css from "./edit.module.css";
|
||||||
|
|
||||||
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
|
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
|
||||||
|
@ -346,6 +345,7 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
|
||||||
const [api, setApi] = createSignal<GridApi>();
|
const [api, setApi] = createSignal<GridApi>();
|
||||||
|
|
||||||
const files = readFiles(() => props.directory);
|
const files = readFiles(() => props.directory);
|
||||||
|
// const __contents = contentsOf(() => props.directory);
|
||||||
const [contents] = createResource(files, (files) => Promise.all(Object.entries(files).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await read(file))! }))), { initialValue: [] });
|
const [contents] = createResource(files, (files) => Promise.all(Object.entries(files).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await read(file))! }))), { initialValue: [] });
|
||||||
|
|
||||||
const [entries, rows] = destructure(() => {
|
const [entries, rows] = destructure(() => {
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default function Experimental(props: ParentProps) {
|
||||||
<Menu.Item command={goTo.withLabel('grid').with('grid')} />
|
<Menu.Item command={goTo.withLabel('grid').with('grid')} />
|
||||||
<Menu.Item command={goTo.withLabel('context-menu').with('context-menu')} />
|
<Menu.Item command={goTo.withLabel('context-menu').with('context-menu')} />
|
||||||
<Menu.Item command={goTo.withLabel('formatter').with('formatter')} />
|
<Menu.Item command={goTo.withLabel('formatter').with('formatter')} />
|
||||||
|
<Menu.Item command={goTo.withLabel('file-system-observer').with('file-system-observer')} />
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
<ErrorBoundary fallback={e => <ErrorComp error={e} />}>
|
<ErrorBoundary fallback={e => <ErrorComp error={e} />}>
|
||||||
|
|
32
src/routes/(editor)/experimental/file-system-observer.tsx
Normal file
32
src/routes/(editor)/experimental/file-system-observer.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { createEffect, createSignal, on } from "solid-js";
|
||||||
|
import { readFiles } from "~/features/file";
|
||||||
|
import { contentsOf } from "~/features/file/helpers";
|
||||||
|
|
||||||
|
export default function FileObserver(props: {}) {
|
||||||
|
const [dir, setDir] = createSignal<FileSystemDirectoryHandle>();
|
||||||
|
|
||||||
|
const files = readFiles(dir);
|
||||||
|
const contents = contentsOf(dir);
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
|
const handle = await window.showDirectoryPicker();
|
||||||
|
|
||||||
|
setDir(handle)
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('dir', dir());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('files', files());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('contents', contents());
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<button onclick={open}>Select folder</button>
|
||||||
|
</div>;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue