Skip to content

Commit

Permalink
Use localised selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb-T-Owens committed Nov 21, 2024
1 parent 766aeeb commit 324760a
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 82 deletions.
11 changes: 7 additions & 4 deletions apps/desktop/src/lib/posts/Post.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import RegisterInterest from '@gitbutler/shared/redux/interest/RegisterInterest.svelte';
import { FeedService } from '@gitbutler/shared/redux/posts/service';
import { postSelectors, postRepliesSelectors } from '@gitbutler/shared/redux/posts/slice';
import { useStore } from '@gitbutler/shared/redux/utils';
import { AppState } from '@gitbutler/shared/redux/store';
type Props = {
postId: string;
Expand All @@ -13,10 +13,13 @@
const feedService = getContext(FeedService);
const store = useStore();
const appState = getContext(AppState);
const postState = appState.post;
const postRepliesState = appState.postReplies;
const postWithRepliesInterest = $derived(feedService.getPostWithRepliesInterest(postId));
const post = $derived(postSelectors.selectById($store, postId));
const postReplies = $derived(postRepliesSelectors.selectById($store, postId));
const post = $derived(postSelectors.selectById($postState, postId));
const postReplies = $derived(postRepliesSelectors.selectById($postRepliesState, postId));
let postCardRef = $state<HTMLDivElement | undefined>(undefined);
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import * as events from '$lib/utils/events';
import { unsubscribe } from '$lib/utils/unsubscribe';
import { HttpClient } from '@gitbutler/shared/httpClient';
import { AppDispatch, AppState } from '@gitbutler/shared/redux/store';
import {
DesktopRoutesService,
setRoutesService,
Expand All @@ -58,6 +59,8 @@
// Setters do not need to be reactive since `data` never updates
setSecretsService(data.secretsService);
setContext(AppState, data.appState);
setContext(AppDispatch, data.appState.appDispatch);
setContext(CommandService, data.commandService);
setContext(UserService, data.userService);
setContext(ProjectsService, data.projectsService);
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/routes/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RustSecretService } from '$lib/secrets/secretsService';
import { TokenMemoryService } from '$lib/stores/tokenMemoryService';
import { UserService } from '$lib/stores/user';
import { HttpClient } from '@gitbutler/shared/httpClient';
import { AppState } from '@gitbutler/shared/redux/store';
import { LineManagerFactory } from '@gitbutler/ui/commitLines/lineManager';
import { LineManagerFactory as StackingLineManagerFactory } from '@gitbutler/ui/commitLines/lineManager';
import lscache from 'lscache';
Expand All @@ -29,6 +30,8 @@ export const csr = true;

// eslint-disable-next-line
export const load: LayoutLoad = async () => {
const appState = new AppState();

// Awaited and will block initial render, but it is necessary in order to respect the user
// settings on telemetry.
const appSettings = await loadAppSettings();
Expand Down Expand Up @@ -74,6 +77,7 @@ export const load: LayoutLoad = async () => {
aiPromptService,
lineManagerFactory,
stackingLineManagerFactory,
secretsService
secretsService,
appState
};
};
7 changes: 3 additions & 4 deletions apps/desktop/src/routes/[projectId]/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { UpstreamIntegrationService } from '$lib/vbranches/upstreamIntegrationSe
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { BranchesApiService, CloudBranchesService } from '@gitbutler/shared/cloud/stacks/service';
import { FeedService } from '@gitbutler/shared/redux/posts/service';
import { useDispatch } from '@gitbutler/shared/redux/utils';
import { error } from '@sveltejs/kit';
import { derived } from 'svelte/store';
import type { LayoutLoad } from './$types';
Expand All @@ -29,7 +28,8 @@ export const prerender = false;

// eslint-disable-next-line
export const load: LayoutLoad = async ({ params, parent }) => {
const { authService, projectsService, cloud, commandService, userService } = await parent();
const { authService, projectsService, cloud, commandService, userService, appState } =
await parent();

const projectId = params.projectId;
projectsService.setLastOpenedProject(projectId);
Expand Down Expand Up @@ -98,8 +98,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
cloudBranchesService
);

const dispatch = useDispatch();
const feedService = new FeedService(cloud, dispatch);
const feedService = new FeedService(cloud, appState.appDispatch);

return {
authService,
Expand Down
12 changes: 8 additions & 4 deletions apps/desktop/src/routes/[projectId]/feed/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
import RegisterInterest from '@gitbutler/shared/redux/interest/RegisterInterest.svelte';
import { FeedService } from '@gitbutler/shared/redux/posts/service';
import { feedSelectors, postSelectors } from '@gitbutler/shared/redux/posts/slice';
import { useStore } from '@gitbutler/shared/redux/utils';
import { AppState } from '@gitbutler/shared/redux/store';
import Button from '@gitbutler/ui/Button.svelte';
const store = useStore();
const appState = getContext(AppState);
const feedService = getContext(FeedService);
// Fetching the head of the feed
const feedHeadInterest = feedService.getFeedHeadInterest();
// List posts associated with the feed
const feed = $derived(feedSelectors.selectById($store, 'all'));
const feedState = appState.feed;
const feed = $derived(feedSelectors.selectById($feedState, 'all'));
$effect(() => console.log(feed));
Expand All @@ -26,11 +27,14 @@
}
// Infinite scrolling
const postState = appState.post;
const lastPostId = $derived(feed?.postIds.at(-1));
const lastPostInterest = $derived(
lastPostId ? feedService.getPostWithRepliesInterest(lastPostId) : undefined
);
const lastPost = $derived(lastPostId ? postSelectors.selectById($store, lastPostId) : undefined);
const lastPost = $derived(
lastPostId ? postSelectors.selectById($postState, lastPostId) : undefined
);
let lastElement = $state<HTMLElement | undefined>();
$effect(() => {
Expand Down
16 changes: 9 additions & 7 deletions apps/desktop/src/routes/reduxExample/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import { getContext } from '@gitbutler/shared/context';
import {
decrement,
increment,
selectExampleValue,
selectExampleValueGreaterThan
} from '@gitbutler/shared/redux/example';
import { useDispatch, useStore } from '@gitbutler/shared/redux/utils';
import { AppDispatch, AppState } from '@gitbutler/shared/redux/store';
import Button from '@gitbutler/ui/Button.svelte';
import { goto } from '$app/navigation';
Expand All @@ -14,14 +15,15 @@
* `location = '/reduxExample'` in the console.
*/
const store = useStore();
const dispatch = useDispatch();
const appState = getContext(AppState);
const appDispatch = getContext(AppDispatch);
let comparisonTarget = $state(4);
const currentValue = $derived(selectExampleValue($store));
const exampleState = appState.example;
const currentValue = $derived(selectExampleValue($exampleState));
const greaterThanComparisonTarget = $derived(
selectExampleValueGreaterThan($store, comparisonTarget)
selectExampleValueGreaterThan($exampleState, comparisonTarget)
);
</script>

Expand All @@ -32,8 +34,8 @@
<p>Current value: {currentValue}</p>

<div>
<Button onclick={() => dispatch(increment())} type="button">increase</Button>
<Button onclick={() => dispatch(decrement())} type="button">decrease</Button>
<Button onclick={() => appDispatch.dispatch(increment())} type="button">increase</Button>
<Button onclick={() => appDispatch.dispatch(decrement())} type="button">decrease</Button>
</div>
<hr />
<p>
Expand Down
5 changes: 3 additions & 2 deletions packages/shared/src/lib/redux/example.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { selectSelf } from '$lib/redux/selectSelf';
import { createSelector, createSlice } from '@reduxjs/toolkit';

export interface ExampleState {
Expand All @@ -25,7 +24,9 @@ const exampleSlice = createSlice({
export const { increment, decrement } = exampleSlice.actions;
export const exampleReducer = exampleSlice.reducer;

export const selectExample = createSelector([selectSelf], (state) => state.example);
export function selectExample(example: ExampleState): ExampleState {
return example;
}
export const selectExampleValue = createSelector([selectExample], (example) => example.value);
export const selectExampleValueGreaterThan = createSelector(
[selectExampleValue, (_state: unknown, target: number) => target],
Expand Down
15 changes: 7 additions & 8 deletions packages/shared/src/lib/redux/posts/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class FeedService {

constructor(
private readonly httpClient: HttpClient,
private readonly dispatch: AppDispatch
private readonly appDispatch: AppDispatch
) {}

/** Fetch and poll the latest entries in the feed */
Expand All @@ -34,20 +34,20 @@ export class FeedService {
async getFeedPage(_identifier: string, lastPostTimestamp?: string) {
const query = lastPostTimestamp ? `?from_created_at=${lastPostTimestamp}` : '';
const apiFeed = await this.httpClient.get<ApiPost[]>(`feed${query}`);
this.dispatch(upsertPosts(apiFeed.map(apiToPost)));
this.appDispatch.dispatch(upsertPosts(apiFeed.map(apiToPost)));

const actionArguments = { identifier: 'all', postIds: apiFeed.map((post) => post.uuid) };
if (lastPostTimestamp) {
this.dispatch(feedAppend(actionArguments));
this.appDispatch.dispatch(feedAppend(actionArguments));
} else {
this.dispatch(feedPrepend(actionArguments));
this.appDispatch.dispatch(feedPrepend(actionArguments));
}
}

async createPost(content: string): Promise<Post> {
const apiPost = await this.httpClient.post<ApiPost>('feed/new', { body: { content } });
const post = apiToPost(apiPost);
this.dispatch(upsertPost(post));
this.appDispatch.dispatch(upsertPost(post));

// TODO: Determine if this is needed / wanted / useful
this.getFeedPage('all');
Expand All @@ -57,14 +57,13 @@ export class FeedService {

getPostWithRepliesInterest(postId: string) {
return this.postWithRepliesInterests.createInterest({ postId }, async () => {
return;
const apiPostWithReplies = await this.httpClient.get<ApiPostWithReplies>(
`feed/post/${postId}`
);
const post = apiToPost(apiPostWithReplies);
const posts = [post, ...apiPostWithReplies.replies.map(apiToPost)];
this.dispatch(upsertPosts(posts));
this.dispatch(
this.appDispatch.dispatch(upsertPosts(posts));
this.appDispatch.dispatch(
upsertPostReplies({
postId,
replyIds: apiPostWithReplies.replies.map((reply) => reply.uuid)
Expand Down
9 changes: 3 additions & 6 deletions packages/shared/src/lib/redux/posts/slice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createEntityAdapter, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { Feed, Post, PostReplies } from '$lib/redux/posts/types';
import type { RootState } from '$lib/redux/store';

const postAdapter = createEntityAdapter({
selectId: (post: Post) => post.uuid,
Expand All @@ -24,7 +23,7 @@ const postSlice = createSlice({

export const postReducer = postSlice.reducer;

export const postSelectors = postAdapter.getSelectors((state: RootState) => state.post);
export const postSelectors = postAdapter.getSelectors();
export const {
addPost,
addPosts,
Expand Down Expand Up @@ -77,7 +76,7 @@ const feedSlice = createSlice({

export const feedReducer = feedSlice.reducer;

export const feedSelectors = feedAdapter.getSelectors((state: RootState) => state.feed);
export const feedSelectors = feedAdapter.getSelectors();
export const { addFeed, updateFeed, removeFeed, upsertFeed, feedAppend, feedPrepend } =
feedSlice.actions;

Expand Down Expand Up @@ -106,8 +105,6 @@ const postRepliesSlice = createSlice({

export const postRepliesReducer = postRepliesSlice.reducer;

export const postRepliesSelectors = postRepliesAdapter.getSelectors(
(state: RootState) => state.postReplies
);
export const postRepliesSelectors = postRepliesAdapter.getSelectors();
export const { addPostReplies, updatePostReplies, removePostReplies, upsertPostReplies } =
postRepliesSlice.actions;
5 changes: 0 additions & 5 deletions packages/shared/src/lib/redux/selectSelf.ts

This file was deleted.

96 changes: 79 additions & 17 deletions packages/shared/src/lib/redux/store.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,83 @@
import { exampleReducer } from '$lib/redux/example';
import { feedReducer, postReducer, postRepliesReducer } from '$lib/redux/posts/slice';
import { configureStore } from '@reduxjs/toolkit';

/**
* The base store.
*
* This is a low level API and should not be used directly.
* @private
*/
export const _store = configureStore({
reducer: {
example: exampleReducer,
post: postReducer,
postReplies: postRepliesReducer,
feed: feedReducer
import { configureStore, createSelector } from '@reduxjs/toolkit';
import { derived, readable, type Readable } from 'svelte/store';

// Individual interfaces to be used when consuming in other servies.
// By specifying only the interfaces you need, IE:
// `appState: AppPostState & AppExampleState`, it means there is less mocking
// needed when testing.
export interface AppExampleState {
readonly example: Readable<ReturnType<typeof exampleReducer>>;
}

export interface AppPostState {
readonly post: Readable<ReturnType<typeof postReducer>>;
}

export interface AppPostRepliesState {
readonly postReplies: Readable<ReturnType<typeof postRepliesReducer>>;
}

export interface AppFeedState {
readonly feed: Readable<ReturnType<typeof feedReducer>>;
}

export class AppDispatch {
constructor(readonly dispatch: Dispatch) {}
}

export class AppState implements AppExampleState, AppPostState, AppPostRepliesState, AppFeedState {
/**
* The base store.
*
* This is a low level API and should not be used directly.
* @private
*/
readonly _store = configureStore({
reducer: {
example: exampleReducer,
post: postReducer,
postReplies: postRepliesReducer,
feed: feedReducer
}
});

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

/**
* Used to access the store directly. It is recommended to access state via
* selectors as they are more efficient.
*/
readonly rootState: Readable<RootState> = readable(this._store.getState(), (set) => {
const unsubscribe = this._store.subscribe(() => {
set(this._store.getState());
});
return unsubscribe;
});

private selectSelf(state: RootState) {
return state;
}
});

export type RootState = ReturnType<typeof _store.getState>;
export type AppDispatch = typeof _store.dispatch;
private readonly selectExample = createSelector(
[this.selectSelf],
(rootState) => rootState.example
);
readonly example = derived(this.rootState, this.selectExample);

private readonly selectPost = createSelector([this.selectSelf], (rootState) => rootState.post);
readonly post = derived(this.rootState, this.selectPost);

private readonly selectPostReplies = createSelector(
[this.selectSelf],
(rootState) => rootState.postReplies
);
readonly postReplies = derived(this.rootState, this.selectPostReplies);

private readonly selectFeed = createSelector([this.selectSelf], (rootState) => rootState.feed);
readonly feed = derived(this.rootState, this.selectFeed);
}

export type RootState = ReturnType<typeof AppState.prototype._store.getState>;
export type Dispatch = typeof AppState.prototype._store.dispatch;
Loading

0 comments on commit 324760a

Please sign in to comment.