From ef27ee2ec66443a87e12f3531517c28c6b218d93 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 20 Feb 2026 22:09:08 +0100 Subject: [PATCH 1/9] wip socket rooms --- server/websocket.ts | 51 ++++++++++++++++++++++++++++++++-- src/lib/core/CoreAPI.svelte.ts | 16 +++++++---- src/lib/websocket.ts | 39 +++++++++++++++++++------- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/server/websocket.ts b/server/websocket.ts index 3a877bd..2c4dbc7 100644 --- a/server/websocket.ts +++ b/server/websocket.ts @@ -1,4 +1,5 @@ import {Server} from 'socket.io'; + import type { TabKind } from '$types/tabs'; import type {HttpServer} from 'vite'; @@ -10,6 +11,8 @@ const fileUserMap = new Map< Set // users connected >(); +const tapeKey = 'tape:'; + export function initServerWebsocket(server: HttpServer | null) { if (!server) { @@ -19,13 +22,55 @@ export function initServerWebsocket(server: HttpServer | null) { const io = new Server(server); - io.on('connection', (socket) => { - console.log('a user connected', socket.id); + io.on('connection', async (socket) => { + console.log( + 'a user connected', + socket.id, + socket.handshake.headers['x-tape-name'], + socket.handshake.headers.cookie + ); + + + // todo: imp + // I would like something like that, but ws is outside of vite and svelte, without env access + // await ensureTapeExists(tape); + + // same here, alias + env + // if (!socket.handshake.headers.cookie) { + // console.warn('Missing session cookie'); + // socket.disconnect(true); + // return; + // } + // const cookieObj = JSON.parse(socket.handshake.headers.cookie); + // const auth = await validateSessionToken( + // cookieObj['auth-session'] + // ); + // if (!auth || !auth.session) { + // console.warn('Invalid session token'); + // socket.disconnect(true); + // return; + // } + + const tape = socket.handshake.headers['x-tape-name']; + if (!tape || typeof tape !== 'string') { + console.warn('Invalid or missing tape name'); + socket.disconnect(true); + return; + } + + await socket.join( + tapeKey + tape + ); socket.on('tab-opened', (tab: {id: string, kind: TabKind}, callback) => { if (!socket.id || tab.kind !== 'file') return; - console.info('[Info] Tab opened by', socket.id, tab); + console.info('[Info] Tab opened by', + socket.id, + tab, + 'on', + [...socket.rooms].filter(r => r.startsWith('tape:')).map(r => r.slice(tapeKey.length)).join() + ); if (!fileUserMap.has(tab.id)) { fileUserMap.set(tab.id, new Set()); diff --git a/src/lib/core/CoreAPI.svelte.ts b/src/lib/core/CoreAPI.svelte.ts index d0cfb67..cef8c67 100644 --- a/src/lib/core/CoreAPI.svelte.ts +++ b/src/lib/core/CoreAPI.svelte.ts @@ -6,7 +6,7 @@ import { getCurrentTape } from '$lib/remotes/files.remote'; import { ViewMap } from '$components/Main/View'; import { Page } from './Page.svelte'; import { InfoUi } from './InfosUi.svelte'; -import { ClientSocket, getSocket } from '$lib/websocket'; +import { type ClientSocket, getSocket } from '$lib/websocket'; import type { FileEntry } from '$types/files'; import type { EntryModification } from '$types/modification'; import type { TabFileEntry } from '$types/tabs'; @@ -22,7 +22,7 @@ class CoreAPI { readonly infoUi: InfoUi; - readonly clientSocket: ClientSocket | null; + #clientSocket: ClientSocket | null = null; constructor() { // Internal @@ -34,14 +34,20 @@ class CoreAPI { this.pageStore = new Page(this); this.infoUi = new InfoUi(this); - - this.clientSocket = getSocket(this); } - + async init() { const tapeName = await getCurrentTape(); this.foldState.init(tapeName); this.pageStore.init(); + this.#clientSocket = getSocket(this); + } + + get clientSocket() { + if (!this.#clientSocket) { + this.#clientSocket = getSocket(this); + } + return this.#clientSocket; } /** diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index 011b35a..743a0c3 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -2,29 +2,46 @@ import ioClient, {Socket} from 'socket.io-client'; import { createSubscriber } from 'svelte/reactivity'; import { browser } from '$app/environment'; +import { page } from '$app/state'; + import type { CoreAPI } from '$core/CoreAPI.svelte'; import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; -let socket: ClientSocket | null = null; +let clientSocket: ClientSocket | null = null; export function getSocket(core: CoreAPI) { if (!browser) return null; - if (!socket) { + if (!clientSocket) { const { protocol, host } = window.location; const url = `${protocol}//${host}`; - const iosocket = ioClient(url); - socket = new ClientSocket(iosocket, core); - } else if (!socket.socket.connected) { - socket.socket.connect(); + + const tape = page.params.tape; + if (!tape) { + console.warn('Could not find tape name'); + return null; + }; + + const socket = ioClient(url, {extraHeaders: { + 'x-tape-name': tape + }}); + clientSocket = new ClientSocket( + core, + socket, + ); + } else if (!clientSocket.socket.connected) { + clientSocket.socket.connect(); } - return socket; + return clientSocket; } -export class ClientSocket { +class ClientSocket { #socket; #subscribe; - constructor(socket: Socket, private core: CoreAPI) { + constructor( + private core: CoreAPI, + socket: Socket, + ) { this.#socket = socket; this.#subscribe = createSubscriber((update) => { @@ -44,4 +61,6 @@ export class ClientSocket { return this.#socket; } -} \ No newline at end of file +} + +export type {ClientSocket}; \ No newline at end of file From bcf67ba2f0bbd911ac870f125288a3d573e28f9f Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 18:21:02 +0100 Subject: [PATCH 2/9] moved entries --- src/lib/remotes/entries.remote.ts | 89 +++++++++++++++++++++++++++++++ src/lib/remotes/files.remote.ts | 82 +--------------------------- 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 src/lib/remotes/entries.remote.ts diff --git a/src/lib/remotes/entries.remote.ts b/src/lib/remotes/entries.remote.ts new file mode 100644 index 0000000..2d0f026 --- /dev/null +++ b/src/lib/remotes/entries.remote.ts @@ -0,0 +1,89 @@ +import z from 'zod'; +import path from 'node:path'; +import { rm, lstat } from 'node:fs/promises'; +import { move } from 'fs-extra/esm'; +import { command } from '$app/server'; + +import { getRelativePathFromTape, getValidPathInTape, sanitizeFileName } from './files.utils'; +import { getFileTree } from './files.remote'; +import type { EntryModification } from '$types/modification'; + + + +export const moveEntry = command(z.object({ + entryPath: z.string(), + destFolder: z.string() +}), async ({ entryPath, destFolder }): Promise => { + const saneEntryPath = getValidPathInTape(entryPath); + const saneDestFolder = getValidPathInTape(destFolder); + + const entryName = path.basename(saneEntryPath); + const newEntryPath = path.resolve(saneDestFolder, entryName); + + if (saneEntryPath === newEntryPath) { + return []; + } + + await move(saneEntryPath, newEntryPath); + + const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); + const modifications = [{ + type: 'moved', + oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(saneDestFolder), entryName, isFolder ? '/' : ''), + isFolder + }] satisfies EntryModification[]; + console.log(modifications); + + await getFileTree().refresh(); + return modifications; +}); + +export const renameEntry = command(z.object({ + entryPath: z.string(), + newName: z.string() +}), async ({ entryPath, newName }): Promise => { + const saneEntryPath = getValidPathInTape(entryPath); + const sanitizedName = sanitizeFileName(newName); + const targetFolder = path.dirname(saneEntryPath); + const newPath = path.resolve(targetFolder, sanitizedName); + + if (saneEntryPath === newPath) { + return []; + } + + await move(saneEntryPath, newPath); + + const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); + const modifications = [{ + type: 'renamed', + oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(newPath), isFolder ? '/' : ''), + isFolder + }] satisfies EntryModification[]; + console.log(modifications); + + await getFileTree().refresh(); + return modifications; +}); + +export const removeEntry = command(z.object({ + entryPath: z.string() +}), async ({ entryPath }): Promise => { + const saneEntryPath = getValidPathInTape(entryPath); + const stats = await lstat(saneEntryPath); + const isFolder = stats.isDirectory(); + + await rm(saneEntryPath, { recursive: true, force: true }); + + const modifications = [{ + type: 'removed', + oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), + newPath: '', + isFolder + }] satisfies EntryModification[]; + console.log(modifications); + + await getFileTree().refresh(); + return modifications; +}); \ No newline at end of file diff --git a/src/lib/remotes/files.remote.ts b/src/lib/remotes/files.remote.ts index 2d1c4ad..847a4f1 100644 --- a/src/lib/remotes/files.remote.ts +++ b/src/lib/remotes/files.remote.ts @@ -1,15 +1,13 @@ import z from 'zod'; import path, { dirname, join } from 'node:path'; import { existsSync } from 'node:fs'; -import { writeFile, mkdir, readFile, rm, lstat } from 'node:fs/promises'; -import { move } from 'fs-extra/esm'; +import { writeFile, mkdir, readFile, lstat } from 'node:fs/promises'; import { error } from '@sveltejs/kit'; import { command, form, getRequestEvent, query } from '$app/server'; import { createFileTree } from '$lib'; import { env } from '$env/dynamic/private'; import { getRelativePathFromTape, getValidPathInTape, sanitizeFileName } from './files.utils'; import type { FileEntry, FsNode } from '$types/files'; -import type { EntryModification } from '$types/modification'; const NOTE_DIR = env.NOTE_DIR; @@ -225,81 +223,3 @@ TS: ${newTimestamp}`); return newTimestamp; }); - -export const moveEntry = command(z.object({ - entryPath: z.string(), - destFolder: z.string() -}), async ({ entryPath, destFolder }): Promise => { - const saneEntryPath = getValidPathInTape(entryPath); - const saneDestFolder = getValidPathInTape(destFolder); - - const entryName = path.basename(saneEntryPath); - const newEntryPath = path.resolve(saneDestFolder, entryName); - - if (saneEntryPath === newEntryPath) { - return []; - } - - await move(saneEntryPath, newEntryPath); - - const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'moved', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(saneDestFolder), entryName, isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - console.log(modifications); - - await getFileTree().refresh(); - return modifications; -}); - -export const renameEntry = command(z.object({ - entryPath: z.string(), - newName: z.string() -}), async ({ entryPath, newName }): Promise => { - const saneEntryPath = getValidPathInTape(entryPath); - const sanitizedName = sanitizeFileName(newName); - const targetFolder = path.dirname(saneEntryPath); - const newPath = path.resolve(targetFolder, sanitizedName); - - if (saneEntryPath === newPath) { - return []; - } - - await move(saneEntryPath, newPath); - - const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'renamed', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(newPath), isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - console.log(modifications); - - await getFileTree().refresh(); - return modifications; -}); - -export const removeEntry = command(z.object({ - entryPath: z.string() -}), async ({ entryPath }): Promise => { - const saneEntryPath = getValidPathInTape(entryPath); - const stats = await lstat(saneEntryPath); - const isFolder = stats.isDirectory(); - - await rm(saneEntryPath, { recursive: true, force: true }); - - const modifications = [{ - type: 'removed', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: '', - isFolder - }] satisfies EntryModification[]; - console.log(modifications); - - await getFileTree().refresh(); - return modifications; -}); \ No newline at end of file From f3d4b0a87c2f2dfb9bcd7f72787e9a1ec7d4c61f Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 20:16:08 +0100 Subject: [PATCH 3/9] entry modification via socket --- server/websocket.ts | 48 ++++++++++++++--------- src/app.d.ts | 5 +++ src/lib/core/CoreAPI.svelte.ts | 8 ++-- src/lib/core/InfosUi.svelte.ts | 16 ++++---- src/lib/core/internal/EntryAPI.svelte.ts | 11 ++---- src/lib/remotes/entries.remote.ts | 49 ++++++++++++++++-------- src/lib/websocket.ts | 5 +++ src/types/socket/index.ts | 7 +++- vite.config.ts | 16 ++++---- 9 files changed, 106 insertions(+), 59 deletions(-) diff --git a/server/websocket.ts b/server/websocket.ts index 2c4dbc7..ee241d4 100644 --- a/server/websocket.ts +++ b/server/websocket.ts @@ -2,27 +2,39 @@ import {Server} from 'socket.io'; import type { TabKind } from '$types/tabs'; import type {HttpServer} from 'vite'; - import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; -const fileUserMap = new Map< - string, // file path - Set // users connected ->(); - const tapeKey = 'tape:'; export function initServerWebsocket(server: HttpServer | null) { - if (!server) { console.error('could not start websocket services'); return; } - const io = new Server(server); + if (globalThis.myServerSocket) { + globalThis.myServerSocket.close(); + globalThis.myServerSocket = null; + } + + const fileUserMap = new Map< + string, // file path + Set // users connected + >(); + + const io = new Server(server); + globalThis.myServerSocket = io; + io.on('connection', async (socket) => { + socket.on('disconnect', () => { + for (const [_file, users] of fileUserMap) { + if (users.has(socket.id)) { + users.delete(socket.id); + } + } + }); console.log( 'a user connected', socket.id, @@ -58,9 +70,7 @@ export function initServerWebsocket(server: HttpServer | null) { return; } - await socket.join( - tapeKey + tape - ); + await socket.join(tapeKey + tape); socket.on('tab-opened', (tab: {id: string, kind: TabKind}, callback) => { if (!socket.id || tab.kind !== 'file') return; @@ -101,13 +111,13 @@ export function initServerWebsocket(server: HttpServer | null) { }); console.log(fileUserMap.get(tab.id)?.size, 'users on', tab.id); }); - - socket.on('disconnect', () => { - for (const [_file, users] of fileUserMap) { - if (users.has(socket.id)) { - users.delete(socket.id); - } - } - }); }); +} + +export function getServerSocket() { + const io = globalThis.myServerSocket; + if (!io) { + throw new Error('Websocket server not initialized'); + } + return io; } \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 0b8eb62..bc41185 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,11 @@ // See https://svelte.dev/docs/kit/types#app.d.ts + +import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; +import type { Server } from 'socket.io'; + // for information about these interfaces declare global { + var myServerSocket: Server< ClientToServerEvents, ServerClientEvents, object, object> | null; namespace App { // interface Error {} interface Locals { diff --git a/src/lib/core/CoreAPI.svelte.ts b/src/lib/core/CoreAPI.svelte.ts index cef8c67..9e225ae 100644 --- a/src/lib/core/CoreAPI.svelte.ts +++ b/src/lib/core/CoreAPI.svelte.ts @@ -5,7 +5,7 @@ import { FoldState } from '$core/internal/FoldState.svelte'; import { getCurrentTape } from '$lib/remotes/files.remote'; import { ViewMap } from '$components/Main/View'; import { Page } from './Page.svelte'; -import { InfoUi } from './InfosUi.svelte'; +import { InfoUi, type ModificationOrigin } from './InfosUi.svelte'; import { type ClientSocket, getSocket } from '$lib/websocket'; import type { FileEntry } from '$types/files'; import type { EntryModification } from '$types/modification'; @@ -161,13 +161,15 @@ class CoreAPI { * Method to sync changes between the server and the client * @fires {@linkcode CoreAPI.tabs} * @fires {@linkcode CoreAPI.activeTab} + * @fires {@linkcode CoreAPI.activeTabInfos} + * @fires {@linkcode InfoUi.addModificationMessage} */ - async syncStates(modifications: EntryModification[]) { + async syncStates(modifications: EntryModification[], origin: ModificationOrigin = 'local') { await this.#tabStore.syncModifications(modifications); await this.foldState.syncModifications(modifications); for (const mod of modifications) { - this.infoUi.addModificationMessage(mod); + this.infoUi.addModificationMessage(mod, origin); } } diff --git a/src/lib/core/InfosUi.svelte.ts b/src/lib/core/InfosUi.svelte.ts index 21c93a8..0042c9c 100644 --- a/src/lib/core/InfosUi.svelte.ts +++ b/src/lib/core/InfosUi.svelte.ts @@ -1,6 +1,8 @@ import type { EntryModification } from '$types/modification'; import type { CoreAPI } from './CoreAPI.svelte'; +export type ModificationOrigin = 'local' | 'socket'; + export class InfoUi { #maxMessages = 100; messageQueue: string[] = $state([]); @@ -8,23 +10,23 @@ export class InfoUi { constructor(private coreAPI: CoreAPI) {} - addModificationMessage(mod: EntryModification) { - let message = ''; + addModificationMessage(mod: EntryModification, kind: ModificationOrigin = 'local') { + let message = `[${kind}] `; switch (mod.type) { case 'created': - message = `Created entry "${mod.newPath}"`; + message += `Created entry "${mod.newPath}"`; break; case 'removed': - message = `Deleted entry "${mod.oldPath}"`; + message += `Deleted entry "${mod.oldPath}"`; break; case 'renamed': - message = `Renamed entry "${mod.oldPath}" to "${mod.newPath}"`; + message += `Renamed entry "${mod.oldPath}" to "${mod.newPath}"`; break; case 'moved': - message = `Moved entry "${mod.oldPath}" to "${mod.newPath}"`; + message += `Moved entry "${mod.oldPath}" to "${mod.newPath}"`; break; default: - message = `Unknown modification on entry "${mod.oldPath}"`; + message += `Unknown modification on entry "${mod.oldPath}"`; } this.addMessage(message); } diff --git a/src/lib/core/internal/EntryAPI.svelte.ts b/src/lib/core/internal/EntryAPI.svelte.ts index 3fe639d..e2a5ee0 100644 --- a/src/lib/core/internal/EntryAPI.svelte.ts +++ b/src/lib/core/internal/EntryAPI.svelte.ts @@ -1,4 +1,4 @@ -import { moveEntry, removeEntry, renameEntry } from '$lib/remotes/files.remote'; +import { moveEntry, removeEntry, renameEntry } from '$lib/remotes/entries.remote'; import type { CoreAPI } from '$core/CoreAPI.svelte'; import type { FolderEntry } from '$types/files'; @@ -12,8 +12,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ removeEntry = async (entryPath: string) => { - const modifications = await removeEntry({ entryPath: entryPath }); - await this.core.syncStates(modifications); + await removeEntry({ entryPath: entryPath }); }; /** * Rename a file or folder entry @@ -21,8 +20,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ renameEntry = async (entryPath: string, newName: string) => { - const modifications = await renameEntry({ entryPath: entryPath, newName: newName }); - await this.core.syncStates(modifications); + await renameEntry({ entryPath: entryPath, newName: newName }); }; /** * Move a file or folder entry to a destination folder @@ -30,7 +28,6 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ moveEntry = async (entryPath: string, folderEntry: FolderEntry) => { - const modifications = await moveEntry({ entryPath: entryPath, destFolder: folderEntry.path }); - await this.core.syncStates(modifications); + await moveEntry({ entryPath: entryPath, destFolder: folderEntry.path }); }; } \ No newline at end of file diff --git a/src/lib/remotes/entries.remote.ts b/src/lib/remotes/entries.remote.ts index 2d0f026..20d8d08 100644 --- a/src/lib/remotes/entries.remote.ts +++ b/src/lib/remotes/entries.remote.ts @@ -6,14 +6,26 @@ import { command } from '$app/server'; import { getRelativePathFromTape, getValidPathInTape, sanitizeFileName } from './files.utils'; import { getFileTree } from './files.remote'; +import { getServerSocket } from '../../../server/websocket'; + import type { EntryModification } from '$types/modification'; +async function syncModification(modifications: EntryModification[]) { + console.log(modifications); + const io = getServerSocket(); + io.emit('remoteModification', modifications); +} +/** + * Move an entry in a tape + * @fires socket 'remoteModification' event with modifications of type 'moved' + */ export const moveEntry = command(z.object({ entryPath: z.string(), destFolder: z.string() -}), async ({ entryPath, destFolder }): Promise => { +}), async ({ entryPath, destFolder }): Promise => { + // Validation const saneEntryPath = getValidPathInTape(entryPath); const saneDestFolder = getValidPathInTape(destFolder); @@ -21,11 +33,14 @@ export const moveEntry = command(z.object({ const newEntryPath = path.resolve(saneDestFolder, entryName); if (saneEntryPath === newEntryPath) { - return []; + return; } + // Operation await move(saneEntryPath, newEntryPath); + await getFileTree().refresh(); + // Emit modification const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); const modifications = [{ type: 'moved', @@ -33,26 +48,29 @@ export const moveEntry = command(z.object({ newPath: path.join(getRelativePathFromTape(saneDestFolder), entryName, isFolder ? '/' : ''), isFolder }] satisfies EntryModification[]; - console.log(modifications); - await getFileTree().refresh(); - return modifications; + await syncModification(modifications); }); +/** + * Rename an entry in a tape + * @fires socket 'remoteModification' event with modifications of type 'renamed' + */ export const renameEntry = command(z.object({ entryPath: z.string(), newName: z.string() -}), async ({ entryPath, newName }): Promise => { +}), async ({ entryPath, newName }): Promise => { const saneEntryPath = getValidPathInTape(entryPath); const sanitizedName = sanitizeFileName(newName); const targetFolder = path.dirname(saneEntryPath); const newPath = path.resolve(targetFolder, sanitizedName); if (saneEntryPath === newPath) { - return []; + return; } await move(saneEntryPath, newPath); + await getFileTree().refresh(); const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); const modifications = [{ @@ -61,20 +79,23 @@ export const renameEntry = command(z.object({ newPath: path.join(getRelativePathFromTape(newPath), isFolder ? '/' : ''), isFolder }] satisfies EntryModification[]; - console.log(modifications); - - await getFileTree().refresh(); - return modifications; + + await syncModification(modifications); }); +/** + * Remove an entry in a tape + * @fires socket 'remoteModification' event with modifications of type 'removed' + */ export const removeEntry = command(z.object({ entryPath: z.string() -}), async ({ entryPath }): Promise => { +}), async ({ entryPath }): Promise => { const saneEntryPath = getValidPathInTape(entryPath); const stats = await lstat(saneEntryPath); const isFolder = stats.isDirectory(); await rm(saneEntryPath, { recursive: true, force: true }); + await getFileTree().refresh(); const modifications = [{ type: 'removed', @@ -82,8 +103,6 @@ export const removeEntry = command(z.object({ newPath: '', isFolder }] satisfies EntryModification[]; - console.log(modifications); - await getFileTree().refresh(); - return modifications; + await syncModification(modifications); }); \ No newline at end of file diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index 743a0c3..3336d2b 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -3,6 +3,7 @@ import ioClient, {Socket} from 'socket.io-client'; import { createSubscriber } from 'svelte/reactivity'; import { browser } from '$app/environment'; import { page } from '$app/state'; +import { getFileTree } from './remotes/files.remote'; import type { CoreAPI } from '$core/CoreAPI.svelte'; import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; @@ -53,6 +54,10 @@ class ClientSocket { } update(); }); + socket.on('remoteModification', async (modifications) => { + await getFileTree().refresh(); + await this.core.syncStates(modifications, 'socket'); + }); }); } diff --git a/src/types/socket/index.ts b/src/types/socket/index.ts index 1b1451a..f30e376 100644 --- a/src/types/socket/index.ts +++ b/src/types/socket/index.ts @@ -1,9 +1,14 @@ +import type { EntryModification } from '$types/modification'; import type { TabKind } from '$types/tabs'; export type ClientToServerEvents = { - 'tab-opened': (_params: {id: string, kind: TabKind}, cb: (usersNb: number) => void) => void + 'tab-opened': ( + _params: {id: string, kind: TabKind}, + cb: (usersNb: number) => void + ) => void 'tab-closed': (_params: {id: string, kind: TabKind}) => void; }; export type ServerClientEvents = { 'users-on-file': (_params: {file: string, usersNb: number}) => void; + 'remoteModification': (_params: EntryModification[]) => void; } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 777ab42..accdc5e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,13 +5,15 @@ import lucidePreprocess from 'vite-plugin-lucide-preprocess'; import {initServerWebsocket} from './server/websocket.js'; -import type {HttpServer} from 'vite'; +import type {HttpServer, Plugin} from 'vite'; -export const webSocketServer = { - name: 'webSocketServer', - configureServer(server: {httpServer: HttpServer | null}) { - initServerWebsocket(server.httpServer); - } +export const webSocketServer: () => Plugin = () => { + return { + name: 'webSocketServer', + configureServer(server: {httpServer: HttpServer | null}) { + initServerWebsocket(server.httpServer); + } + }; }; export default defineConfig({ @@ -19,6 +21,6 @@ export default defineConfig({ tailwindcss(), sveltekit(), lucidePreprocess(), - webSocketServer + webSocketServer() ], }); From f1f6f4c9e9847311f23da3d4f73a63313738092a Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 21:13:38 +0100 Subject: [PATCH 4/9] changed entries to use socket --- server/setupSocket.ts | 30 ++++ server/websocket.ts | 123 ---------------- src/hooks.server.ts | 3 + src/lib/core/internal/EntryAPI.svelte.ts | 7 +- src/lib/remotes/entries.remote.ts | 108 -------------- src/lib/remotes/files.remote.ts | 12 +- src/lib/remotes/files.utils.spec.ts | 4 +- src/lib/remotes/files.utils.ts | 22 ++- src/socket.server.ts | 178 +++++++++++++++++++++++ src/types/socket/index.ts | 4 + vite.config.ts | 2 +- 11 files changed, 241 insertions(+), 252 deletions(-) create mode 100644 server/setupSocket.ts delete mode 100644 server/websocket.ts delete mode 100644 src/lib/remotes/entries.remote.ts create mode 100644 src/socket.server.ts diff --git a/server/setupSocket.ts b/server/setupSocket.ts new file mode 100644 index 0000000..4b5e831 --- /dev/null +++ b/server/setupSocket.ts @@ -0,0 +1,30 @@ +import {Server} from 'socket.io'; + +import type {HttpServer} from 'vite'; +import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; + +/** + * Init the global websocket server object + * Intended to be called once, in vite plugin or express middleware, before sveltekit server hook import + */ +export function initServerWebsocket(server: HttpServer | null) { + if (!server) { + console.error('could not start websocket services'); + return; + } + + if (globalThis.myServerSocket) { + globalThis.myServerSocket.close(); + globalThis.myServerSocket = null; + } + const io = new Server(server); + globalThis.myServerSocket = io; +} + +export function getServerSocket() { + const io = globalThis.myServerSocket; + if (!io) { + throw new Error('Websocket server not initialized'); + } + return io; +} \ No newline at end of file diff --git a/server/websocket.ts b/server/websocket.ts deleted file mode 100644 index ee241d4..0000000 --- a/server/websocket.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {Server} from 'socket.io'; - -import type { TabKind } from '$types/tabs'; -import type {HttpServer} from 'vite'; -import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; - - -const tapeKey = 'tape:'; - -export function initServerWebsocket(server: HttpServer | null) { - if (!server) { - console.error('could not start websocket services'); - return; - } - - if (globalThis.myServerSocket) { - globalThis.myServerSocket.close(); - globalThis.myServerSocket = null; - } - - const fileUserMap = new Map< - string, // file path - Set // users connected - >(); - - const io = new Server(server); - globalThis.myServerSocket = io; - - - io.on('connection', async (socket) => { - socket.on('disconnect', () => { - for (const [_file, users] of fileUserMap) { - if (users.has(socket.id)) { - users.delete(socket.id); - } - } - }); - console.log( - 'a user connected', - socket.id, - socket.handshake.headers['x-tape-name'], - socket.handshake.headers.cookie - ); - - - // todo: imp - // I would like something like that, but ws is outside of vite and svelte, without env access - // await ensureTapeExists(tape); - - // same here, alias + env - // if (!socket.handshake.headers.cookie) { - // console.warn('Missing session cookie'); - // socket.disconnect(true); - // return; - // } - // const cookieObj = JSON.parse(socket.handshake.headers.cookie); - // const auth = await validateSessionToken( - // cookieObj['auth-session'] - // ); - // if (!auth || !auth.session) { - // console.warn('Invalid session token'); - // socket.disconnect(true); - // return; - // } - - const tape = socket.handshake.headers['x-tape-name']; - if (!tape || typeof tape !== 'string') { - console.warn('Invalid or missing tape name'); - socket.disconnect(true); - return; - } - - await socket.join(tapeKey + tape); - - socket.on('tab-opened', (tab: {id: string, kind: TabKind}, callback) => { - if (!socket.id || tab.kind !== 'file') return; - - console.info('[Info] Tab opened by', - socket.id, - tab, - 'on', - [...socket.rooms].filter(r => r.startsWith('tape:')).map(r => r.slice(tapeKey.length)).join() - ); - - if (!fileUserMap.has(tab.id)) { - fileUserMap.set(tab.id, new Set()); - } - const users = fileUserMap.get(tab.id)!; - users.add(socket.id); - - socket.broadcast.emit('users-on-file', { - file: tab.id, - usersNb: fileUserMap.get(tab.id)?.size ?? 0 - }); - console.log(fileUserMap.get(tab.id)?.size, 'users on', tab.id); - callback(fileUserMap.get(tab.id)?.size ?? 0); - }); - - socket.on('tab-closed', (tab: {id: string, kind: TabKind}) => { - if (!socket.id || tab.kind !== 'file') return; - console.info('[Info] Tab closed', tab); - - if (fileUserMap.has(tab.id)) { - const users = fileUserMap.get(tab.id)!; - users.delete(socket.id); - } - - socket.broadcast.emit('users-on-file', { - file: tab.id, - usersNb: fileUserMap.get(tab.id)?.size ?? 0 - }); - console.log(fileUserMap.get(tab.id)?.size, 'users on', tab.id); - }); - }); -} - -export function getServerSocket() { - const io = globalThis.myServerSocket; - if (!io) { - throw new Error('Websocket server not initialized'); - } - return io; -} \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 5bfd050..6a5dbad 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,8 @@ import type { Handle } from '@sveltejs/kit'; import * as auth from '$lib/server/auth'; +import { registerSvelteKitWebsocket } from './socket.server'; + +registerSvelteKitWebsocket(); const handleAuth: Handle = async ({ event, resolve }) => { const sessionToken = event.cookies.get(auth.sessionCookieName); diff --git a/src/lib/core/internal/EntryAPI.svelte.ts b/src/lib/core/internal/EntryAPI.svelte.ts index e2a5ee0..9f89c49 100644 --- a/src/lib/core/internal/EntryAPI.svelte.ts +++ b/src/lib/core/internal/EntryAPI.svelte.ts @@ -1,4 +1,3 @@ -import { moveEntry, removeEntry, renameEntry } from '$lib/remotes/entries.remote'; import type { CoreAPI } from '$core/CoreAPI.svelte'; import type { FolderEntry } from '$types/files'; @@ -12,7 +11,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ removeEntry = async (entryPath: string) => { - await removeEntry({ entryPath: entryPath }); + this.core.clientSocket?.socket.emit('entry-deleted', entryPath); }; /** * Rename a file or folder entry @@ -20,7 +19,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ renameEntry = async (entryPath: string, newName: string) => { - await renameEntry({ entryPath: entryPath, newName: newName }); + this.core.clientSocket?.socket.emit('entry-renamed', {entryPath, newName}); }; /** * Move a file or folder entry to a destination folder @@ -28,6 +27,6 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ moveEntry = async (entryPath: string, folderEntry: FolderEntry) => { - await moveEntry({ entryPath: entryPath, destFolder: folderEntry.path }); + this.core.clientSocket?.socket.emit('entry-moved', {entryPath, destFolder: folderEntry.path}); }; } \ No newline at end of file diff --git a/src/lib/remotes/entries.remote.ts b/src/lib/remotes/entries.remote.ts deleted file mode 100644 index 20d8d08..0000000 --- a/src/lib/remotes/entries.remote.ts +++ /dev/null @@ -1,108 +0,0 @@ -import z from 'zod'; -import path from 'node:path'; -import { rm, lstat } from 'node:fs/promises'; -import { move } from 'fs-extra/esm'; -import { command } from '$app/server'; - -import { getRelativePathFromTape, getValidPathInTape, sanitizeFileName } from './files.utils'; -import { getFileTree } from './files.remote'; -import { getServerSocket } from '../../../server/websocket'; - -import type { EntryModification } from '$types/modification'; - -async function syncModification(modifications: EntryModification[]) { - console.log(modifications); - - const io = getServerSocket(); - io.emit('remoteModification', modifications); -} - -/** - * Move an entry in a tape - * @fires socket 'remoteModification' event with modifications of type 'moved' - */ -export const moveEntry = command(z.object({ - entryPath: z.string(), - destFolder: z.string() -}), async ({ entryPath, destFolder }): Promise => { - // Validation - const saneEntryPath = getValidPathInTape(entryPath); - const saneDestFolder = getValidPathInTape(destFolder); - - const entryName = path.basename(saneEntryPath); - const newEntryPath = path.resolve(saneDestFolder, entryName); - - if (saneEntryPath === newEntryPath) { - return; - } - - // Operation - await move(saneEntryPath, newEntryPath); - await getFileTree().refresh(); - - // Emit modification - const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'moved', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(saneDestFolder), entryName, isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - - await syncModification(modifications); -}); - -/** - * Rename an entry in a tape - * @fires socket 'remoteModification' event with modifications of type 'renamed' - */ -export const renameEntry = command(z.object({ - entryPath: z.string(), - newName: z.string() -}), async ({ entryPath, newName }): Promise => { - const saneEntryPath = getValidPathInTape(entryPath); - const sanitizedName = sanitizeFileName(newName); - const targetFolder = path.dirname(saneEntryPath); - const newPath = path.resolve(targetFolder, sanitizedName); - - if (saneEntryPath === newPath) { - return; - } - - await move(saneEntryPath, newPath); - await getFileTree().refresh(); - - const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'renamed', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(newPath), isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - - await syncModification(modifications); -}); - -/** - * Remove an entry in a tape - * @fires socket 'remoteModification' event with modifications of type 'removed' - */ -export const removeEntry = command(z.object({ - entryPath: z.string() -}), async ({ entryPath }): Promise => { - const saneEntryPath = getValidPathInTape(entryPath); - const stats = await lstat(saneEntryPath); - const isFolder = stats.isDirectory(); - - await rm(saneEntryPath, { recursive: true, force: true }); - await getFileTree().refresh(); - - const modifications = [{ - type: 'removed', - oldPath: path.join(getRelativePathFromTape(saneEntryPath), isFolder ? '/' : ''), - newPath: '', - isFolder - }] satisfies EntryModification[]; - - await syncModification(modifications); -}); \ No newline at end of file diff --git a/src/lib/remotes/files.remote.ts b/src/lib/remotes/files.remote.ts index 847a4f1..d843eb4 100644 --- a/src/lib/remotes/files.remote.ts +++ b/src/lib/remotes/files.remote.ts @@ -6,7 +6,7 @@ import { error } from '@sveltejs/kit'; import { command, form, getRequestEvent, query } from '$app/server'; import { createFileTree } from '$lib'; import { env } from '$env/dynamic/private'; -import { getRelativePathFromTape, getValidPathInTape, sanitizeFileName } from './files.utils'; +import { getRelativePathInTape, getValidPathInTape, sanitizeFileName } from './files.utils'; import type { FileEntry, FsNode } from '$types/files'; const NOTE_DIR = env.NOTE_DIR; @@ -69,7 +69,7 @@ export const resolveFile = query(z.string(), async (filePath): Promise { describe('sanitizeFileName', () => { @@ -98,7 +98,7 @@ describe('files.utils', () => { it('should return the correct relative path', () => { const input = 'test-data/testing_tape/snup/___dap/t_pe.md'; const expected = 'snup/___dap/t_pe.md'; - expect(getRelativePathFromTape(input)).toBe(expected); + expect(getRelativePathInTape(input)).toBe(expected); }); }); diff --git a/src/lib/remotes/files.utils.ts b/src/lib/remotes/files.utils.ts index 4a5c7fa..21c3f5f 100644 --- a/src/lib/remotes/files.utils.ts +++ b/src/lib/remotes/files.utils.ts @@ -40,6 +40,13 @@ export function getFileExtension(fileName: string): string { return ext.startsWith('.') ? ext.slice(1) : ext; } +export function getValidPathFromTape(tape: string, p: string): string { + const sanePath = path.join(NOTE_DIR, tape, sanitizeFilePath(p)); + if (!isValidPath(sanePath)) { + throw new Error(`Invalid path: ${sanePath}`); + } + return sanePath; +} /** * @throws if no tape param is set * @throws if path is invalid @@ -50,18 +57,17 @@ export function getValidPathInTape(p: string): string { if (!params.tape) { throw new Error('No Tape param'); } - const sanePath = path.join(NOTE_DIR, params.tape, sanitizeFilePath(p)); - if (!isValidPath(sanePath)) { - throw new Error(`Invalid path: ${sanePath}`); - } - return sanePath; + return getValidPathFromTape(params.tape, p); } -export function getRelativePathFromTape(fullPath: string): string { +export function getRelativePathFromTape(tape: string, fullPath: string): string { + const tapePath = path.join(NOTE_DIR, tape); + return path.relative(tapePath, fullPath); +} +export function getRelativePathInTape(fullPath: string): string { const { params } = getRequestEvent(); if (!params.tape) { throw new Error('No Tape param'); } - const tapePath = path.join(NOTE_DIR, params.tape); - return path.relative(tapePath, fullPath); + return getRelativePathFromTape(params.tape, fullPath); } \ No newline at end of file diff --git a/src/socket.server.ts b/src/socket.server.ts new file mode 100644 index 0000000..4090d4a --- /dev/null +++ b/src/socket.server.ts @@ -0,0 +1,178 @@ +import path from 'node:path'; +import { move, remove } from 'fs-extra/esm'; +import { lstat } from 'fs/promises'; +import { getRelativePathFromTape, getValidPathFromTape, sanitizeFileName } from '$lib/remotes/files.utils'; +import type { EntryModification } from '$types/modification'; +import type { TabKind } from '$types/tabs'; + +const tapePrefix = 'tape:'; + +export function registerSvelteKitWebsocket() { + const io = globalThis.myServerSocket; + if (!io) { + console.error('Websocket server not initialized'); + return; + } + + const fileUserMap = new Map< + string, // file path + Set // users connected + >(); + + io.disconnectSockets(); + io.removeAllListeners(); + + io.on('connection', async (socket) => { + socket.on('disconnect', () => { + for (const [_file, users] of fileUserMap) { + if (users.has(socket.id)) { + users.delete(socket.id); + } + } + }); + console.log( + socket.id, + 'connected to', + socket.handshake.headers['x-tape-name'], + ); + + // todo: imp + // I would like something like that, but ws is outside of vite and svelte, without env access + // await ensureTapeExists(tape); + + // same here, alias + env + // if (!socket.handshake.headers.cookie) { + // console.warn('Missing session cookie'); + // socket.disconnect(true); + // return; + // } + // const cookieObj = JSON.parse(socket.handshake.headers.cookie); + // const auth = await validateSessionToken( + // cookieObj['auth-session'] + // ); + // if (!auth || !auth.session) { + // console.warn('Invalid session token'); + // socket.disconnect(true); + // return; + // } + + const tape = socket.handshake.headers['x-tape-name']; + if (!tape || typeof tape !== 'string') { + console.warn('Invalid or missing tape name'); + socket.disconnect(true); + return; + } + + await socket.join(tapePrefix + tape); + + /* * Tab operations handlers * */ + socket.on('tab-opened', (tab: {id: string, kind: TabKind}, callback) => { + if (!socket.id || tab.kind !== 'file') return; + + console.info('[Info] Tab opened by', + socket.id, + tab, + 'on', + [...socket.rooms].filter(r => r.startsWith('tape:')).map(r => r.slice(tapePrefix.length)).join() + ); + + if (!fileUserMap.has(tab.id)) { + fileUserMap.set(tab.id, new Set()); + } + const users = fileUserMap.get(tab.id)!; + users.add(socket.id); + + socket.broadcast.emit('users-on-file', { + file: tab.id, + usersNb: fileUserMap.get(tab.id)?.size ?? 0 + }); + console.log(fileUserMap.get(tab.id)?.size, 'users on', tab.id); + callback(fileUserMap.get(tab.id)?.size ?? 0); + }); + socket.on('tab-closed', (tab: {id: string, kind: TabKind}) => { + if (!socket.id || tab.kind !== 'file') return; + console.info('[Info] Tab closed', tab); + + if (fileUserMap.has(tab.id)) { + const users = fileUserMap.get(tab.id)!; + users.delete(socket.id); + } + + socket.broadcast.emit('users-on-file', { + file: tab.id, + usersNb: fileUserMap.get(tab.id)?.size ?? 0 + }); + console.log(fileUserMap.get(tab.id)?.size, 'users on', tab.id); + }); + + /* * Entry operations handlers * */ + socket.on('entry-deleted', async (entryPath: string) => { + const saneEntryPath = getValidPathFromTape(tape, entryPath); + const stats = await lstat(saneEntryPath); + const isFolder = stats.isDirectory(); + + await remove(saneEntryPath); + + const modifications = [{ + type: 'removed', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: '', + isFolder + }] satisfies EntryModification[]; + + io.to(tapePrefix + tape) + .emit('remoteModification', modifications); + }); + socket.on('entry-renamed', async (params: {entryPath: string, newName: string}) => { + const saneEntryPath = getValidPathFromTape(tape, params.entryPath); + const sanitizedName = sanitizeFileName(params.newName); + const targetFolder = path.dirname(saneEntryPath); + console.log(targetFolder); + + const newPath = path.resolve(targetFolder, sanitizedName); + + if (saneEntryPath === newPath) { + return; + } + + await move(saneEntryPath, newPath); + + const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); + const modifications = [{ + type: 'renamed', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(tape, newPath), isFolder ? '/' : ''), + isFolder + }] satisfies EntryModification[]; + + io.to(tapePrefix + tape) + .emit('remoteModification', modifications); + }); + socket.on('entry-moved', async (params: {entryPath: string, destFolder: string}) => { + // Validation + const saneEntryPath = getValidPathFromTape(tape, params.entryPath); + const saneDestFolder = getValidPathFromTape(tape, params.destFolder); + + const entryName = path.basename(saneEntryPath); + const newEntryPath = path.resolve(saneDestFolder, entryName); + + if (saneEntryPath === newEntryPath) { + return; + } + + // Operation + await move(saneEntryPath, newEntryPath); + + // Emit modification + const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); + const modifications = [{ + type: 'moved', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(tape, newEntryPath), isFolder ? '/' : ''), + isFolder + }] satisfies EntryModification[]; + io.to(tapePrefix + tape) + .emit('remoteModification', modifications); + }); + }); +} diff --git a/src/types/socket/index.ts b/src/types/socket/index.ts index f30e376..e996047 100644 --- a/src/types/socket/index.ts +++ b/src/types/socket/index.ts @@ -7,6 +7,10 @@ export type ClientToServerEvents = { cb: (usersNb: number) => void ) => void 'tab-closed': (_params: {id: string, kind: TabKind}) => void; + + 'entry-deleted': (_params: string) => void; + 'entry-renamed': (_params: {entryPath: string, newName: string}) => void; + 'entry-moved': (_params: {entryPath: string, destFolder: string}) => void; }; export type ServerClientEvents = { 'users-on-file': (_params: {file: string, usersNb: number}) => void; diff --git a/vite.config.ts b/vite.config.ts index accdc5e..96484bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; import lucidePreprocess from 'vite-plugin-lucide-preprocess'; -import {initServerWebsocket} from './server/websocket.js'; +import {initServerWebsocket} from './server/setupSocket.js'; import type {HttpServer, Plugin} from 'vite'; From c581fc10227d01f9cfef685e1beaaf0640c42413 Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 21:32:17 +0100 Subject: [PATCH 5/9] validation of socket endpoint + update when creating an entry --- src/lib/core/CoreAPI.svelte.ts | 1 + src/lib/remotes/files.remote.ts | 5 +++ src/socket.server.ts | 54 +++++++++++++++++++++++++++------ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/lib/core/CoreAPI.svelte.ts b/src/lib/core/CoreAPI.svelte.ts index 9e225ae..cc43ab6 100644 --- a/src/lib/core/CoreAPI.svelte.ts +++ b/src/lib/core/CoreAPI.svelte.ts @@ -186,6 +186,7 @@ class CoreAPI { async clear() { this.#tabStore.clear(); this.foldState.clear(); + this.#clientSocket?.socket.disconnect(); } } diff --git a/src/lib/remotes/files.remote.ts b/src/lib/remotes/files.remote.ts index d843eb4..1c61da4 100644 --- a/src/lib/remotes/files.remote.ts +++ b/src/lib/remotes/files.remote.ts @@ -8,6 +8,7 @@ import { createFileTree } from '$lib'; import { env } from '$env/dynamic/private'; import { getRelativePathInTape, getValidPathInTape, sanitizeFileName } from './files.utils'; import type { FileEntry, FsNode } from '$types/files'; +import { getServerSocket, tapePrefix } from '../../socket.server'; const NOTE_DIR = env.NOTE_DIR; @@ -120,6 +121,10 @@ export const createFile = form(z.object({ await getFileTree().refresh(); + const io = getServerSocket(); + io.to(tapePrefix + params.tape) + .emit('remoteModification', []); // will refresh client file tree + return { name: newFilename, path: getRelativePathInTape(saneFilePath), diff --git a/src/socket.server.ts b/src/socket.server.ts index 4090d4a..51666ac 100644 --- a/src/socket.server.ts +++ b/src/socket.server.ts @@ -1,11 +1,19 @@ import path from 'node:path'; +import { z } from 'zod'; import { move, remove } from 'fs-extra/esm'; import { lstat } from 'fs/promises'; import { getRelativePathFromTape, getValidPathFromTape, sanitizeFileName } from '$lib/remotes/files.utils'; import type { EntryModification } from '$types/modification'; import type { TabKind } from '$types/tabs'; -const tapePrefix = 'tape:'; +export const tapePrefix = 'tape:'; + +export function getServerSocket() { + if (!globalThis.myServerSocket) { + throw new Error('Websocket server not initialized'); + } + return globalThis.myServerSocket; +} export function registerSvelteKitWebsocket() { const io = globalThis.myServerSocket; @@ -106,8 +114,13 @@ export function registerSvelteKitWebsocket() { }); /* * Entry operations handlers * */ - socket.on('entry-deleted', async (entryPath: string) => { - const saneEntryPath = getValidPathFromTape(tape, entryPath); + socket.on('entry-deleted', async (entryDeletion: EntryDeletedParams) => { + const parsed = await entryDeletedSchema.safeParseAsync(entryDeletion); + if (!parsed.success) { + console.warn('Invalid entry deletion params:', parsed.error); + return; + } + const saneEntryPath = getValidPathFromTape(tape, parsed.data); const stats = await lstat(saneEntryPath); const isFolder = stats.isDirectory(); @@ -123,9 +136,14 @@ export function registerSvelteKitWebsocket() { io.to(tapePrefix + tape) .emit('remoteModification', modifications); }); - socket.on('entry-renamed', async (params: {entryPath: string, newName: string}) => { - const saneEntryPath = getValidPathFromTape(tape, params.entryPath); - const sanitizedName = sanitizeFileName(params.newName); + socket.on('entry-renamed', async (params: EntryRenamedParams) => { + const parsed = await entryRenamedSchema.safeParseAsync(params); + if (!parsed.success) { + console.warn('Invalid entry renamed params:', parsed.error); + return; + } + const saneEntryPath = getValidPathFromTape(tape, parsed.data.entryPath); + const sanitizedName = sanitizeFileName(parsed.data.newName); const targetFolder = path.dirname(saneEntryPath); console.log(targetFolder); @@ -148,10 +166,15 @@ export function registerSvelteKitWebsocket() { io.to(tapePrefix + tape) .emit('remoteModification', modifications); }); - socket.on('entry-moved', async (params: {entryPath: string, destFolder: string}) => { + socket.on('entry-moved', async (params: EntryMovedParams) => { // Validation - const saneEntryPath = getValidPathFromTape(tape, params.entryPath); - const saneDestFolder = getValidPathFromTape(tape, params.destFolder); + const parsed = await entryMovedSchema.safeParseAsync(params); + if (!parsed.success) { + console.warn('Invalid entry moved params:', parsed.error); + return; + } + const saneEntryPath = getValidPathFromTape(tape, parsed.data.entryPath); + const saneDestFolder = getValidPathFromTape(tape, parsed.data.destFolder); const entryName = path.basename(saneEntryPath); const newEntryPath = path.resolve(saneDestFolder, entryName); @@ -176,3 +199,16 @@ export function registerSvelteKitWebsocket() { }); }); } + +const entryDeletedSchema = z.string(); +const entryRenamedSchema = z.object({ + entryPath: z.string(), + newName: z.string()}); +const entryMovedSchema = z.object({ + entryPath: z.string(), + destFolder: z.string() +}); + +type EntryDeletedParams = z.infer; +type EntryRenamedParams = z.infer; +type EntryMovedParams = z.infer; \ No newline at end of file From 83b74f083a4b10b2a06546b94392f1a1fac86afa Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 21:34:40 +0100 Subject: [PATCH 6/9] added disconnect notification when leaving --- src/socket.server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/socket.server.ts b/src/socket.server.ts index 51666ac..38067b6 100644 --- a/src/socket.server.ts +++ b/src/socket.server.ts @@ -32,9 +32,13 @@ export function registerSvelteKitWebsocket() { io.on('connection', async (socket) => { socket.on('disconnect', () => { - for (const [_file, users] of fileUserMap) { + for (const [file, users] of fileUserMap) { if (users.has(socket.id)) { users.delete(socket.id); + socket.broadcast.emit('users-on-file', { + file, + usersNb: fileUserMap.get(file)?.size ?? 0 + }); } } }); From bf520b37e2af2611725569c2b4060f439e15d36f Mon Sep 17 00:00:00 2001 From: matysse Date: Sat, 21 Feb 2026 22:48:22 +0100 Subject: [PATCH 7/9] refacto reorganisation --- src/app.d.ts | 11 ++- src/lib/server/entries.ts | 67 +++++++++++++++ src/lib/server/tape.utils.ts | 24 ++++++ src/socket.server.ts | 160 +++++++++++++++-------------------- 4 files changed, 168 insertions(+), 94 deletions(-) create mode 100644 src/lib/server/entries.ts create mode 100644 src/lib/server/tape.utils.ts diff --git a/src/app.d.ts b/src/app.d.ts index bc41185..cd00f93 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,11 +1,20 @@ // See https://svelte.dev/docs/kit/types#app.d.ts +import type { validateSessionToken } from '$lib/server/auth'; import type { ClientToServerEvents, ServerClientEvents } from '$types/socket'; import type { Server } from 'socket.io'; // for information about these interfaces declare global { - var myServerSocket: Server< ClientToServerEvents, ServerClientEvents, object, object> | null; + var myServerSocket: Server< + ClientToServerEvents, + ServerClientEvents, + object, + { + auth: Awaited> + tape: string; + } + > | null; namespace App { // interface Error {} interface Locals { diff --git a/src/lib/server/entries.ts b/src/lib/server/entries.ts new file mode 100644 index 0000000..b3982be --- /dev/null +++ b/src/lib/server/entries.ts @@ -0,0 +1,67 @@ +import path from 'node:path'; +import { lstat } from 'fs/promises'; +import { move, remove } from 'fs-extra/esm'; +import { getRelativePathFromTape, getValidPathFromTape, sanitizeFileName } from '$lib/remotes/files.utils'; +import type { EntryModification } from '$types/modification'; + +export async function deleteEntry(tape: string, entryPath: string): Promise { + const saneEntryPath = getValidPathFromTape(tape, entryPath); + const stats = await lstat(saneEntryPath); + const isFolder = stats.isDirectory(); + + await remove(saneEntryPath); + + return { + type: 'removed', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: '', + isFolder + }; +} + +export async function renameEntry(tape: string, entryPath: string, newName: string): Promise { + const saneEntryPath = getValidPathFromTape(tape, entryPath); + const sanitizedName = sanitizeFileName(newName); + const targetFolder = path.dirname(saneEntryPath); + console.log(targetFolder); + + const newPath = path.resolve(targetFolder, sanitizedName); + + if (saneEntryPath === newPath) { + return null; + } + + await move(saneEntryPath, newPath); + + const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); + return { + type: 'renamed', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(tape, newPath), isFolder ? '/' : ''), + isFolder + }; +} + +export async function moveEntry(tape: string, entryPath: string, targetFolderPath: string): Promise { + const saneEntryPath = getValidPathFromTape(tape, entryPath); + const saneDestFolder = getValidPathFromTape(tape, targetFolderPath); + + const entryName = path.basename(saneEntryPath); + const newEntryPath = path.resolve(saneDestFolder, entryName); + + if (saneEntryPath === newEntryPath) { + return null; + } + + // Operation + await move(saneEntryPath, newEntryPath); + + // Emit modification + const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); + return { + type: 'moved', + oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), + newPath: path.join(getRelativePathFromTape(tape, newEntryPath), isFolder ? '/' : ''), + isFolder + }; +} \ No newline at end of file diff --git a/src/lib/server/tape.utils.ts b/src/lib/server/tape.utils.ts new file mode 100644 index 0000000..d9b96bb --- /dev/null +++ b/src/lib/server/tape.utils.ts @@ -0,0 +1,24 @@ +import { env } from '$env/dynamic/private'; +import { access } from 'node:fs/promises'; +import path from 'node:path'; + +const NOTE_DIR = env.NOTE_DIR; + +if (!NOTE_DIR) { + throw new Error('NOTE_DIR environment variable is not set'); +} + +export async function tapeExists(tapeName: string): Promise { + const tapePath = path.join(NOTE_DIR, tapeName); + try { + await access(tapePath); + return true; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } else { + console.error('Error accessing tape: ', err); + throw new Error('Failed to access tape'); + } + } +} \ No newline at end of file diff --git a/src/socket.server.ts b/src/socket.server.ts index 38067b6..514a722 100644 --- a/src/socket.server.ts +++ b/src/socket.server.ts @@ -1,10 +1,10 @@ -import path from 'node:path'; import { z } from 'zod'; -import { move, remove } from 'fs-extra/esm'; -import { lstat } from 'fs/promises'; -import { getRelativePathFromTape, getValidPathFromTape, sanitizeFileName } from '$lib/remotes/files.utils'; -import type { EntryModification } from '$types/modification'; +import { validateSessionToken } from '$lib/server/auth'; +import { tapeExists } from '$lib/server/tape.utils'; +import { deleteEntry, moveEntry, renameEntry } from '$lib/server/entries'; + import type { TabKind } from '$types/tabs'; +import type { Socket } from 'socket.io'; export const tapePrefix = 'tape:'; @@ -15,6 +15,47 @@ export function getServerSocket() { return globalThis.myServerSocket; } +async function validateAndAttachUserInfos(socket: Socket) { + // Check tape + const tape = socket.handshake.headers['x-tape-name']; + if (!tape || typeof tape !== 'string') { + console.warn('Invalid or missing tape name'); + socket.disconnect(true); + return; + } + const tapeExistsResult = await tapeExists(tape); + if (!tapeExistsResult) { + console.warn('Tape does not exist:', tape); + socket.disconnect(true); + return; + } + console.info('Client connected to tape:', tape); + socket.data.tape = tape; + + // Check auth + if (!socket.handshake.headers.cookie) { + console.warn('Missing session cookie'); + socket.disconnect(true); + return; + } + const cookieObj = JSON.parse(JSON.stringify(socket.handshake.headers.cookie.split(';').reduce((acc, cookie) => { + const [key, value] = cookie.trim().split('='); + acc[key] = value; + return acc; + }, {} as Record))); + const auth = await validateSessionToken( + cookieObj['auth-session'] + ); + if (!auth || !auth.session) { + console.warn('Invalid session token'); + socket.disconnect(true); + return; + } + console.info('Session token valid, user authenticated for tape:', tape); + socket.data.auth = auth; +} + + export function registerSvelteKitWebsocket() { const io = globalThis.myServerSocket; if (!io) { @@ -42,40 +83,12 @@ export function registerSvelteKitWebsocket() { } } }); - console.log( - socket.id, - 'connected to', - socket.handshake.headers['x-tape-name'], - ); - - // todo: imp - // I would like something like that, but ws is outside of vite and svelte, without env access - // await ensureTapeExists(tape); - - // same here, alias + env - // if (!socket.handshake.headers.cookie) { - // console.warn('Missing session cookie'); - // socket.disconnect(true); - // return; - // } - // const cookieObj = JSON.parse(socket.handshake.headers.cookie); - // const auth = await validateSessionToken( - // cookieObj['auth-session'] - // ); - // if (!auth || !auth.session) { - // console.warn('Invalid session token'); - // socket.disconnect(true); - // return; - // } - - const tape = socket.handshake.headers['x-tape-name']; - if (!tape || typeof tape !== 'string') { - console.warn('Invalid or missing tape name'); - socket.disconnect(true); - return; - } - - await socket.join(tapePrefix + tape); + + await validateAndAttachUserInfos(socket); + + // Join tape room + await socket.join(tapePrefix + socket.data.tape); + console.info(socket.data.auth.user?.username, 'joined tape', socket.data.tape, 'with socket id', socket.id); /* * Tab operations handlers * */ socket.on('tab-opened', (tab: {id: string, kind: TabKind}, callback) => { @@ -124,21 +137,11 @@ export function registerSvelteKitWebsocket() { console.warn('Invalid entry deletion params:', parsed.error); return; } - const saneEntryPath = getValidPathFromTape(tape, parsed.data); - const stats = await lstat(saneEntryPath); - const isFolder = stats.isDirectory(); - await remove(saneEntryPath); - - const modifications = [{ - type: 'removed', - oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), - newPath: '', - isFolder - }] satisfies EntryModification[]; - - io.to(tapePrefix + tape) - .emit('remoteModification', modifications); + const modif = await deleteEntry(socket.data.tape, parsed.data); + + io.to(tapePrefix + socket.data.tape) + .emit('remoteModification', [modif]); }); socket.on('entry-renamed', async (params: EntryRenamedParams) => { const parsed = await entryRenamedSchema.safeParseAsync(params); @@ -146,60 +149,31 @@ export function registerSvelteKitWebsocket() { console.warn('Invalid entry renamed params:', parsed.error); return; } - const saneEntryPath = getValidPathFromTape(tape, parsed.data.entryPath); - const sanitizedName = sanitizeFileName(parsed.data.newName); - const targetFolder = path.dirname(saneEntryPath); - console.log(targetFolder); - - const newPath = path.resolve(targetFolder, sanitizedName); - if (saneEntryPath === newPath) { + const modif = await renameEntry(socket.data.tape, parsed.data.entryPath, parsed.data.newName); + if (!modif) { return; } - await move(saneEntryPath, newPath); - - const isFolder = await lstat(newPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'renamed', - oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(tape, newPath), isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - - io.to(tapePrefix + tape) - .emit('remoteModification', modifications); + io.to(tapePrefix + socket.data.tape) + .emit('remoteModification', [modif]); }); socket.on('entry-moved', async (params: EntryMovedParams) => { - // Validation + // Validation const parsed = await entryMovedSchema.safeParseAsync(params); if (!parsed.success) { console.warn('Invalid entry moved params:', parsed.error); return; } - const saneEntryPath = getValidPathFromTape(tape, parsed.data.entryPath); - const saneDestFolder = getValidPathFromTape(tape, parsed.data.destFolder); - - const entryName = path.basename(saneEntryPath); - const newEntryPath = path.resolve(saneDestFolder, entryName); - if (saneEntryPath === newEntryPath) { + const {entryPath, destFolder} = parsed.data; + const modif = await moveEntry(socket.data.tape, entryPath, destFolder); + if (!modif) { return; } - - // Operation - await move(saneEntryPath, newEntryPath); - - // Emit modification - const isFolder = await lstat(newEntryPath).then(stats => stats.isDirectory()).catch(() => false); - const modifications = [{ - type: 'moved', - oldPath: path.join(getRelativePathFromTape(tape, saneEntryPath), isFolder ? '/' : ''), - newPath: path.join(getRelativePathFromTape(tape, newEntryPath), isFolder ? '/' : ''), - isFolder - }] satisfies EntryModification[]; - io.to(tapePrefix + tape) - .emit('remoteModification', modifications); + + io.to(tapePrefix + socket.data.tape) + .emit('remoteModification', [modif]); }); }); } From 760f1b00a4e0f15a133221bea2b96e690df53bf9 Mon Sep 17 00:00:00 2001 From: matysse Date: Mon, 23 Feb 2026 21:14:27 +0100 Subject: [PATCH 8/9] better socket info --- src/lib/{websocket.ts => clientSocket.ts} | 3 +++ src/lib/components/SideBar/FileTree/Entry.svelte | 2 ++ src/lib/components/SideBar/Header.svelte | 3 +++ src/lib/components/SideBar/SideBar.svelte | 10 +++++++++- src/lib/core/CoreAPI.svelte.ts | 7 +++++-- src/lib/core/internal/stores/TabStore.svelte.ts | 4 ++-- src/routes/(app)/tape/[tape]/BottomNav.svelte | 16 +++++++++++++--- src/routes/(app)/tape/[tape]/StatusBar.svelte | 11 ++++++----- 8 files changed, 43 insertions(+), 13 deletions(-) rename src/lib/{websocket.ts => clientSocket.ts} (95%) diff --git a/src/lib/websocket.ts b/src/lib/clientSocket.ts similarity index 95% rename from src/lib/websocket.ts rename to src/lib/clientSocket.ts index 3336d2b..8a333b6 100644 --- a/src/lib/websocket.ts +++ b/src/lib/clientSocket.ts @@ -35,6 +35,9 @@ export function getSocket(core: CoreAPI) { return clientSocket; } +/** + * Small wrapper around socket.io-client instance to handle reactivity in Svelte + */ class ClientSocket { #socket; #subscribe; diff --git a/src/lib/components/SideBar/FileTree/Entry.svelte b/src/lib/components/SideBar/FileTree/Entry.svelte index 4d8c9c6..93b2d26 100644 --- a/src/lib/components/SideBar/FileTree/Entry.svelte +++ b/src/lib/components/SideBar/FileTree/Entry.svelte @@ -82,6 +82,7 @@ { e.stopPropagation(); renaming = true; @@ -92,6 +93,7 @@ { e.stopPropagation(); open = false; diff --git a/src/lib/components/SideBar/Header.svelte b/src/lib/components/SideBar/Header.svelte index 7d59cd5..8eca9ca 100644 --- a/src/lib/components/SideBar/Header.svelte +++ b/src/lib/components/SideBar/Header.svelte @@ -1,5 +1,6 @@
+ {#if !coreAPI.clientSocket?.connected} +
+ +
+ {/if}
{@render header?.()} diff --git a/src/lib/core/CoreAPI.svelte.ts b/src/lib/core/CoreAPI.svelte.ts index cc43ab6..ad29f51 100644 --- a/src/lib/core/CoreAPI.svelte.ts +++ b/src/lib/core/CoreAPI.svelte.ts @@ -6,7 +6,7 @@ import { getCurrentTape } from '$lib/remotes/files.remote'; import { ViewMap } from '$components/Main/View'; import { Page } from './Page.svelte'; import { InfoUi, type ModificationOrigin } from './InfosUi.svelte'; -import { type ClientSocket, getSocket } from '$lib/websocket'; +import { type ClientSocket, getSocket } from '$lib/clientSocket'; import type { FileEntry } from '$types/files'; import type { EntryModification } from '$types/modification'; import type { TabFileEntry } from '$types/tabs'; @@ -43,11 +43,14 @@ class CoreAPI { this.#clientSocket = getSocket(this); } + /** + * @reactive call to the socket instance in a reactive context to subscribe to its events and get the latest socket data + */ get clientSocket() { if (!this.#clientSocket) { this.#clientSocket = getSocket(this); } - return this.#clientSocket; + return this.#clientSocket?.socket ?? null; } /** diff --git a/src/lib/core/internal/stores/TabStore.svelte.ts b/src/lib/core/internal/stores/TabStore.svelte.ts index dad04bf..03e3adb 100644 --- a/src/lib/core/internal/stores/TabStore.svelte.ts +++ b/src/lib/core/internal/stores/TabStore.svelte.ts @@ -28,7 +28,7 @@ export class TabStore { id: tab.id, }; - this.core.clientSocket?.socket.emit('tab-opened', { + this.core.clientSocket?.emit('tab-opened', { id: tab.id, kind: tab.kind }, (users: number) => { @@ -51,7 +51,7 @@ export class TabStore { this.tabs = afterTabs; - this.core.clientSocket?.socket.emit('tab-closed', { + this.core.clientSocket?.emit('tab-closed', { id: tab!.id, kind: tab!.kind }); diff --git a/src/routes/(app)/tape/[tape]/BottomNav.svelte b/src/routes/(app)/tape/[tape]/BottomNav.svelte index b54a97d..b807fa1 100644 --- a/src/routes/(app)/tape/[tape]/BottomNav.svelte +++ b/src/routes/(app)/tape/[tape]/BottomNav.svelte @@ -14,9 +14,19 @@
+
+ {#if coreAPI.clientSocket?.connected} + + {coreAPI.activeTabInfos?.usersNb ? `(${coreAPI.activeTabInfos?.usersNb})` : ''} + + + {:else} + + {/if} +
diff --git a/src/routes/(app)/tape/[tape]/StatusBar.svelte b/src/routes/(app)/tape/[tape]/StatusBar.svelte index e3b977c..f3c002b 100644 --- a/src/routes/(app)/tape/[tape]/StatusBar.svelte +++ b/src/routes/(app)/tape/[tape]/StatusBar.svelte @@ -22,13 +22,14 @@
{/key}
- {#if coreAPI.clientSocket?.socket?.connected} - - WS Connected {coreAPI.activeTab ? coreAPI.activeTabInfos?.usersNb : ''} + {#if coreAPI.clientSocket?.connected} + + {coreAPI.activeTabInfos?.usersNb ? `(${coreAPI.activeTabInfos?.usersNb} on file)` : ''} + {:else} - - WS Disconnected + + {/if} {#if settings.saveSet.saving} From 28e40ed019c1a61eb70286683c5d196bd5c6b411 Mon Sep 17 00:00:00 2001 From: matysse Date: Mon, 23 Feb 2026 21:15:56 +0100 Subject: [PATCH 9/9] fix lint --- src/lib/core/internal/EntryAPI.svelte.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/core/internal/EntryAPI.svelte.ts b/src/lib/core/internal/EntryAPI.svelte.ts index 9f89c49..ea7fb3e 100644 --- a/src/lib/core/internal/EntryAPI.svelte.ts +++ b/src/lib/core/internal/EntryAPI.svelte.ts @@ -11,7 +11,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ removeEntry = async (entryPath: string) => { - this.core.clientSocket?.socket.emit('entry-deleted', entryPath); + this.core.clientSocket?.emit('entry-deleted', entryPath); }; /** * Rename a file or folder entry @@ -19,7 +19,7 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ renameEntry = async (entryPath: string, newName: string) => { - this.core.clientSocket?.socket.emit('entry-renamed', {entryPath, newName}); + this.core.clientSocket?.emit('entry-renamed', {entryPath, newName}); }; /** * Move a file or folder entry to a destination folder @@ -27,6 +27,6 @@ export class EntryAPI { * @fires {@linkcode FileAPI.getActiveFile} */ moveEntry = async (entryPath: string, folderEntry: FolderEntry) => { - this.core.clientSocket?.socket.emit('entry-moved', {entryPath, destFolder: folderEntry.path}); + this.core.clientSocket?.emit('entry-moved', {entryPath, destFolder: folderEntry.path}); }; } \ No newline at end of file