Skip to content

Commit

Permalink
Add redux based service for stacks
Browse files Browse the repository at this point in the history
- uses RTK Query
  • Loading branch information
mtsgrd committed Jan 22, 2025
1 parent 1614e15 commit dd575b1
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 162 deletions.
47 changes: 47 additions & 0 deletions apps/desktop/src/lib/redux/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Tauri } from '$lib/backend/tauri';
import { createApi, type BaseQueryApi, type BaseQueryFn } from '@reduxjs/toolkit/query';

export const reduxApi = createApi({
reducerPath: 'api',
tagTypes: ['Stacks'],
baseQuery: tauriBaseQuery,
endpoints: (_) => {
return {};
}
});

function tauriBaseQuery<T>(
args: ApiArgs,
api: BaseQueryApi
): ReturnType<BaseQueryFn<ApiArgs, Promise<T>, TauriCommandError, object, object>> {
if (!hasTauriExtra(api.extra)) {
return { error: 'Redux dependency Tauri not found!' };
}
try {
return { data: api.extra.tauri.invoke(args.command, args.params) };
} catch (error: unknown) {
return { error };
}
}

type ApiArgs = {
command: string;
params: Record<string, unknown>;
};

type TauriCommandError = unknown;

/**
* Typeguard that makes `tauriBaseQuery` more concise.
*/
function hasTauriExtra(extra: unknown): extra is {
tauri: Tauri;
} {
return (
!!extra &&
typeof extra === 'object' &&
extra !== null &&
'tauri' in extra &&
extra.tauri instanceof Tauri
);
}
116 changes: 31 additions & 85 deletions apps/desktop/src/lib/redux/store.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,39 @@
import { branchReviewListingsReducer } from '@gitbutler/shared/branches/branchReviewListingsSlice';
import { branchesReducer } from '@gitbutler/shared/branches/branchesSlice';
import { latestBranchLookupsReducer } from '@gitbutler/shared/branches/latestBranchLookupSlice';
import { patchSectionsReducer } from '@gitbutler/shared/branches/patchSectionsSlice';
import { patchesReducer } from '@gitbutler/shared/branches/patchesSlice';
import { chatChannelsReducer } from '@gitbutler/shared/chat/chatChannelsSlice';
import { feedsReducer } from '@gitbutler/shared/feeds/feedsSlice';
import { postsReducer } from '@gitbutler/shared/feeds/postsSlice';
import { organizationsReducer } from '@gitbutler/shared/organizations/organizationsSlice';
import { projectsReducer } from '@gitbutler/shared/organizations/projectsSlice';
import { repositoryIdLookupsReducer } from '@gitbutler/shared/organizations/repositoryIdLookupsSlice';
import { exampleReducer } from '@gitbutler/shared/redux/example';
import { AppDispatch, AppState } from '@gitbutler/shared/redux/store.svelte';
import { usersReducer } from '@gitbutler/shared/users/usersSlice';
import { configureStore, createSelector, createSlice } from '@reduxjs/toolkit';

type DesktopOnly = {
value: number;
};

const desktopOnly = createSlice({
name: 'desktopOnly',
initialState: { value: 69 } as DesktopOnly,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
}
}
});

export const { increment: desktopIncrement, decrement: desktopDecrement } = desktopOnly.actions;

export class DesktopDispatch extends AppDispatch {
constructor(readonly dispatch: typeof DesktopState.prototype._store.dispatch) {
super(dispatch);
import { reduxApi } from './api';
import { configureStore } from '@reduxjs/toolkit';
import type { Tauri } from '$lib/backend/tauri';

export class DesktopRedux {
readonly store: ReturnType<typeof createStore>;
readonly dispatch: typeof DesktopRedux.prototype.store.dispatch;

constructor(readonly tauri: Tauri) {
this.store = createStore(tauri);
this.dispatch = this.store.dispatch;
this.rootState$ = this.store.getState();

$effect(() =>
this.store.subscribe(() => {
this.rootState$ = this.store.getState();
})
);
}
}

interface AppDesktopOnlyState {
readonly desktopOnly: ReturnType<typeof desktopOnly.reducer>;
rootState$ = $state({} as ReturnType<typeof this.store.getState>);
}

// There is some minor duplication in terms of what is declared, but we do get
// type errors if you are missing a base reducer in the configureStore call.
// As such, there shouldn't be any concern about the two getting out of sync.
// This is due to limitations in typescript.
export class DesktopState extends AppState implements AppDesktopOnlyState {
/**
* The base store.
*
* This is a low level API and should not be used directly.
* @private
*/
readonly _store = configureStore({
/**
* We need this function in order to declare the store type in `DesktopState`
* and then assign the value in the constructor.
*/
function createStore(tauri: Tauri) {
return configureStore({
reducer: {
examples: exampleReducer,
posts: postsReducer,
feeds: feedsReducer,
orgnaizations: organizationsReducer,
users: usersReducer,
projects: projectsReducer,
branches: branchesReducer,
patches: patchesReducer,
patchSections: patchSectionsReducer,
chatChannels: chatChannelsReducer,
repositoryIdLookups: repositoryIdLookupsReducer,
latestBranchLookups: latestBranchLookupsReducer,
branchReviewListings: branchReviewListingsReducer,
desktopOnly: desktopOnly.reducer
api: reduxApi.reducer
},
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware({
thunk: { extraArgument: { tauri } }
}).concat(reduxApi.middleware);
}
});

readonly appDispatch = new DesktopDispatch(this._store.dispatch);

/**
* Used to access the store directly. It is recommended to access state via
* selectors as they are more efficient.
*/
rootState = $state<ReturnType<typeof this._store.getState>>(this._store.getState());

protected selectSelf(state: ReturnType<typeof this._store.getState>) {
return state;
}

private readonly selectDesktopOnly = createSelector(
[this.selectSelf],
(rootState) => rootState.desktopOnly
);
readonly desktopOnly = $derived(this.selectDesktopOnly(this.rootState));
}
7 changes: 7 additions & 0 deletions apps/desktop/src/lib/stacks/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Return type of Tauri `stacks` command.
*/
export type Stack = {
id: string;
branchNames: string[];
};
48 changes: 48 additions & 0 deletions apps/desktop/src/lib/stacks/stackService.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { reduxApi } from '$lib/redux/api';
import { DesktopRedux } from '$lib/redux/store.svelte';
import type { Stack } from './stack';

enum Tags {
Stacks = 'Stacks'
}

export class StackService {
private stacksApi = reduxApi.injectEndpoints({
endpoints: (build) => ({
/** Fetches all stacks. */
get: build.query<Stack[], { projectId: string }>({
query: ({ projectId }) => ({ command: 'stacks', params: { projectId } }),
providesTags: [Tags.Stacks]
}),
new: build.mutation<Stack, { projectId: string }>({
query: ({ projectId }) => ({
command: 'create_virtual_branch',
params: { projectId, branch: {} }
}),
invalidatesTags: [Tags.Stacks]
})
})
});

constructor(private state: DesktopRedux) {}

poll(projectId: string) {
$effect(() => {
const { unsubscribe } = this.state.dispatch(
this.stacksApi.endpoints.get.initiate({ projectId })
);
return () => {
unsubscribe();
};
});
}

select(projectId: string) {
return this.stacksApi.endpoints.get.select({ projectId });
}

// eslint-disable-next-line @typescript-eslint/promise-function-async
new(projectId: string) {
return this.state.dispatch(this.stacksApi.endpoints.new.initiate({ projectId }));
}
}
8 changes: 4 additions & 4 deletions apps/desktop/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import { platformName } from '$lib/platform/platform';
import { ProjectsService } from '$lib/project/projectsService';
import { PromptService } from '$lib/prompt/promptService';
import { DesktopDispatch, DesktopState } from '$lib/redux/store.svelte';
import { DesktopRedux } from '$lib/redux/store.svelte';
import { RemotesService } from '$lib/remotes/remotesService';
import { DesktopRoutesService } from '$lib/routes/routes.svelte';
import { setSecretsService } from '$lib/secrets/secretsService';
Expand Down Expand Up @@ -71,7 +71,8 @@
const userSettings = loadUserSettings();
setContext(SETTINGS, userSettings);
const appState = new DesktopState();
const appState = new AppState();
const desktopState = new DesktopRedux(data.tauri);
const feedService = new FeedService(data.cloud, appState.appDispatch);
const organizationService = new OrganizationService(data.cloud, appState.appDispatch);
const cloudUserService = new CloudUserService(data.cloud, appState.appDispatch);
Expand All @@ -85,8 +86,7 @@
setContext(AppState, appState);
setContext(AppDispatch, appState.appDispatch);
setContext(DesktopState, appState);
setContext(DesktopDispatch, appState.appDispatch);
setContext(DesktopRedux, desktopState);
setContext(FeedService, feedService);
setContext(OrganizationService, organizationService);
setContext(CloudUserService, cloudUserService);
Expand Down
73 changes: 0 additions & 73 deletions apps/desktop/src/routes/reduxExample/+page.svelte

This file was deleted.

0 comments on commit dd575b1

Please sign in to comment.