Improve tanstack db collections

This commit is contained in:
Hakan Shehu
2025-10-08 11:04:47 +02:00
parent 3d336b9ef3
commit aa5fac683e
8 changed files with 233 additions and 165 deletions

View File

@@ -26,8 +26,6 @@ export const AppProvider = ({ type }: AppProviderProps) => {
database
.preload()
.then(() => {
console.log('Colanode | Database', database);
console.log('Colanode | Preloaded');
setIsInitialized(true);
})
.catch((err) => {

View File

@@ -105,7 +105,7 @@ export const LayoutDesktop = () => {
routeTree,
context: {},
history: createMemoryHistory({
initialEntries: [tab.location],
initialEntries: [tab.location ?? '/'],
}),
defaultPreload: 'intent',
scrollRestoration: true,

View File

@@ -2,21 +2,19 @@ import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { useTabManager } from '@colanode/ui/contexts/tab-manager';
import { useAccountMetadata } from '@colanode/ui/hooks/use-account-metadata';
import { useAppMetadata } from '@colanode/ui/hooks/use-app-metadata';
export const TabAddButton = () => {
const tabManager = useTabManager();
const lastActiveAccount = useAppMetadata('account');
const lastActiveWorkspace = useAccountMetadata('workspace');
const location = useMemo(() => {
if (lastActiveAccount && lastActiveWorkspace) {
return `/acc/${lastActiveAccount}/${lastActiveWorkspace}/home`;
if (lastActiveAccount) {
return `/acc/${lastActiveAccount}`;
}
return '/';
}, [lastActiveAccount, lastActiveWorkspace]);
}, [lastActiveAccount]);
return (
<button

View File

@@ -1,25 +1,43 @@
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { createCollection } from '@tanstack/react-db';
import { QueryClient } from '@tanstack/react-query';
import { buildQueryKey, AccountListQueryInput } from '@colanode/client/queries';
import { Account } from '@colanode/client/types';
export const createAccountsCollection = (queryClient: QueryClient) => {
const input: AccountListQueryInput = {
type: 'account.list',
};
export const createAccountsCollection = () => {
return createCollection<Account, string>({
getKey(item) {
return item.id;
},
sync: {
async sync({ begin, write, commit, markReady }) {
const accounts = await window.colanode.executeQuery({
type: 'account.list',
});
const key = buildQueryKey(input);
begin();
return createCollection(
queryCollectionOptions({
id: key,
queryKey: [key],
queryClient,
getKey: (item) => item.id,
queryFn: async () => {
return await window.colanode.executeQueryAndSubscribe(key, input);
for (const account of accounts) {
write({ type: 'insert', value: account });
}
commit();
markReady();
window.eventBus.subscribe((event) => {
if (event.type === 'account.created') {
begin();
write({ type: 'insert', value: event.account });
commit();
} else if (event.type === 'account.updated') {
begin();
write({ type: 'update', value: event.account });
commit();
} else if (event.type === 'account.deleted') {
begin();
write({ type: 'delete', value: event.account });
commit();
}
});
},
})
);
},
});
};

View File

@@ -1,70 +1,89 @@
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { createCollection } from '@tanstack/react-db';
import { QueryClient } from '@tanstack/react-query';
import {
AppMetadataListQueryInput,
buildQueryKey,
} from '@colanode/client/queries';
import { AppMetadata } from '@colanode/client/types';
export const createAppMetadataCollection = (queryClient: QueryClient) => {
const input: AppMetadataListQueryInput = {
type: 'app.metadata.list',
};
const key = buildQueryKey(input);
export const createAppMetadataCollection = () => {
return createCollection<AppMetadata, string>({
getKey(item) {
return item.key;
},
sync: {
async sync({ begin, write, commit, markReady, collection }) {
const appMetadata = await window.colanode.executeQuery({
type: 'app.metadata.list',
});
return createCollection(
queryCollectionOptions({
id: key,
queryKey: [key],
queryClient,
getKey: (item) => item.key,
queryFn: async () => {
return await window.colanode.executeQueryAndSubscribe(key, input);
},
onInsert: async ({ transaction }) => {
const metadata = transaction.mutations[0].modified;
return await window.colanode.executeMutation({
type: 'app.metadata.update',
key: metadata.key,
value: metadata.value,
begin();
for (const item of appMetadata) {
write({ type: 'insert', value: item });
}
commit();
markReady();
window.eventBus.subscribe((event) => {
if (event.type === 'app.metadata.updated') {
const existing = collection.get(event.metadata.key);
if (existing) {
begin();
write({ type: 'update', value: event.metadata });
commit();
} else {
begin();
write({ type: 'insert', value: event.metadata });
commit();
}
} else if (event.type === 'app.metadata.deleted') {
begin();
write({ type: 'delete', value: event.metadata });
commit();
}
});
},
onUpdate: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original, changes } = mutation;
if (!(`key` in original)) {
throw new Error(`Original todo not found for update`);
}
},
onInsert: async ({ transaction }) => {
const metadata = transaction.mutations[0].modified;
return await window.colanode.executeMutation({
type: 'app.metadata.update',
key: metadata.key,
value: metadata.value,
});
},
onUpdate: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original, changes } = mutation;
if (!(`key` in original)) {
throw new Error(`Original todo not found for update`);
}
if (!changes.value) {
return;
}
if (!changes.value) {
return;
}
return await window.colanode.executeMutation({
type: 'app.metadata.update',
key: original.key,
value: changes.value,
});
})
);
},
onDelete: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original } = mutation;
if (!(`key` in original)) {
throw new Error(`Original app metadata not found for delete`);
}
return await window.colanode.executeMutation({
type: 'app.metadata.update',
key: original.key,
value: changes.value,
});
})
);
},
onDelete: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original } = mutation;
if (!(`key` in original)) {
throw new Error(`Original app metadata not found for delete`);
}
await window.colanode.executeMutation({
type: 'app.metadata.delete',
key: original.key,
});
})
);
},
})
);
await window.colanode.executeMutation({
type: 'app.metadata.delete',
key: original.key,
});
})
);
},
});
};

View File

@@ -85,10 +85,10 @@ class WorkspaceDatabase {
}
class AppDatabase {
public readonly servers = createServersCollection(queryClient);
public readonly accounts = createAccountsCollection(queryClient);
public readonly tabs = createTabsCollection(queryClient);
public readonly metadata = createAppMetadataCollection(queryClient);
public readonly servers = createServersCollection();
public readonly accounts = createAccountsCollection();
public readonly tabs = createTabsCollection();
public readonly metadata = createAppMetadataCollection();
private readonly accountDatabases: Map<string, AccountDatabase> = new Map();

View File

@@ -1,26 +1,43 @@
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { createCollection } from '@tanstack/react-db';
import { QueryClient } from '@tanstack/react-query';
import { buildQueryKey, ServerListQueryInput } from '@colanode/client/queries';
import { Server } from '@colanode/client/types';
export const createServersCollection = (queryClient: QueryClient) => {
const input: ServerListQueryInput = {
type: 'server.list',
};
export const createServersCollection = () => {
return createCollection<Server, string>({
getKey(item) {
return item.domain;
},
sync: {
async sync({ begin, write, commit, markReady }) {
const servers = await window.colanode.executeQuery({
type: 'server.list',
});
const key = buildQueryKey(input);
begin();
return createCollection(
queryCollectionOptions({
id: 'servers',
queryKey: [key],
queryClient,
getKey: (item) => item.domain,
queryFn: async () => {
console.log('Colanode | Executing query', key, input);
return await window.colanode.executeQueryAndSubscribe(key, input);
for (const server of servers) {
write({ type: 'insert', value: server });
}
commit();
markReady();
window.eventBus.subscribe((event) => {
if (event.type === 'server.created') {
begin();
write({ type: 'insert', value: event.server });
commit();
} else if (event.type === 'server.updated') {
begin();
write({ type: 'update', value: event.server });
commit();
} else if (event.type === 'server.deleted') {
begin();
write({ type: 'delete', value: event.server });
commit();
}
});
},
})
);
},
});
};

View File

@@ -1,66 +1,84 @@
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { createCollection } from '@tanstack/react-db';
import { QueryClient } from '@tanstack/react-query';
import { buildQueryKey, TabsListQueryInput } from '@colanode/client/queries';
import { Tab } from '@colanode/client/types';
export const createTabsCollection = (queryClient: QueryClient) => {
const input: TabsListQueryInput = {
type: 'tabs.list',
};
export const createTabsCollection = () => {
return createCollection<Tab, string>({
getKey(item) {
return item.id;
},
sync: {
async sync({ begin, write, commit, markReady }) {
const tabs = await window.colanode.executeQuery({
type: 'tabs.list',
});
const key = buildQueryKey(input);
begin();
return createCollection(
queryCollectionOptions({
id: key,
queryKey: [key],
queryClient,
getKey: (item) => item.id,
queryFn: async () => {
return await window.colanode.executeQueryAndSubscribe(key, input);
},
onInsert: async ({ transaction }) => {
const tab = transaction.mutations[0].modified;
return await window.colanode.executeMutation({
type: 'tab.create',
id: tab.id,
location: tab.location,
index: tab.index,
for (const tab of tabs) {
write({ type: 'insert', value: tab });
}
commit();
markReady();
window.eventBus.subscribe((event) => {
if (event.type === 'tab.created') {
begin();
write({ type: 'insert', value: event.tab });
commit();
} else if (event.type === 'tab.updated') {
begin();
write({ type: 'update', value: event.tab });
commit();
} else if (event.type === 'tab.deleted') {
begin();
write({ type: 'delete', value: event.tab });
commit();
}
});
},
onUpdate: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original, changes } = mutation;
if (!(`id` in original)) {
throw new Error(`Original todo not found for update`);
}
},
onInsert: async ({ transaction }) => {
const tab = transaction.mutations[0].modified;
return await window.colanode.executeMutation({
type: 'tab.create',
id: tab.id,
location: tab.location,
index: tab.index,
});
},
onUpdate: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original, changes } = mutation;
if (!(`id` in original)) {
throw new Error(`Original todo not found for update`);
}
return await window.colanode.executeMutation({
type: 'tab.update',
id: original.id,
location: changes.location,
index: changes.index,
});
})
);
},
onDelete: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original } = mutation;
if (!(`id` in original)) {
throw new Error(`Original todo not found for delete`);
}
return await window.colanode.executeMutation({
type: 'tab.update',
id: original.id,
location: changes.location,
index: changes.index,
});
})
);
},
onDelete: async ({ transaction }) => {
return await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original } = mutation;
if (!(`id` in original)) {
throw new Error(`Original todo not found for delete`);
}
await window.colanode.executeMutation({
type: 'tab.delete',
id: original.id,
});
})
);
},
})
);
await window.colanode.executeMutation({
type: 'tab.delete',
id: original.id,
});
})
);
},
});
};