Skip to content

Commit 2dc6183

Browse files
authored
Merge pull request #89 from CodeExpertETH/cx-3451-performance
Cx 3451 API metadata performance
2 parents d218138 + 2288a52 commit 2dc6183

File tree

7 files changed

+82
-62
lines changed

7 files changed

+82
-62
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Without the following projects, Code Expert Sync would not be possible:
2525
## Prerequisites
2626

2727
- [Tauri environment](https://tauri.app/v1/guides/getting-started/prerequisites)
28+
- Make sure to install `rustc` 1.74.x: `rustup install 1.74.1 & rustup default 1.74.1`
2829
- [Node.js](https://nodejs.org/en)
2930
- [Yarn](https://yarnpkg.com/getting-started/install)
3031

package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55
"scripts": {
66
"setup": "scripts/setup",
77
"clean:setup": "scripts/clean-setup",
8-
"build": "dotenv -- vite build",
8+
"build": "dotenv -c -- vite build",
99
"clean:build": "rm -rf dist && rm -rf src-tauri/target",
10-
"dev": "dotenv -- tauri dev",
10+
"dev": "dotenv -c -- tauri dev",
1111
"=== TEST ===": "",
1212
"test": "yarn lint && yarn test:unit",
13-
"test:unit": "dotenv -- vitest run",
13+
"test:unit": "dotenv -c -- vitest run",
1414
"test:rust": "cargo test --manifest-path src-tauri/Cargo.toml",
15-
"watch:test": "dotenv -- vitest watch",
15+
"watch:test": "dotenv -c -- vitest watch",
1616
"=== LINT ===": "",
1717
"lint": "concurrently \"yarn:lint:*\"",
1818
"lint:format": "prettier --check --cache --cache-strategy content \"./src/**/*.{js,jsx,ts,tsx,json,less,html}\" \"!./src/build/**/*\"",
1919
"lint:static": "eslint . --cache --cache-strategy content --ext .js,.jsx,.ts,.tsx",
20-
"lint:ts": "dotenv -- tsc --build",
20+
"lint:ts": "dotenv -c -- tsc --build",
2121
"lint:rust": "cargo fmt --manifest-path src-tauri/Cargo.toml --all --check",
2222
"=== STORYBOOK ===": "",
23-
"storybook": "dotenv -- storybook dev -p 6006",
24-
"storybook:build": "dotenv -- storybook build",
23+
"storybook": "dotenv -c -- storybook dev -p 6006",
24+
"storybook:build": "dotenv -c -- storybook build",
2525
"storybook:test": "npx chromatic --project-token=af98efe0a6a9",
2626
"=== UTILITIES ===": "",
2727
"clean": "concurrently \"yarn:clean:*\"",
28-
"dev:server": "dotenv -- vite",
29-
"dev:preview": "dotenv -- vite preview",
28+
"dev:server": "dotenv -c -- vite",
29+
"dev:preview": "dotenv -c -- vite preview",
3030
"=== LIFE CYCLE HOOKS ===": "",
3131
"postinstall": "SKIP_INSTALL=true yarn setup"
3232
},
@@ -48,6 +48,7 @@
4848
"@frp-ts/react": "^1.0.0-beta.3",
4949
"@noble/hashes": "latest",
5050
"@tauri-apps/api": "^1.5.1",
51+
"@types/throttle-debounce": "^5.0.2",
5152
"antd": "^5.10.3",
5253
"base64url": "^3.0.1",
5354
"conditional-type-checks": "^1.0.6",
@@ -70,6 +71,7 @@
7071
"react-fontawesome-svg-icon": "^1.1.2",
7172
"react-hotkeys-hook": "latest",
7273
"tauri-plugin-store-api": "git+https://github.com/tauri-apps/tauri-plugin-store",
74+
"throttle-debounce": "^5.0.2",
7375
"tslib": "latest",
7476
"vite-plugin-eslint": "^1.8.1",
7577
"vitest": "latest"

packages/prelude/iots.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { $Unexpressable } from '@code-expert/type-utils';
2+
import { monoid, option } from 'fp-ts';
23
import * as either from 'fp-ts/Either';
34
import { Refinement } from 'fp-ts/Refinement';
45
import { flow } from 'fp-ts/function';
@@ -54,6 +55,36 @@ export const URL = t.brand(
5455
'URL',
5556
);
5657

58+
// -------------------------------------------------------------------------------------------------
59+
60+
/**
61+
* Parse a value, returning either an array of error messages or the parsed value.
62+
*/
63+
export const parseEither = <I, A>(
64+
decoder: t.Decoder<I, A>,
65+
): ((i: I) => either.Either<Array<string>, A>) =>
66+
flow(decoder.decode, either.mapLeft(formatValidationErrors));
67+
68+
/**
69+
* Parse a value, returning an optional value.
70+
*/
71+
export const parseOption = <I, A>(decoder: t.Decoder<I, A>): ((i: I) => option.Option<A>) =>
72+
flow(decoder.decode, option.fromEither);
73+
74+
/**
75+
* Parse a value or fail by throwing a TypeError.
76+
*/
77+
export const parseSync = <I, A>(decoder: t.Decoder<I, A>): ((i: I) => A) =>
78+
flow(
79+
parseEither(decoder),
80+
either.mapLeft(monoid.concatAll(string.semicolonSeparated)),
81+
either.getOrElseW((err) => {
82+
throw new TypeError(err);
83+
}),
84+
);
85+
86+
// -------------------------------------------------------------------------------------------------
87+
5788
const prefixedString = (prefix: string) =>
5889
new t.Type<string, string, unknown>(
5990
'Prefixed',

src/infrastructure/tauri/ProjectRepository.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ export const mkProjectRepositoryTauri = (): task.Task<ProjectRepository> => {
6868
() =>
6969
projectsDb.set(projects);
7070

71-
const readProjects = pipe(
71+
const loadProjects = pipe(
7272
projectMetadataStore.findAll(),
7373
taskOption.getOrElseW(() => task.of([])),
7474
task.chain(projectsFromMetadata),
75+
task.chainIOK(setProjects),
7576
);
7677

7778
const getProjectDirPath: (project: LocalProject) => task.Task<NativePath> = flow(
@@ -80,8 +81,7 @@ export const mkProjectRepositoryTauri = (): task.Task<ProjectRepository> => {
8081
);
8182

8283
return pipe(
83-
readProjects,
84-
task.chainFirstIOK(setProjects),
84+
loadProjects,
8585
task.map(() => ({
8686
projects: property.newProperty<Array<Project>>(projectsDb.get, projectsDb.subscribe),
8787

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,14 @@
11
import { Store as TauriStore } from 'tauri-plugin-store-api';
2-
import {
3-
array,
4-
constVoid,
5-
either,
6-
flow,
7-
iots,
8-
option,
9-
pipe,
10-
task,
11-
taskOption,
12-
} from '@code-expert/prelude';
2+
import { constVoid, flow, iots, option, pipe, task, taskOption } from '@code-expert/prelude';
133
import { ProjectId } from '@/domain/Project';
144
import { ProjectMetadata } from '@/domain/ProjectMetadata';
15-
import { panic } from '@/utils/error';
165

176
const store = new TauriStore('project_metadata.json');
187

19-
// Decompose the input to ensure that no excess properties are persisted
20-
const storeProject = ({
21-
projectId,
22-
exerciseName,
23-
taskOrder,
24-
exerciseOrder,
25-
projectName,
26-
taskName,
27-
courseName,
28-
semester,
29-
}: ProjectMetadata) =>
30-
store.set(projectId, {
31-
projectId,
32-
exerciseName,
33-
taskOrder,
34-
exerciseOrder,
35-
projectName,
36-
taskName,
37-
courseName,
38-
semester,
39-
});
8+
const addToStore = (project: ProjectMetadata) =>
9+
taskOption.tryCatch(() => store.set(project.projectId, ProjectMetadata.encode(project)));
10+
11+
const persistStore = taskOption.tryCatch(() => store.save());
4012

4113
export const projectMetadataStore = {
4214
find: (projectId: ProjectId): taskOption.TaskOption<ProjectMetadata> =>
@@ -47,29 +19,24 @@ export const projectMetadataStore = {
4719
findAll: (): taskOption.TaskOption<Array<ProjectMetadata>> =>
4820
pipe(
4921
taskOption.tryCatch(() => store.values()),
50-
taskOption.chainOptionK(flow(iots.array(ProjectMetadata).decode, option.fromEither)),
22+
taskOption.chainOptionK(iots.parseOption(iots.array(ProjectMetadata))),
5123
),
5224
write: (metadata: ProjectMetadata): taskOption.TaskOption<void> =>
53-
taskOption.tryCatch(() => storeProject(metadata).then(() => store.save())),
25+
pipe(
26+
addToStore(metadata),
27+
taskOption.chainFirstTaskK(() => persistStore),
28+
),
5429
remove: (projectId: ProjectId): task.Task<void> =>
5530
pipe(
56-
taskOption.tryCatch(() => store.delete(projectId).then(() => store.save())),
31+
taskOption.tryCatch(() => store.delete(projectId)),
32+
taskOption.chainFirst(() => persistStore),
5733
task.map(constVoid),
5834
),
5935
writeAll: (projects: Array<ProjectMetadata>): taskOption.TaskOption<void> =>
6036
pipe(
61-
taskOption.tryCatch(() =>
62-
pipe(
63-
iots.array(ProjectMetadata).decode(projects),
64-
either.getOrElseW((errs) =>
65-
panic(`Project metadata is incorrect: ${iots.formatValidationErrors(errs).join('; ')}`),
66-
),
67-
array.map(storeProject),
68-
(xs) => Promise.allSettled(xs),
69-
),
70-
),
71-
taskOption.chainFirstTaskK(() => () => store.save()),
72-
taskOption.chainOptionK(flow(iots.array(ProjectMetadata).decode, option.fromEither)),
37+
projects,
38+
taskOption.traverseArray(addToStore),
39+
taskOption.chainFirst(() => persistStore),
7340
taskOption.map(constVoid),
7441
),
7542
};

src/ui/pages/projects/hooks/useProjectEventUpdate.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { throttle } from 'throttle-debounce';
23
import { pipe, remoteEither, tagged, task, taskEither } from '@code-expert/prelude';
34
import { config } from '@/config';
45
import { ClientId } from '@/domain/ClientId';
@@ -34,6 +35,8 @@ export const useProjectEventUpdate = (
3435
setSseStatus(remoteEither.initial);
3536
};
3637

38+
const onProjectAccess = throttle(1000, onProjectAccessGranted, { noLeading: true });
39+
3740
let timeout: NodeJS.Timeout | null = null;
3841
const registerEventSource = () => {
3942
if (sse.current == null) {
@@ -45,7 +48,7 @@ export const useProjectEventUpdate = (
4548
if (sse.current == null) {
4649
setSseStatus(remoteEither.pending);
4750
sse.current = new EventSource(`${config.CX_API_URL}/projectAccess?token=${token}`);
48-
sse.current.addEventListener('projectAccess', onProjectAccessGranted);
51+
sse.current.addEventListener('projectAccess', onProjectAccess);
4952
sse.current.addEventListener('error', onError);
5053
sse.current.addEventListener('open', onConnect);
5154
sse.current.addEventListener('close', onDisconnect);
@@ -70,7 +73,7 @@ export const useProjectEventUpdate = (
7073
};
7174

7275
const cleanUp = () => {
73-
sse.current?.removeEventListener('projectAccess', onProjectAccessGranted);
76+
sse.current?.removeEventListener('projectAccess', onProjectAccess);
7477
sse.current?.removeEventListener('error', onError);
7578
sse.current?.removeEventListener('open', onConnect);
7679
sse.current?.removeEventListener('close', onDisconnect);

yarn.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,6 +2200,7 @@ __metadata:
22002200
"@types/node": 18.18.4
22012201
"@types/react": ^18.2.33
22022202
"@types/react-dom": ^18.2.14
2203+
"@types/throttle-debounce": ^5.0.2
22032204
"@vitejs/plugin-react": latest
22042205
antd: ^5.10.3
22052206
base64url: ^3.0.1
@@ -2235,6 +2236,7 @@ __metadata:
22352236
react-hotkeys-hook: latest
22362237
storybook: ^7.5.2
22372238
tauri-plugin-store-api: "git+https://github.com/tauri-apps/tauri-plugin-store"
2239+
throttle-debounce: ^5.0.2
22382240
tslib: latest
22392241
typescript: latest
22402242
vite: ^4.5.0
@@ -5517,6 +5519,13 @@ __metadata:
55175519
languageName: node
55185520
linkType: hard
55195521

5522+
"@types/throttle-debounce@npm:^5.0.2":
5523+
version: 5.0.2
5524+
resolution: "@types/throttle-debounce@npm:5.0.2"
5525+
checksum: cf2bdd03e7c7348a4ee0046b53b163490fd669b83ad467df0e9fd135fd6e1bdc3c589b444e86b509d650db4c2657f20a3410fa3786f6696eea50313d477b7a74
5526+
languageName: node
5527+
linkType: hard
5528+
55205529
"@types/unist@npm:^2.0.0":
55215530
version: 2.0.6
55225531
resolution: "@types/unist@npm:2.0.6"
@@ -14384,6 +14393,13 @@ __metadata:
1438414393
languageName: node
1438514394
linkType: hard
1438614395

14396+
"throttle-debounce@npm:^5.0.2":
14397+
version: 5.0.2
14398+
resolution: "throttle-debounce@npm:5.0.2"
14399+
checksum: 90d026691bfedf692d9a5addd1d5b30460c6a87a9c588ae05779402e3bfd042bad2bf828edb05512f2e9e601566e8663443d929cf963a998207e193fb1d7eff8
14400+
languageName: node
14401+
linkType: hard
14402+
1438714403
"through2@npm:^2.0.3":
1438814404
version: 2.0.5
1438914405
resolution: "through2@npm:2.0.5"

0 commit comments

Comments
 (0)