Use tanstackdb for temp files

This commit is contained in:
Hakan Shehu
2025-10-11 15:40:30 +02:00
parent 35820350eb
commit 71319c48c0
11 changed files with 174 additions and 195 deletions

View File

@@ -1,75 +0,0 @@
import { SelectAccount } from '@colanode/client/databases/app';
import { mapAccount } from '@colanode/client/lib/mappers';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { AccountGetQueryInput } from '@colanode/client/queries/accounts/account-get';
import { AppService } from '@colanode/client/services/app-service';
import { Account } from '@colanode/client/types/accounts';
import { Event } from '@colanode/client/types/events';
export class AccountGetQueryHandler
implements QueryHandler<AccountGetQueryInput>
{
private readonly app: AppService;
constructor(app: AppService) {
this.app = app;
}
public async handleQuery(
input: AccountGetQueryInput
): Promise<Account | null> {
const row = await this.fetchAccount(input.accountId);
if (!row) {
return null;
}
return mapAccount(row);
}
public async checkForChanges(
event: Event,
input: AccountGetQueryInput
): Promise<ChangeCheckResult<AccountGetQueryInput>> {
if (
event.type === 'account.created' &&
event.account.id === input.accountId
) {
return {
hasChanges: true,
result: event.account,
};
}
if (
event.type === 'account.updated' &&
event.account.id === input.accountId
) {
return {
hasChanges: true,
result: event.account,
};
}
if (
event.type === 'account.deleted' &&
event.account.id === input.accountId
) {
return {
hasChanges: true,
result: null,
};
}
return {
hasChanges: false,
};
}
private fetchAccount(accountId: string): Promise<SelectAccount | undefined> {
return this.app.database
.selectFrom('accounts')
.selectAll()
.where('id', '=', accountId)
.executeTakeFirst();
}
}

View File

@@ -1,63 +0,0 @@
import { mapTempFile } from '@colanode/client/lib';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { TempFileGetQueryInput } from '@colanode/client/queries';
import { AppService } from '@colanode/client/services';
import { Event } from '@colanode/client/types/events';
import { TempFile } from '@colanode/client/types/files';
export class TempFileGetQueryHandler
implements QueryHandler<TempFileGetQueryInput>
{
private readonly app: AppService;
public constructor(app: AppService) {
this.app = app;
}
public async handleQuery(
input: TempFileGetQueryInput
): Promise<TempFile | null> {
return await this.fetchTempFile(input);
}
public async checkForChanges(
event: Event,
input: TempFileGetQueryInput,
_: TempFile | null
): Promise<ChangeCheckResult<TempFileGetQueryInput>> {
if (event.type === 'temp.file.created' && event.tempFile.id === input.id) {
return {
hasChanges: true,
result: event.tempFile,
};
}
if (event.type === 'temp.file.deleted' && event.tempFile.id === input.id) {
return {
hasChanges: true,
result: null,
};
}
return {
hasChanges: false,
};
}
private async fetchTempFile(
input: TempFileGetQueryInput
): Promise<TempFile | null> {
const tempFile = await this.app.database
.selectFrom('temp_files')
.selectAll()
.where('id', '=', input.id)
.executeTakeFirst();
if (!tempFile) {
return null;
}
const url = await this.app.fs.url(tempFile.path);
return mapTempFile(tempFile, url);
}
}

View File

@@ -0,0 +1,65 @@
import { mapTempFile } from '@colanode/client/lib';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { TempFileListQueryInput } from '@colanode/client/queries';
import { AppService } from '@colanode/client/services';
import { Event } from '@colanode/client/types/events';
import { TempFile } from '@colanode/client/types/files';
export class TempFileListQueryHandler
implements QueryHandler<TempFileListQueryInput>
{
private readonly app: AppService;
public constructor(app: AppService) {
this.app = app;
}
public async handleQuery(_: TempFileListQueryInput): Promise<TempFile[]> {
return await this.fetchTempFiles();
}
public async checkForChanges(
event: Event,
input: TempFileListQueryInput,
output: TempFile[]
): Promise<ChangeCheckResult<TempFileListQueryInput>> {
if (event.type === 'temp.file.created') {
const newResult = [...output, event.tempFile];
return {
hasChanges: true,
result: newResult,
};
}
if (event.type === 'temp.file.deleted') {
const newResult = output.filter(
(tempFile) => tempFile.id !== event.tempFile.id
);
return {
hasChanges: true,
result: newResult,
};
}
return {
hasChanges: false,
};
}
private async fetchTempFiles(): Promise<TempFile[]> {
const tempFiles = await this.app.database
.selectFrom('temp_files')
.selectAll()
.execute();
const result: TempFile[] = [];
for (const tempFile of tempFiles) {
const url = await this.app.fs.url(tempFile.path);
result.push(mapTempFile(tempFile, url));
}
return result;
}
}

View File

@@ -2,7 +2,6 @@ import { QueryHandler } from '@colanode/client/lib/types';
import { QueryMap } from '@colanode/client/queries';
import { AppService } from '@colanode/client/services/app-service';
import { AccountGetQueryHandler } from './accounts/account-get';
import { AccountListQueryHandler } from './accounts/accounts-list';
import { MetadataListQueryHandler } from './apps/metadata-list';
import { TabsListQueryHandler } from './apps/tabs-list';
@@ -23,7 +22,7 @@ import { DownloadListQueryHandler } from './files/download-list';
import { FileDownloadRequestGetQueryHandler } from './files/file-download-request-get';
import { FileListQueryHandler } from './files/file-list';
import { LocalFileGetQueryHandler } from './files/local-file-get';
import { TempFileGetQueryHandler } from './files/temp-file-get';
import { TempFileListQueryHandler } from './files/temp-file-list';
import { UploadListQueryHandler } from './files/upload-list';
import { IconCategoryListQueryHandler } from './icons/icon-category-list';
import { IconListQueryHandler } from './icons/icon-list';
@@ -77,7 +76,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'icon.category.list': new IconCategoryListQueryHandler(app),
'node.children.get': new NodeChildrenGetQueryHandler(app),
'radar.data.get': new RadarDataGetQueryHandler(app),
'account.get': new AccountGetQueryHandler(app),
'database.list': new DatabaseListQueryHandler(app),
'database.view.list': new DatabaseViewListQueryHandler(app),
'record.search': new RecordSearchQueryHandler(app),
@@ -92,7 +90,7 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'workspace.storage.get': new WorkspaceStorageGetQueryHandler(app),
'upload.list': new UploadListQueryHandler(app),
'download.list': new DownloadListQueryHandler(app),
'temp.file.get': new TempFileGetQueryHandler(app),
'temp.file.list': new TempFileListQueryHandler(app),
'icon.svg.get': new IconSvgGetQueryHandler(app),
'emoji.svg.get': new EmojiSvgGetQueryHandler(app),
'tabs.list': new TabsListQueryHandler(app),

View File

@@ -1,15 +0,0 @@
import { Account } from '@colanode/client/types/accounts';
export type AccountGetQueryInput = {
type: 'account.get';
accountId: string;
};
declare module '@colanode/client/queries' {
interface QueryMap {
'account.get': {
input: AccountGetQueryInput;
output: Account | null;
};
}
}

View File

@@ -1,15 +0,0 @@
import { TempFile } from '@colanode/client/types';
export type TempFileGetQueryInput = {
type: 'temp.file.get';
id: string;
};
declare module '@colanode/client/queries' {
interface QueryMap {
'temp.file.get': {
input: TempFileGetQueryInput;
output: TempFile | null;
};
}
}

View File

@@ -0,0 +1,14 @@
import { TempFile } from '@colanode/client/types';
export type TempFileListQueryInput = {
type: 'temp.file.list';
};
declare module '@colanode/client/queries' {
interface QueryMap {
'temp.file.list': {
input: TempFileListQueryInput;
output: TempFile[];
};
}
}

View File

@@ -1,6 +1,5 @@
import { sha256 } from 'js-sha256';
export * from './accounts/account-get';
export * from './accounts/account-list';
export * from './apps/metadata-list';
export * from './chats/chat-list';
@@ -39,7 +38,7 @@ export * from './records/record-field-value-count';
export * from './workspaces/workspace-storage-get';
export * from './files/upload-list';
export * from './files/download-list';
export * from './files/temp-file-get';
export * from './files/temp-file-list';
export * from './icons/icon-svg-get';
export * from './emojis/emoji-svg-get';
export * from './apps/tabs-list';

View File

@@ -6,6 +6,7 @@ import { createDownloadsCollection } from '@colanode/ui/data/downloads';
import { createMetadataCollection } from '@colanode/ui/data/metadata';
import { createServersCollection } from '@colanode/ui/data/servers';
import { createTabsCollection } from '@colanode/ui/data/tabs';
import { createTempFilesCollection } from '@colanode/ui/data/temp-files';
import { createUploadsCollection } from '@colanode/ui/data/uploads';
import { createUsersCollection } from '@colanode/ui/data/users';
import { createWorkspacesCollection } from '@colanode/ui/data/workspaces';
@@ -31,6 +32,7 @@ class AppDatabase {
public readonly tabs = createTabsCollection();
public readonly metadata = createMetadataCollection();
public readonly workspaces = createWorkspacesCollection();
public readonly tempFiles = createTempFilesCollection();
private readonly workspaceDatabases: Map<string, WorkspaceDatabase> =
new Map();
@@ -54,6 +56,7 @@ class AppDatabase {
this.metadata.preload(),
this.tabs.preload(),
this.workspaces.preload(),
this.tempFiles.preload(),
]);
}

View File

@@ -0,0 +1,47 @@
import { createCollection } from '@tanstack/react-db';
import { TempFile } from '@colanode/client/types';
export const createTempFilesCollection = () => {
return createCollection<TempFile, string>({
getKey(item) {
return item.id;
},
sync: {
sync({ begin, write, commit, markReady }) {
window.colanode
.executeQuery({
type: 'temp.file.list',
})
.then((tempFiles) => {
begin();
for (const tempFile of tempFiles) {
write({ type: 'insert', value: tempFile });
}
commit();
markReady();
});
const subscriptionId = window.eventBus.subscribe((event) => {
if (event.type === 'temp.file.created') {
begin();
write({ type: 'insert', value: event.tempFile });
commit();
} else if (event.type === 'temp.file.deleted') {
begin();
write({ type: 'delete', value: event.tempFile });
commit();
}
});
return {
cleanup: () => {
window.eventBus.unsubscribe(subscriptionId);
},
};
},
},
});
};

View File

@@ -1,49 +1,65 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { type NodeViewProps } from '@tiptap/core';
import { NodeViewWrapper } from '@tiptap/react';
import { X } from 'lucide-react';
import { TempFile } from '@colanode/client/types';
import { FileSubtype } from '@colanode/core';
import { FileNoPreview } from '@colanode/ui/components/files/file-no-preview';
import { FilePreviewAudio } from '@colanode/ui/components/files/previews/file-preview-audio';
import { FilePreviewImage } from '@colanode/ui/components/files/previews/file-preview-image';
import { FilePreviewVideo } from '@colanode/ui/components/files/previews/file-preview-video';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { database } from '@colanode/ui/data';
import { canPreviewFile } from '@colanode/ui/lib/files';
const TempFilePreview = ({ file }: { file: TempFile }) => {
if (file.subtype === 'image') {
return <FilePreviewImage url={file.url} name={file.name} />;
interface TempFilePreviewProps {
name: string;
mimeType: string;
subtype: FileSubtype;
url: string;
}
const TempFilePreview = ({
name,
mimeType,
subtype,
url,
}: TempFilePreviewProps) => {
if (subtype === 'image') {
return <FilePreviewImage url={url} name={name} />;
}
if (file.subtype === 'video') {
return <FilePreviewVideo url={file.url} />;
if (subtype === 'video') {
return <FilePreviewVideo url={url} />;
}
if (file.subtype === 'audio') {
return <FilePreviewAudio url={file.url} />;
if (subtype === 'audio') {
return <FilePreviewAudio url={url} />;
}
return <FileNoPreview mimeType={file.mimeType} />;
return <FileNoPreview mimeType={mimeType} />;
};
export const TempFileNodeView = ({ node, deleteNode }: NodeViewProps) => {
const fileId = node.attrs.id;
const tempFileQuery = useLiveQuery(
{
type: 'temp.file.get',
id: fileId,
},
{
enabled: !!fileId,
}
const tempFileQuery = useLiveQuery((q) =>
q
.from({ tempFiles: database.tempFiles })
.where(({ tempFiles }) => eq(tempFiles.id, fileId))
.select(({ tempFiles }) => ({
name: tempFiles.name,
mimeType: tempFiles.mimeType,
subtype: tempFiles.subtype,
url: tempFiles.url,
}))
.findOne()
);
if (!fileId || tempFileQuery.isPending || !tempFileQuery.data) {
const tempFile = tempFileQuery.data;
if (!fileId || !tempFile) {
return null;
}
const tempFile = tempFileQuery.data;
const mimeType = tempFile.mimeType;
const subtype = tempFile.subtype;
const canPreview = canPreviewFile(subtype);
@@ -61,7 +77,12 @@ export const TempFileNodeView = ({ node, deleteNode }: NodeViewProps) => {
<X className="size-4" />
</button>
{canPreview ? (
<TempFilePreview file={tempFile} />
<TempFilePreview
name={tempFile.name}
mimeType={tempFile.mimeType}
subtype={tempFile.subtype}
url={tempFile.url}
/>
) : (
<FileNoPreview mimeType={mimeType} />
)}