import { onDestroy } from "../../lib/skeleton/index.js"; import rxjs from "../../lib/rx.js"; import fscache from "./cache.js"; import { hooks } from "./model_files.js"; import { extractPath, isDir, currentPath } from "./helper.js"; /* * The virtual files is used to rerender the list of files in a particular location. That's used * when we want to update the dom when doing either of a touch, mkdir, rm, mv, ... * * |---------------| |---------------| * | LS | ---> | Virtual Layer | ---> Observable * |---------------| |---------------| * * It is split onto 2 parts: * - the virtualFiles$: which are things we want to display in addition to what is currently * visible on the screen * - the mutationFiles$: which are things already on the screen which we need to mutate. For * example when we want a particular file to show a loading spinner, ... */ const virtualFiles$ = new rxjs.BehaviorSubject({ // "/tmp/": [], // "/home/": [{ name: "test", type: "directory" }] }); const mutationFiles$ = new rxjs.BehaviorSubject({ // "/home/": [{ name: "test", fn: (file) => file, ...] }); class IVirtualLayer { before() { throw new Error("NOT_IMPLEMENTED"); } async afterSuccess() { throw new Error("NOT_IMPLEMENTED"); } async afterError() { return rxjs.EMPTY; } } export function withVirtualLayer(ajax$, mutate) { mutate.before(); return ajax$.pipe( rxjs.tap((resp) => mutate.afterSuccess(resp)), rxjs.catchError(mutate.afterError), ); }; export function touch(path) { const [basepath, filename] = extractPath(path); const file = { name: filename, type: "file", size: 0, time: new Date().getTime(), }; return new class TouchVL extends IVirtualLayer { /** * @override */ before() { stateAdd(virtualFiles$, basepath, { ...file, loading: true, }); } /** * @override */ async afterSuccess() { removeLoading(virtualFiles$, basepath, filename); onDestroy(() => statePop(virtualFiles$, basepath, filename)); await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); hooks.mutation.emit({ op: "touch", path: basepath }); } /** * @override */ async afterError() { statePop(virtualFiles$, basepath, filename); return rxjs.of(fscache().remove(basepath)).pipe( rxjs.mergeMap(() => rxjs.EMPTY), ); } }(); } export function mkdir(path) { const [basepath, dirname] = extractPath(path); const file = { name: dirname, type: "directory", size: 0, time: new Date().getTime(), }; return new class MkdirVL extends IVirtualLayer { /** * @override */ before() { stateAdd(virtualFiles$, basepath, { ...file, loading: true, }); statePop(mutationFiles$, basepath, dirname); // case: rm followed by mkdir } /** * @override */ async afterSuccess() { if (basepath === currentPath()) removeLoading(virtualFiles$, basepath, dirname); else onDestroy(() => removeLoading(virtualFiles$, basepath, dirname)); onDestroy(() => statePop(virtualFiles$, basepath, dirname)); await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); hooks.mutation.emit({ op: "mkdir", path: basepath }); } /** * @override */ async afterError() { statePop(virtualFiles$, basepath, dirname); return rxjs.of(fscache().remove(basepath)).pipe( rxjs.mergeMap(() => rxjs.EMPTY), ); } }(); } export function save(path, size) { const [basepath, filename] = extractPath(path); const file = { name: filename, type: "file", size, time: new Date().getTime(), }; return new class SaveVL extends IVirtualLayer { /** * @override */ before() { stateAdd(virtualFiles$, basepath, { ...file, loading: true, }); statePop(mutationFiles$, basepath, filename); // eg: rm followed by save } /** * @override */ async afterSuccess() { if (basepath === currentPath()) removeLoading(virtualFiles$, basepath, filename); else onDestroy(() => removeLoading(virtualFiles$, basepath, filename)); onDestroy(() => statePop(virtualFiles$, basepath, filename)); await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); hooks.mutation.emit({ op: "save", path: basepath }); } /** * @override */ async afterError() { statePop(virtualFiles$, basepath, filename); return rxjs.EMPTY; } }(); } export function rm(...paths) { if (paths.length === 0) return rxjs.of(null); const arr = new Array(paths.length * 2); let basepath = null; for (let i=0; i { if (file.name === arr[i+1]) { file.loading = true; file.last = true; } return file; }, }); statePop(virtualFiles$, arr[i], arr[i+1]); // eg: touch followed by rm } } /** * @override */ async afterSuccess() { for (let i=0; i { for (let i=0; i { for (let i=0; i fscache().remove(path, false))); await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.filter(({ name }) => { for (let i=0; i 0) hooks.mutation.emit({ op: "rm", path: arr[0] }); } /** * @override */ async afterError() { for (let i=0; i { if (file.name === arr[i+1]) { file = { ...file, loading: false, last: false }; } return file; }, }); } return rxjs.EMPTY; } }(); } export function mv(fromPath, toPath) { const [fromBasepath, fromName] = extractPath(fromPath); const [toBasepath, toName] = extractPath(toPath); let type = null; return new class MvVL extends IVirtualLayer { /** * @override */ before() { if (fromBasepath === toBasepath) this._beforeSamePath(); else this._beforeDifferentPath(); } _beforeSamePath() { stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === fromName) { type = file.type; return { ...file, name: toName, loading: true, }; } return file; }, }); } _beforeDifferentPath() { stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === fromName) { type = file.type; return { ...file, loading: true, last: true, }; } return file; }, }); stateAdd(virtualFiles$, toBasepath, { name: toName, loading: true, type, }); } /** * @override */ async afterSuccess() { fscache().remove(fromPath, false); if (fromBasepath === toBasepath) await this._afterSuccessSamePath(); else await this._afterSuccessDifferentPath(); } async _afterSuccessSamePath() { stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === toName) { file = { ...file, loading: false }; } return file; }, }); await fscache().update(fromBasepath, ({ files = [], ...rest }) => { return { files: files.map((file) => { if (file.name === fromName) { file.name = toName; } return file; }), ...rest, }; }); hooks.mutation.emit({ op: "mv", path: fromBasepath }); } async _afterSuccessDifferentPath() { stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === fromName) return null; return file; }, }); onDestroy(() => statePop(mutationFiles$, fromBasepath, fromName)); statePop(virtualFiles$, toBasepath, toName); await fscache().update(fromBasepath, ({ files = [], ...rest }) => ({ files: files.filter((file) => file.name !== fromName), ...rest, })); await fscache().update(toBasepath, ({ files = [], ...rest }) => ({ files: files.concat([{ name: fromName, time: new Date().getTime(), type, }]), ...rest, })); if (isDir(fromPath)) await fscache().remove(fromPath); hooks.mutation.emit({ op: "mv", path: fromBasepath }); hooks.mutation.emit({ op: "mv", path: toBasepath }); } /** * @override */ async afterError() { if (fromBasepath === toBasepath) stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === toName) return { ...file, name: fromName, loading: false, }; return file; }, }); else { stateAdd(mutationFiles$, fromBasepath, { name: fromName, fn: (file) => { if (file.name === fromName) return { ...file, loading: false, last: false, }; return file; }, }); statePop(virtualFiles$, toBasepath, toName); } statePop(mutationFiles$, fromBasepath, fromName); return rxjs.EMPTY; } }(); } export function ls(path) { return rxjs.pipe( // case1: file mutation = update a file state, typically to add a loading state to an // file or remove it entirely rxjs.switchMap(({ files, ...res }) => mutationFiles$.pipe( rxjs.map((all) => all[path]), rxjs.mergeMap((fns) => { const shouldContinue = !!(fns && fns.length > 0); if (!shouldContinue) return rxjs.of({ ...res, files }); for (let i=files.length-1; i>=0; i--) { for (let j=0; j virtualFiles$.pipe( rxjs.map((all) => all[path] || []), rxjs.distinctUntilChanged((prev, curr) => { // we only want to get notified of changes within "path" if (prev.length !== curr.length) return false; for (let i=0; i { if (virtualFiles.length === 0) return rxjs.of({ ...res, files }); return rxjs.of({ ...res, files: files.concat(virtualFiles), }); }), )), ); } function stateAdd(behavior, path, obj) { let arr = behavior.value[path]; if (!arr) arr = []; let alreadyKnown = false; for (let i=0; i name !== filename); if (newArr.length === 0) { const newState = { ...behavior.value }; delete newState[path]; behavior.next(newState); return; } behavior.next({ ...behavior.value, [path]: newArr, }); } function removeLoading(behavior, path, filename) { const arr = behavior.value[path]; if (!arr) return; virtualFiles$.next({ ...virtualFiles$.value, [path]: arr.map((file) => { if (file.name === filename) { return { ...file, loading: false }; } return file; }), }); }