Use tanstackdb for chat list

This commit is contained in:
Hakan Shehu
2025-11-20 10:46:57 -08:00
parent 86cef58b26
commit 58c551b62e
6 changed files with 18 additions and 128 deletions

View File

@@ -1,99 +0,0 @@
import { SelectNode } from '@colanode/client/databases/workspace';
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import { mapNode } from '@colanode/client/lib/mappers';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { ChatListQueryInput } from '@colanode/client/queries/chats/chat-list';
import { Event } from '@colanode/client/types/events';
import { LocalChatNode } from '@colanode/client/types/nodes';
export class ChatListQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<ChatListQueryInput>
{
public async handleQuery(
input: ChatListQueryInput
): Promise<LocalChatNode[]> {
const rows = await this.fetchChildren(input);
return rows.map(mapNode) as LocalChatNode[];
}
public async checkForChanges(
event: Event,
input: ChatListQueryInput,
output: LocalChatNode[]
): Promise<ChangeCheckResult<ChatListQueryInput>> {
if (
event.type === 'workspace.deleted' &&
event.workspace.userId === input.userId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'node.created' &&
event.workspace.userId === input.userId &&
event.node.type === 'chat'
) {
const newChildren = [...output, event.node];
return {
hasChanges: true,
result: newChildren,
};
}
if (
event.type === 'node.updated' &&
event.workspace.userId === input.userId &&
event.node.type === 'chat'
) {
const node = output.find((node) => node.id === event.node.id);
if (node) {
const newChildren = output.map((node) =>
node.id === event.node.id ? (event.node as LocalChatNode) : node
);
return {
hasChanges: true,
result: newChildren,
};
}
}
if (
event.type === 'node.deleted' &&
event.workspace.userId === input.userId &&
event.node.type === 'chat'
) {
const node = output.find((node) => node.id === event.node.id);
if (node) {
const newChildren = output.filter((node) => node.id !== event.node.id);
return {
hasChanges: true,
result: newChildren,
};
}
}
return {
hasChanges: false,
};
}
private async fetchChildren(
input: ChatListQueryInput
): Promise<SelectNode[]> {
const workspace = this.getWorkspace(input.userId);
const rows = await workspace.database
.selectFrom('nodes')
.selectAll()
.where('parent_id', 'is', null)
.where('type', '=', 'chat')
.execute();
return rows;
}
}

View File

@@ -6,7 +6,6 @@ import { AccountListQueryHandler } from './accounts/accounts-list';
import { MetadataListQueryHandler } from './apps/metadata-list'; import { MetadataListQueryHandler } from './apps/metadata-list';
import { TabsListQueryHandler } from './apps/tabs-list'; import { TabsListQueryHandler } from './apps/tabs-list';
import { AvatarGetQueryHandler } from './avatars/avatar-get'; import { AvatarGetQueryHandler } from './avatars/avatar-get';
import { ChatListQueryHandler } from './chats/chat-list';
import { DatabaseListQueryHandler } from './databases/database-list'; import { DatabaseListQueryHandler } from './databases/database-list';
import { DatabaseViewListQueryHandler } from './databases/database-view-list'; import { DatabaseViewListQueryHandler } from './databases/database-view-list';
import { DocumentGetQueryHandler } from './documents/document-get'; import { DocumentGetQueryHandler } from './documents/document-get';
@@ -89,7 +88,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'user.storage.get': new UserStorageGetQueryHandler(app), 'user.storage.get': new UserStorageGetQueryHandler(app),
'local.file.get': new LocalFileGetQueryHandler(app), 'local.file.get': new LocalFileGetQueryHandler(app),
'file.download.request.get': new FileDownloadRequestGetQueryHandler(app), 'file.download.request.get': new FileDownloadRequestGetQueryHandler(app),
'chat.list': new ChatListQueryHandler(app),
'space.list': new SpaceListQueryHandler(app), 'space.list': new SpaceListQueryHandler(app),
'document.get': new DocumentGetQueryHandler(app), 'document.get': new DocumentGetQueryHandler(app),
'document.state.get': new DocumentStateGetQueryHandler(app), 'document.state.get': new DocumentStateGetQueryHandler(app),

View File

@@ -1,17 +0,0 @@
import { LocalChatNode } from '@colanode/client/types/nodes';
export type ChatListQueryInput = {
type: 'chat.list';
page: number;
count: number;
userId: string;
};
declare module '@colanode/client/queries' {
interface QueryMap {
'chat.list': {
input: ChatListQueryInput;
output: LocalChatNode[];
};
}
}

View File

@@ -2,7 +2,6 @@ import { sha256 } from 'js-sha256';
export * from './accounts/account-list'; export * from './accounts/account-list';
export * from './apps/metadata-list'; export * from './apps/metadata-list';
export * from './chats/chat-list';
export * from './databases/database-list'; export * from './databases/database-list';
export * from './databases/database-view-list'; export * from './databases/database-view-list';
export * from './documents/document-get'; export * from './documents/document-get';

View File

@@ -2,6 +2,7 @@ import { Collection, createLiveQueryCollection, eq } from '@tanstack/react-db';
import { import {
Download, Download,
LocalChatNode,
LocalNode, LocalNode,
LocalRecordNode, LocalRecordNode,
Upload, Upload,
@@ -26,6 +27,7 @@ class WorkspaceCollections {
public readonly uploads: Collection<Upload, string>; public readonly uploads: Collection<Upload, string>;
public readonly nodes: Collection<LocalNode, string>; public readonly nodes: Collection<LocalNode, string>;
public readonly records: Collection<LocalRecordNode>; public readonly records: Collection<LocalRecordNode>;
public readonly chats: Collection<LocalChatNode>;
constructor(userId: string) { constructor(userId: string) {
this.userId = userId; this.userId = userId;
@@ -34,8 +36,14 @@ class WorkspaceCollections {
this.uploads = createUploadsCollection(userId); this.uploads = createUploadsCollection(userId);
this.nodes = createNodesCollection(userId); this.nodes = createNodesCollection(userId);
this.chats = createLiveQueryCollection((q) =>
q.from({ nodes: this.nodes }).where(({ nodes }) => eq(nodes.type, 'chat'))
);
this.records = createLiveQueryCollection((q) => this.records = createLiveQueryCollection((q) =>
q.from({ node: this.nodes }).where(({ node }) => eq(node.type, 'record')) q
.from({ nodes: this.nodes })
.where(({ nodes }) => eq(nodes.type, 'record'))
); );
} }
} }

View File

@@ -1,21 +1,22 @@
import { useLiveQuery } from '@tanstack/react-db';
import { collections } from '@colanode/ui/collections';
import { ChatCreatePopover } from '@colanode/ui/components/chats/chat-create-popover'; import { ChatCreatePopover } from '@colanode/ui/components/chats/chat-create-popover';
import { ChatSidebarItem } from '@colanode/ui/components/chats/chat-sidebar-item'; import { ChatSidebarItem } from '@colanode/ui/components/chats/chat-sidebar-item';
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header'; import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
import { Link } from '@colanode/ui/components/ui/link'; import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace'; import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const SidebarChats = () => { export const SidebarChats = () => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const chatListQuery = useLiveQuery({ const chatListQuery = useLiveQuery((q) =>
type: 'chat.list', q
userId: workspace.userId, .from({ chats: collections.workspace(workspace.userId).chats })
page: 0, .orderBy(({ chats }) => chats.id, 'asc')
count: 100, );
});
const chats = chatListQuery.data ?? []; const chats = chatListQuery.data;
return ( return (
<div className="flex flex-col group/sidebar h-full px-2"> <div className="flex flex-col group/sidebar h-full px-2">