frontend: refactor api structure

This commit is contained in:
Nikita Galaiko 2023-04-25 10:09:28 +02:00
parent 96bccad65e
commit a2c332302c
47 changed files with 312 additions and 316 deletions

View File

@ -0,0 +1,2 @@
export { default as Api } from './api';
export type { User, LoginToken, Project } from './api';

2
src/lib/api/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './ipc';
export { Api as CloudApi, type User, type LoginToken } from './cloud';

View File

@ -1,8 +1,7 @@
import { log } from '$lib';
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { clone } from './utils';
import { clone } from '$lib/utils';
import { writable } from 'svelte/store';
export type OperationDelete = { delete: [number, number] };
export type OperationInsert = { insert: [number, string] };
@ -19,11 +18,6 @@ export namespace Operation {
export type Delta = { timestampMs: number; operations: Operation[] };
export type DeltasEvent = {
deltas: Delta[];
filePath: string;
};
const cache: Record<string, Record<string, Promise<Record<string, Delta[]>>>> = {};
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
@ -53,22 +47,27 @@ export const list = async (params: { projectId: string; sessionId: string; paths
);
};
export default async (params: { projectId: string; sessionId: string }) => {
const init = await list(params);
const store = writable<Record<string, Delta[]>>(init);
appWindow.listen<DeltasEvent>(
export const subscribe = (
params: { projectId: string; sessionId: string },
callback: (params: {
projectId: string;
sessionId: string;
filePath: string;
deltas: Delta[];
}) => Promise<void> | void
) =>
appWindow.listen<{ deltas: Delta[]; filePath: string }>(
`project://${params.projectId}/sessions/${params.sessionId}/deltas`,
(event) => {
log.info(
`Received deltas for ${params.projectId}, ${params.sessionId}, ${event.payload.filePath}`
);
store.update((deltas) => ({
...deltas,
[event.payload.filePath]: event.payload.deltas
}));
}
(event) => callback({ ...params, ...event.payload })
);
return store as Readable<Record<string, Delta[]>>;
export const Deltas = async (params: { projectId: string; sessionId: string }) => {
const store = writable(await list(params));
subscribe(params, ({ filePath, deltas }) => {
store.update((deltasCache) => {
deltasCache[filePath] = deltas;
return deltasCache;
});
});
return { subscribe: store.subscribe };
};

31
src/lib/api/ipc/files.ts Normal file
View File

@ -0,0 +1,31 @@
import { invoke } from '@tauri-apps/api';
import { clone } from '$lib/utils';
const cache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
const sessionFilesCache = cache[params.projectId] || {};
if (params.sessionId in sessionFilesCache) {
return sessionFilesCache[params.sessionId].then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
}
const promise = invoke<Record<string, string>>('list_session_files', {
sessionId: params.sessionId,
projectId: params.projectId
});
sessionFilesCache[params.sessionId] = promise;
cache[params.projectId] = sessionFilesCache;
return promise.then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
};

View File

@ -0,0 +1,28 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { get, writable } from 'svelte/store';
export type Activity = {
type: string;
timestampMs: number;
message: string;
};
export const list = (params: { projectId: string; startTimeMs?: number }) =>
invoke<Activity[]>('git_activity', params);
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string }) => Promise<void> | void
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback(params));
export const Activities = async (params: { projectId: string }) => {
const store = writable(await list(params));
subscribe(params, async () => {
const activity = get(store);
const startTimeMs = activity.at(-1)?.timestampMs;
const newActivities = await list({ projectId: params.projectId, startTimeMs });
store.update((activities) => [...activities, ...newActivities]);
});
return { subscribe: store.subscribe };
};

View File

@ -0,0 +1,13 @@
import { invoke } from '@tauri-apps/api';
import { writable } from 'svelte/store';
import { sessions, git } from '$lib/api';
const list = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_wd_diff', params);
export const Diffs = async (params: { projectId: string }) => {
const store = writable(await list(params));
git.activities.subscribe(params, ({ projectId }) => list({ projectId }).then(store.set));
sessions.subscribe(params, () => list(params).then(store.set));
return { subscribe: store.subscribe };
};

View File

@ -0,0 +1,19 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { derived, writable } from 'svelte/store';
export const get = (params: { projectId: string }) => invoke<string>('git_head', params);
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string; head: string }) => Promise<void> | void
) =>
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, (event) =>
callback({ ...params, ...event.payload })
);
export const Head = async (params: { projectId: string }) => {
const store = writable(await get(params));
subscribe(params, ({ head }) => store.set(head));
return derived(store, (head) => head.replace('refs/heads/', ''));
};

View File

@ -1,7 +1,12 @@
import { invoke } from '@tauri-apps/api';
export * as statuses from './statuses';
export { Status } from './statuses';
export * as activities from './activities';
export type { Activity } from './activities';
export * as heads from './heads';
export * as diffs from './diffs';
export * as indexes from './indexes';
export { default as statuses } from './statuses';
export { default as activity } from './activity';
import { invoke } from '@tauri-apps/api';
export const commit = (params: { projectId: string; message: string; push: boolean }) =>
invoke<boolean>('git_commit', params);

View File

@ -0,0 +1,6 @@
import { appWindow } from '@tauri-apps/api/window';
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string }) => Promise<void>
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback({ ...params }));

View File

@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { writable } from 'svelte/store';
import { sessions, git } from '$lib/api';
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'typeChange' | 'other';
@ -16,22 +16,13 @@ export namespace Status {
'unstaged' in status && status.unstaged !== null;
}
const list = (params: { projectId: string }) =>
export const list = (params: { projectId: string }) =>
invoke<Record<string, Status>>('git_status', params);
export default async (params: { projectId: string }) => {
const statuses = await list(params);
const store = writable(statuses);
[
`project://${params.projectId}/git/index`,
`project://${params.projectId}/git/activity`,
`project://${params.projectId}/sessions`
].forEach((eventName) => {
appWindow.listen(eventName, async () => {
store.set(await list(params));
});
});
return store as Readable<Record<string, Status>>;
export const Statuses = async (params: { projectId: string }) => {
const store = writable(await list(params));
sessions.subscribe(params, () => list(params).then(store.set));
git.activities.subscribe(params, () => list(params).then(store.set));
git.indexes.subscribe(params, () => list(params).then(store.set));
return { subscribe: store.subscribe };
};

12
src/lib/api/ipc/index.ts Normal file
View File

@ -0,0 +1,12 @@
export * as git from './git';
export { Status, type Activity } from './git';
export * as deltas from './deltas';
export { type Delta, Operation } from './deltas';
export * as sessions from './sessions';
export { Session } from './sessions';
export * as users from './users';
export * as projects from './projects';
export type { Project } from './projects';
export * as searchResults from './search';
export { type SearchResult } from './search';
export * as files from './files';

View File

@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api';
import { derived, writable } from 'svelte/store';
import type { Project as ApiProject } from '$lib/api';
import type { Project as ApiProject } from '$lib/api/cloud';
import { derived, readable, writable } from 'svelte/store';
export type Project = {
id: string;
@ -9,9 +9,9 @@ export type Project = {
api: ApiProject & { sync: boolean };
};
const list = () => invoke<Project[]>('list_projects');
export const list = () => invoke<Project[]>('list_projects');
const update = (params: {
export const update = (params: {
project: {
id: string;
title?: string;
@ -19,14 +19,12 @@ const update = (params: {
};
}) => invoke<Project>('update_project', params);
const add = (params: { path: string }) => invoke<Project>('add_project', params);
export const add = (params: { path: string }) => invoke<Project>('add_project', params);
const del = (params: { id: string }) => invoke('delete_project', params);
export default async () => {
const init = await list();
const store = writable<Project[]>(init);
export const del = (params: { id: string }) => invoke('delete_project', params);
export const Projects = async () => {
const store = writable(await list());
return {
subscribe: store.subscribe,
get: (id: string) => {

View File

@ -10,7 +10,7 @@ export type SearchResult = {
highlighted: string[];
};
export const search = (params: {
export const list = (params: {
projectId: string;
query: string;
limit?: number;

View File

@ -0,0 +1,63 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { clone } from '$lib/utils';
import { writable } from 'svelte/store';
export namespace Session {
export const within = (session: Session | undefined, timestampMs: number) => {
if (!session) return false;
const { startTimestampMs, lastTimestampMs } = session.meta;
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
};
}
export type Session = {
id: string;
hash?: string;
meta: {
startTimestampMs: number;
lastTimestampMs: number;
branch?: string;
commit?: string;
};
};
const cache: Record<string, Promise<Session[]>> = {};
export const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
if (params.projectId in cache) {
return cache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
}
cache[params.projectId] = invoke<Session[]>('list_sessions', {
projectId: params.projectId
});
return cache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
};
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string; session: Session }) => Promise<void> | void
) =>
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) =>
callback({ ...params, session: event.payload })
);
export const Sessions = async (params: { projectId: string }) => {
const store = writable(await list(params));
subscribe(params, ({ session }) => {
store.update((sessions) => {
const index = sessions.findIndex((s) => s.id === session.id);
if (index === -1) return [...sessions, session];
sessions[index] = session;
return sessions;
});
});
return { subscribe: store.subscribe };
};

View File

@ -1,18 +1,15 @@
import type { User } from '$lib/api';
import { writable } from 'svelte/store';
import { invoke } from '@tauri-apps/api';
import { writable } from 'svelte/store';
const get = () => invoke<User | undefined>('get_user');
export const get = () => invoke<User | undefined>('get_user');
const set = (params: { user: User }) => invoke<void>('set_user', params);
export const set = (params: { user: User }) => invoke<void>('set_user', params);
const del = () => invoke<void>('delete_user');
export const del = () => invoke<void>('delete_user');
export default async () => {
const store = writable<User | undefined>(undefined);
const init = await get();
store.set(init);
export const CurrentUser = async () => {
const store = writable<User | undefined>(await get());
return {
subscribe: store.subscribe,
set: async (user: User) => {

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import type { Readable } from 'svelte/store';
import { IconHome } from './icons';
import { Tooltip } from '$lib/components';

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { type Delta, Operation } from '$lib/deltas';
import { type Delta, Operation } from '$lib/api';
import { lineDiff } from './diff';
import { create } from './CodeHighlighter';
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';

View File

@ -1,6 +1,6 @@
<script lang="ts">
import tinykeys from 'tinykeys';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { derived, readable, writable, type Readable } from 'svelte/store';
import { Modal } from '$lib/components';
import listAvailableCommands, { Action, type Group } from './commands';
@ -8,10 +8,12 @@
import { onMount } from 'svelte';
import { open } from '@tauri-apps/api/shell';
import { IconExternalLink } from '../icons';
import type { User } from '$lib/api';
export let projects: Readable<Project[]>;
export let addProject: (params: { path: string }) => Promise<Project>;
export let project = readable<Project | undefined>(undefined);
export let user = readable<User | undefined>(undefined);
const input = writable('');
const scopeToProject = writable(!!$project);

View File

@ -1,4 +1,4 @@
import type { Project } from '$lib/projects';
import { type Project, git } from '$lib/api';
import { open } from '@tauri-apps/api/dialog';
import { toasts } from '$lib';
import {
@ -12,7 +12,6 @@ import {
IconAdjustmentsHorizontal,
IconDiscord
} from '../icons';
import { matchFiles } from '$lib/git';
import type { SvelteComponent } from 'svelte';
import { format, startOfISOWeek, startOfMonth, subDays, subMonths, subWeeks } from 'date-fns';
@ -216,7 +215,7 @@ const fileGroup = ({
description: 'type part of a file name',
commands: []
}
: matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
: git.matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
title: 'Files',
description: files.length === 0 ? `no files containing '${input}'` : '',
commands: files.map((file) => ({

View File

@ -1,12 +1,10 @@
<script lang="ts">
import type Users from '$lib/users';
import type Api from '$lib/api';
import type { LoginToken } from '$lib/api';
import type { LoginToken, CloudApi, users } from '$lib/api';
import { derived, writable } from 'svelte/store';
import { open } from '@tauri-apps/api/shell';
export let user: Awaited<ReturnType<typeof Users>>;
export let api: Awaited<ReturnType<typeof Api>>;
export let user: Awaited<ReturnType<typeof users.CurrentUser>>;
export let api: Awaited<ReturnType<typeof CloudApi>>;
const pollForUser = async (token: string) => {
const apiUser = await api.login.user.get(token).catch(() => null);

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { collapsable } from '$lib/paths';
import { Status } from '$lib/git/statuses';
import { Status } from '$lib/api';
export let statuses: Record<string, Status>;
</script>

View File

@ -1,27 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
export type Activity = {
type: string;
timestampMs: number;
message: string;
};
const list = (params: { projectId: string; startTimeMs?: number }) =>
invoke<Activity[]>('git_activity', params);
export default async (params: { projectId: string }) => {
const activity = await list(params);
const store = writable(activity);
appWindow.listen(`project://${params.projectId}/git/activity`, async () => {
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
const startTimeMs = activity.at(-1)?.timestampMs;
const newActivities = await list({ projectId: params.projectId, startTimeMs });
store.update((activities) => [...activities, ...newActivities]);
});
return store as Readable<Activity[]>;
};

View File

@ -1,24 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
const getDiffs = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_wd_diff', params);
export default async (params: { projectId: string }) => {
const diffs = await getDiffs(params);
const store = writable(diffs);
appWindow.listen(`project://${params.projectId}/sessions`, async () => {
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
store.set(await getDiffs(params));
});
appWindow.listen(`project://${params.projectId}/git/index`, async () => {
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
store.set(await getDiffs(params));
});
return store as Readable<Record<string, string>>;
};

View File

@ -1,18 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { derived, writable } from 'svelte/store';
import { log } from '$lib';
const list = (params: { projectId: string }) => invoke<string>('git_head', params);
export default async (params: { projectId: string }) => {
const head = await list(params);
const store = writable(head);
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, async (payload) => {
log.info(`Status: Received git head event, projectId: ${params.projectId}`);
store.set(payload.payload.head);
});
return derived(store, (head) => head.replace('refs/heads/', ''));
};

View File

@ -1,8 +1,6 @@
export * as deltas from './deltas';
export * as projects from './projects';
export * as api from './api';
export * as log from './log';
export * as toasts from './toasts';
export * as sessions from './sessions';
export { Toaster } from './toasts';
export * as week from './week';
export * as uisessions from './uisessions';
export * from './search';

View File

@ -1,101 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
import type { Activity } from './git/activity';
import { clone } from './utils';
export namespace Session {
export const within = (session: Session | undefined, timestampMs: number) => {
if (!session) return false;
const { startTimestampMs, lastTimestampMs } = session.meta;
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
};
}
export type Session = {
id: string;
hash?: string;
meta: {
startTimestampMs: number;
lastTimestampMs: number;
branch?: string;
commit?: string;
};
activity: Activity[];
};
const filesCache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
export const listFiles = async (params: {
projectId: string;
sessionId: string;
paths?: string[];
}) => {
const sessionFilesCache = filesCache[params.projectId] || {};
if (params.sessionId in sessionFilesCache) {
return sessionFilesCache[params.sessionId].then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
}
const promise = invoke<Record<string, string>>('list_session_files', {
sessionId: params.sessionId,
projectId: params.projectId
});
sessionFilesCache[params.sessionId] = promise;
filesCache[params.projectId] = sessionFilesCache;
return promise.then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
};
const sessionsCache: Record<string, Promise<Session[]>> = {};
const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
if (params.projectId in sessionsCache) {
return sessionsCache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
}
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
projectId: params.projectId
});
return sessionsCache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
};
export default async (params: { projectId: string; earliestTimestampMs?: number }) => {
const store = writable([] as Session[]);
list(params).then((sessions) => {
store.set(sessions);
});
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
log.info(`Received sessions event, projectId: ${params.projectId}`);
const session = event.payload;
store.update((sessions) => {
const index = sessions.findIndex((session) => session.id === event.payload.id);
if (index === -1) {
return [...sessions, session];
} else {
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
}
});
});
return store as Readable<Session[]>;
};

View File

@ -1,4 +1,5 @@
import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
export { Toaster } from 'svelte-french-toast';
const defaultOptions = {
position: 'bottom-center' as ToastPosition,

View File

@ -1,5 +1,4 @@
import type { Session } from '$lib/sessions';
import type { Delta } from '$lib/deltas';
import type { Session, Delta } from '$lib/api';
export type UISession = {
session: Session;

View File

@ -2,14 +2,11 @@
import '../app.postcss';
import { open } from '@tauri-apps/api/dialog';
import { toasts } from '$lib';
import { toasts, Toaster } from '$lib';
import tinykeys from 'tinykeys';
import { Toaster } from 'svelte-french-toast';
import type { LayoutData } from './$types';
import { BackForwardButtons, Button } from '$lib/components';
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
import { BackForwardButtons, Button, CommandPalette, Breadcrumbs } from '$lib/components';
import { page } from '$app/stores';
import CommandPalette from '$lib/components/CommandPalette/CommandPalette.svelte';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
@ -79,5 +76,11 @@
<slot />
</div>
<Toaster />
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
<CommandPalette
bind:this={commandPalette}
{projects}
{project}
{user}
addProject={projects.add}
/>
</div>

View File

@ -1,11 +1,11 @@
import { readable } from 'svelte/store';
import type { LayoutLoad } from './$types';
import { building } from '$app/environment';
import type { Project } from '$lib/projects';
import Api from '$lib/api';
import type { Project } from '$lib/api';
import { Api } from '$lib/api/cloud';
import Posthog from '$lib/posthog';
import Sentry from '$lib/sentry';
import * as log from '$lib/log';
import { setup as setupLogger } from '$lib/log';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const ssr = false;
@ -23,7 +23,7 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
throw new Error('not implemented');
}
}
: await (await import('$lib/projects')).default();
: await (await import('$lib/api')).projects.Projects();
const user = building
? {
...readable<undefined>(undefined),
@ -34,8 +34,8 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
throw new Error('not implemented');
}
}
: await (await import('$lib/users')).default();
await log.setup();
: await (await import('$lib/api')).users.CurrentUser();
setupLogger();
return {
projects,
user,

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { LayoutData } from './$types';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { Button, Tooltip } from '$lib/components';
import { page } from '$app/stores';
import { goto } from '$app/navigation';

View File

@ -1,9 +1,7 @@
import { building } from '$app/environment';
import type { Session } from '$lib/sessions';
import type { Status } from '$lib/git/statuses';
import type { Session, Delta, Status } from '$lib/api';
import { readable } from 'svelte/store';
import type { LayoutLoad } from './$types';
import type { Delta } from '$lib/deltas';
import type { Readable } from 'svelte/store';
export const prerender = false;
@ -12,17 +10,19 @@ export const load: LayoutLoad = async ({ parent, params }) => {
const { projects } = await parent();
const sessions = building
? readable<Session[]>([])
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.sessions.Sessions({ projectId: params.projectId }));
const statuses = building
? readable<Record<string, Status>>({})
: await import('$lib/git/statuses').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) =>
m.git.statuses.Statuses({ projectId: params.projectId })
);
const head = building
? readable<string>('')
: await import('$lib/git/head').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.git.heads.Head({ projectId: params.projectId }));
const deltas = building
? () => Promise.resolve(readable<Record<string, Delta[]>>({}))
: (sessionId: string) =>
import('$lib/deltas').then((m) => m.default({ projectId: params.projectId, sessionId }));
import('$lib/api').then((m) => m.deltas.Deltas({ projectId: params.projectId, sessionId }));
const cache: Record<string, Promise<Readable<Record<string, Delta[]>>>> = {};
const cachedDeltas = (sessionId: string) => {

View File

@ -4,7 +4,7 @@
import { derived } from 'svelte/store';
import { IconGitBranch, IconLoading } from '$lib/components/icons';
import { asyncDerived } from '@square/svelte-store';
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import FileSummaries from './FileSummaries.svelte';
import { Button, Statuses, Tooltip } from '$lib/components';

View File

@ -1,13 +1,15 @@
import { building } from '$app/environment';
import type { PageLoad } from './$types';
import { readable } from 'svelte/store';
import type { Activity } from '$lib/git/activity';
import type { Activity } from '$lib/api';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const load: PageLoad = wrapLoadWithSentry(async ({ params }) => {
const activity = building
? readable<Activity[]>([])
: await import('$lib/git/activity').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) =>
m.git.activities.Activities({ projectId: params.projectId })
);
return {
activity
};

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import { bucketByTimestamp } from './histogram';
export let deltas: Delta[];

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { format, startOfDay } from 'date-fns';
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import { derived, type Readable } from 'svelte/store';
import { collapsable } from '$lib/paths';
import FileActivity from './FileActivity.svelte';

View File

@ -284,7 +284,7 @@
>
</div>
<style>
<style lang="postcss">
.automated-message {
@apply max-w-[500px] rounded-[18px] rounded-tl-md bg-zinc-200 text-[14px] font-medium text-zinc-800;
}

View File

@ -3,8 +3,7 @@
import { Button } from '$lib/components';
import { collapsable } from '$lib/paths';
import { derived, writable } from 'svelte/store';
import * as git from '$lib/git';
import { Status } from '$lib/git/statuses';
import { git, Status } from '$lib/api';
import DiffViewer from '$lib/components/DiffViewer.svelte';
import { error, success } from '$lib/toasts';
import { fly } from 'svelte/transition';

View File

@ -7,7 +7,7 @@ export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params }) => {
const { project } = await parent();
const diffs = building
? readable<Record<string, string>>({})
: await import('$lib/git/diffs').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.git.diffs.Diffs({ projectId: params.projectId }));
return {
diffs,
project

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { list as listDeltas } from '$lib/deltas';
import { deltas } from '$lib/api';
import { asyncDerived } from '@square/svelte-store';
import { format } from 'date-fns';
import { derived } from 'svelte/store';
@ -14,7 +14,7 @@
const dates = asyncDerived([sessions, fileFilter], async ([sessions, fileFilter]) => {
const sessionDeltas = await Promise.all(
sessions.map((session) =>
listDeltas({
deltas.list({
projectId,
sessionId: session.id,
paths: fileFilter ? [fileFilter] : undefined

View File

@ -1,20 +1,21 @@
<script lang="ts" context="module">
import { listFiles, type Session } from '$lib/sessions';
import { list as listDeltas, type Delta } from '$lib/deltas';
import { deltas, files, type Session, type Delta } from '$lib/api';
const enrichSession = async (projectId: string, session: Session, paths?: string[]) => {
const files = await listFiles({ projectId, sessionId: session.id, paths });
const deltas = await listDeltas({ projectId, sessionId: session.id, paths }).then((deltas) =>
Object.entries(deltas)
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
);
const deltasFiles = new Set(deltas.map(([path]) => path));
const sessionFiles = await files.list({ projectId, sessionId: session.id, paths });
const sessionDeltas = await deltas
.list({ projectId, sessionId: session.id, paths })
.then((deltas) =>
Object.entries(deltas)
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
);
const deltasFiles = new Set(sessionDeltas.map(([path]) => path));
return {
...session,
files: Object.fromEntries(
Object.entries(files).filter(([filepath]) => deltasFiles.has(filepath))
Object.entries(sessionFiles).filter(([filepath]) => deltasFiles.has(filepath))
),
deltas
deltas: sessionDeltas
};
};
</script>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import slider from '$lib/slider';
import { onMount } from 'svelte';
type RichSession = { id: string; deltas: [string, Delta][] };
export let sessions: RichSession[];

View File

@ -1,12 +1,10 @@
<script lang="ts">
import type { PageData } from './$types';
import { search, type SearchResult } from '$lib';
import { IconChevronLeft, IconChevronRight } from '$lib/components/icons';
import { listFiles } from '$lib/sessions';
import { files, deltas, searchResults, type SearchResult } from '$lib/api';
import { asyncDerived } from '@square/svelte-store';
import { IconLoading } from '$lib/components/icons';
import { format, formatDistanceToNow } from 'date-fns';
import { list as listDeltas } from '$lib/deltas';
import { CodeViewer } from '$lib/components';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
@ -29,16 +27,17 @@
index,
highlighted
}: SearchResult) => {
const [doc, deltas] = await Promise.all([
listFiles({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
listDeltas({ projectId, sessionId, paths: [filePath] })
const [doc, dd] = await Promise.all([
files.list({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
deltas
.list({ projectId, sessionId, paths: [filePath] })
.then((r) => r[filePath] ?? [])
.then((d) => d.slice(0, index + 1))
]);
const date = format(deltas[deltas.length - 1].timestampMs, 'yyyy-MM-dd');
const date = format(dd[dd.length - 1].timestampMs, 'yyyy-MM-dd');
return {
doc,
deltas,
deltas: dd,
filepath: filePath,
highlight: highlighted,
sessionId,
@ -47,11 +46,11 @@
};
};
const { store: searchResults, state: searchState } = asyncDerived(
const { store: results, state: searchState } = asyncDerived(
[query, project, offset],
async ([query, project, offset]) => {
if (!query || !project) return { page: [], total: 0, haveNext: false, havePrev: false };
const results = await search({ projectId: project.id, query, limit, offset });
const results = await searchResults.list({ projectId: project.id, query, limit, offset });
return {
page: await Promise.all(results.page.map(fetchResultData)),
haveNext: offset + limit < results.total,
@ -78,16 +77,16 @@
</figcaption>
{:else if $searchState?.isLoaded}
<figcaption class="mt-14">
{#if $searchResults.total > 0}
{#if $results.total > 0}
<p class="mb-2 text-xl text-[#D4D4D8]">Results for "{$query}"</p>
<p class="text-lg text-[#717179]">{$searchResults.total} change instances</p>
<p class="text-lg text-[#717179]">{$results.total} change instances</p>
{:else}
<p class="mb-2 text-xl text-[#D4D4D8]">No results for "{$query}"</p>
{/if}
</figcaption>
<ul class="search-result-list -mr-14 flex flex-auto flex-col gap-6 overflow-auto pb-6">
{#each $searchResults.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
{#each $results.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
{@const timestamp = deltas[deltas.length - 1].timestampMs}
<li class="search-result mr-14">
<a
@ -111,18 +110,18 @@
<nav class="mx-auto flex text-zinc-400">
<button
on:click={openPrevPage}
disabled={!$searchResults.havePrev}
disabled={!$results.havePrev}
title="Back"
class:text-zinc-50={$searchResults.havePrev}
class:text-zinc-50={$results.havePrev}
class="h-9 w-9 rounded-tl-md rounded-bl-md border border-r-0 border-zinc-700 hover:bg-zinc-700"
>
<IconChevronLeft class="ml-1 h-5 w-6" />
</button>
<button
on:click={openNextPage}
disabled={!$searchResults.haveNext}
disabled={!$results.haveNext}
title="Next"
class:text-zinc-50={$searchResults.haveNext}
class:text-zinc-50={$results.haveNext}
class="h-9 w-9 rounded-tr-md rounded-br-md border border-l border-zinc-700 hover:bg-zinc-700"
>
<IconChevronRight class="ml-1 h-5 w-6" />

View File

@ -3,7 +3,7 @@
import ResizeObserver from 'svelte-resize-observer';
import setupTerminal from './terminal';
import 'xterm/css/xterm.css';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { debounce } from '$lib/utils';
import { Button, Statuses } from '$lib/components';

View File

@ -1,4 +1,4 @@
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { Terminal } from 'xterm';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';

View File

@ -18,7 +18,7 @@ export default defineConfig({
ignoreEmpty: true
},
telemetry: false,
uploadSourceMaps: true
uploadSourceMaps: process.env.SENTRY_RELEASE !== undefined
}
}),
sveltekit()