From b9dabd30aade1312ccb3606b32208a4afdee4c5c Mon Sep 17 00:00:00 2001 From: Peter Gassner Date: Thu, 6 Mar 2025 10:12:03 +0100 Subject: [PATCH 1/5] fix: Use cascade for dotenv files --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bf00928..9d38978 100644 --- a/package.json +++ b/package.json @@ -5,28 +5,28 @@ "scripts": { "setup": "scripts/setup", "clean:setup": "scripts/clean-setup", - "build": "dotenv -- vite build", + "build": "dotenv -c -- vite build", "clean:build": "rm -rf dist && rm -rf src-tauri/target", - "dev": "dotenv -- tauri dev", + "dev": "dotenv -c -- tauri dev", "=== TEST ===": "", "test": "yarn lint && yarn test:unit", - "test:unit": "dotenv -- vitest run", + "test:unit": "dotenv -c -- vitest run", "test:rust": "cargo test --manifest-path src-tauri/Cargo.toml", - "watch:test": "dotenv -- vitest watch", + "watch:test": "dotenv -c -- vitest watch", "=== LINT ===": "", "lint": "concurrently \"yarn:lint:*\"", "lint:format": "prettier --check --cache --cache-strategy content \"./src/**/*.{js,jsx,ts,tsx,json,less,html}\" \"!./src/build/**/*\"", "lint:static": "eslint . --cache --cache-strategy content --ext .js,.jsx,.ts,.tsx", - "lint:ts": "dotenv -- tsc --build", + "lint:ts": "dotenv -c -- tsc --build", "lint:rust": "cargo fmt --manifest-path src-tauri/Cargo.toml --all --check", "=== STORYBOOK ===": "", - "storybook": "dotenv -- storybook dev -p 6006", - "storybook:build": "dotenv -- storybook build", + "storybook": "dotenv -c -- storybook dev -p 6006", + "storybook:build": "dotenv -c -- storybook build", "storybook:test": "npx chromatic --project-token=af98efe0a6a9", "=== UTILITIES ===": "", "clean": "concurrently \"yarn:clean:*\"", - "dev:server": "dotenv -- vite", - "dev:preview": "dotenv -- vite preview", + "dev:server": "dotenv -c -- vite", + "dev:preview": "dotenv -c -- vite preview", "=== LIFE CYCLE HOOKS ===": "", "postinstall": "SKIP_INSTALL=true yarn setup" }, From 414a688ca7389a1dc2205076cc2c1513b04ae0c0 Mon Sep 17 00:00:00 2001 From: Peter Gassner Date: Thu, 6 Mar 2025 13:15:05 +0100 Subject: [PATCH 2/5] fix: Actually read and write from store The codec was broken: We didn't encode and thus decoding failed. Also, the errors we threw were caught and vanished into the ether due to our use of option.tryCatch --- packages/prelude/iots.ts | 31 +++++++++ .../tauri/internal/ProjectMetadataStore.ts | 63 +++++-------------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/packages/prelude/iots.ts b/packages/prelude/iots.ts index a801663..268a9aa 100644 --- a/packages/prelude/iots.ts +++ b/packages/prelude/iots.ts @@ -1,4 +1,5 @@ import { $Unexpressable } from '@code-expert/type-utils'; +import { monoid, option } from 'fp-ts'; import * as either from 'fp-ts/Either'; import { Refinement } from 'fp-ts/Refinement'; import { flow } from 'fp-ts/function'; @@ -54,6 +55,36 @@ export const URL = t.brand( 'URL', ); +// ------------------------------------------------------------------------------------------------- + +/** + * Parse a value, returning either an array of error messages or the parsed value. + */ +export const parseEither = ( + decoder: t.Decoder, +): ((i: I) => either.Either, A>) => + flow(decoder.decode, either.mapLeft(formatValidationErrors)); + +/** + * Parse a value, returning an optional value. + */ +export const parseOption = (decoder: t.Decoder): ((i: I) => option.Option) => + flow(decoder.decode, option.fromEither); + +/** + * Parse a value or fail by throwing a TypeError. + */ +export const parseSync = (decoder: t.Decoder): ((i: I) => A) => + flow( + parseEither(decoder), + either.mapLeft(monoid.concatAll(string.semicolonSeparated)), + either.getOrElseW((err) => { + throw new TypeError(err); + }), + ); + +// ------------------------------------------------------------------------------------------------- + const prefixedString = (prefix: string) => new t.Type( 'Prefixed', diff --git a/src/infrastructure/tauri/internal/ProjectMetadataStore.ts b/src/infrastructure/tauri/internal/ProjectMetadataStore.ts index 28352e3..026e8db 100644 --- a/src/infrastructure/tauri/internal/ProjectMetadataStore.ts +++ b/src/infrastructure/tauri/internal/ProjectMetadataStore.ts @@ -1,42 +1,14 @@ import { Store as TauriStore } from 'tauri-plugin-store-api'; -import { - array, - constVoid, - either, - flow, - iots, - option, - pipe, - task, - taskOption, -} from '@code-expert/prelude'; +import { constVoid, flow, iots, option, pipe, task, taskOption } from '@code-expert/prelude'; import { ProjectId } from '@/domain/Project'; import { ProjectMetadata } from '@/domain/ProjectMetadata'; -import { panic } from '@/utils/error'; const store = new TauriStore('project_metadata.json'); -// Decompose the input to ensure that no excess properties are persisted -const storeProject = ({ - projectId, - exerciseName, - taskOrder, - exerciseOrder, - projectName, - taskName, - courseName, - semester, -}: ProjectMetadata) => - store.set(projectId, { - projectId, - exerciseName, - taskOrder, - exerciseOrder, - projectName, - taskName, - courseName, - semester, - }); +const addToStore = (project: ProjectMetadata) => + taskOption.tryCatch(() => store.set(project.projectId, ProjectMetadata.encode(project))); + +const persistStore = taskOption.tryCatch(() => store.save()); export const projectMetadataStore = { find: (projectId: ProjectId): taskOption.TaskOption => @@ -47,29 +19,24 @@ export const projectMetadataStore = { findAll: (): taskOption.TaskOption> => pipe( taskOption.tryCatch(() => store.values()), - taskOption.chainOptionK(flow(iots.array(ProjectMetadata).decode, option.fromEither)), + taskOption.chainOptionK(iots.parseOption(iots.array(ProjectMetadata))), ), write: (metadata: ProjectMetadata): taskOption.TaskOption => - taskOption.tryCatch(() => storeProject(metadata).then(() => store.save())), + pipe( + addToStore(metadata), + taskOption.chainFirstTaskK(() => persistStore), + ), remove: (projectId: ProjectId): task.Task => pipe( - taskOption.tryCatch(() => store.delete(projectId).then(() => store.save())), + taskOption.tryCatch(() => store.delete(projectId)), + taskOption.chainFirst(() => persistStore), task.map(constVoid), ), writeAll: (projects: Array): taskOption.TaskOption => pipe( - taskOption.tryCatch(() => - pipe( - iots.array(ProjectMetadata).decode(projects), - either.getOrElseW((errs) => - panic(`Project metadata is incorrect: ${iots.formatValidationErrors(errs).join('; ')}`), - ), - array.map(storeProject), - (xs) => Promise.allSettled(xs), - ), - ), - taskOption.chainFirstTaskK(() => () => store.save()), - taskOption.chainOptionK(flow(iots.array(ProjectMetadata).decode, option.fromEither)), + projects, + taskOption.traverseArray(addToStore), + taskOption.chainFirst(() => persistStore), taskOption.map(constVoid), ), }; From 0759d555dc80f2d226120b6c543c98cc07bd4ad8 Mon Sep 17 00:00:00 2001 From: Peter Gassner Date: Thu, 6 Mar 2025 13:26:47 +0100 Subject: [PATCH 3/5] fix: Don't start with an empty state If we have projects, we need to list them, otherwise the app falls back into the setup state. --- src/infrastructure/tauri/ProjectRepository.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/infrastructure/tauri/ProjectRepository.ts b/src/infrastructure/tauri/ProjectRepository.ts index 29410c0..0c56cda 100644 --- a/src/infrastructure/tauri/ProjectRepository.ts +++ b/src/infrastructure/tauri/ProjectRepository.ts @@ -68,10 +68,11 @@ export const mkProjectRepositoryTauri = (): task.Task => { () => projectsDb.set(projects); - const readProjects = pipe( + const loadProjects = pipe( projectMetadataStore.findAll(), taskOption.getOrElseW(() => task.of([])), task.chain(projectsFromMetadata), + task.chainIOK(setProjects), ); const getProjectDirPath: (project: LocalProject) => task.Task = flow( @@ -80,8 +81,7 @@ export const mkProjectRepositoryTauri = (): task.Task => { ); return pipe( - readProjects, - task.chainFirstIOK(setProjects), + loadProjects, task.map(() => ({ projects: property.newProperty>(projectsDb.get, projectsDb.subscribe), From 2fb2672e7bfee9a5ce7d1ef0cdec560aee879290 Mon Sep 17 00:00:00 2001 From: Peter Gassner Date: Thu, 6 Mar 2025 14:14:20 +0100 Subject: [PATCH 4/5] fix: Throttle metadata calls to API We currently do not load information about a single project, only about all projects. Therefore we can throttle calls to metadata to ease the load on the server. --- package.json | 2 ++ .../projects/hooks/useProjectEventUpdate.ts | 7 +++++-- yarn.lock | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d38978..c12adf2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@frp-ts/react": "^1.0.0-beta.3", "@noble/hashes": "latest", "@tauri-apps/api": "^1.5.1", + "@types/throttle-debounce": "^5.0.2", "antd": "^5.10.3", "base64url": "^3.0.1", "conditional-type-checks": "^1.0.6", @@ -70,6 +71,7 @@ "react-fontawesome-svg-icon": "^1.1.2", "react-hotkeys-hook": "latest", "tauri-plugin-store-api": "git+https://github.com/tauri-apps/tauri-plugin-store", + "throttle-debounce": "^5.0.2", "tslib": "latest", "vite-plugin-eslint": "^1.8.1", "vitest": "latest" diff --git a/src/ui/pages/projects/hooks/useProjectEventUpdate.ts b/src/ui/pages/projects/hooks/useProjectEventUpdate.ts index 144746c..dada6fc 100644 --- a/src/ui/pages/projects/hooks/useProjectEventUpdate.ts +++ b/src/ui/pages/projects/hooks/useProjectEventUpdate.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { throttle } from 'throttle-debounce'; import { pipe, remoteEither, tagged, task, taskEither } from '@code-expert/prelude'; import { config } from '@/config'; import { ClientId } from '@/domain/ClientId'; @@ -34,6 +35,8 @@ export const useProjectEventUpdate = ( setSseStatus(remoteEither.initial); }; + const onProjectAccess = throttle(1000, onProjectAccessGranted, { noLeading: true }); + let timeout: NodeJS.Timeout | null = null; const registerEventSource = () => { if (sse.current == null) { @@ -45,7 +48,7 @@ export const useProjectEventUpdate = ( if (sse.current == null) { setSseStatus(remoteEither.pending); sse.current = new EventSource(`${config.CX_API_URL}/projectAccess?token=${token}`); - sse.current.addEventListener('projectAccess', onProjectAccessGranted); + sse.current.addEventListener('projectAccess', onProjectAccess); sse.current.addEventListener('error', onError); sse.current.addEventListener('open', onConnect); sse.current.addEventListener('close', onDisconnect); @@ -70,7 +73,7 @@ export const useProjectEventUpdate = ( }; const cleanUp = () => { - sse.current?.removeEventListener('projectAccess', onProjectAccessGranted); + sse.current?.removeEventListener('projectAccess', onProjectAccess); sse.current?.removeEventListener('error', onError); sse.current?.removeEventListener('open', onConnect); sse.current?.removeEventListener('close', onDisconnect); diff --git a/yarn.lock b/yarn.lock index 7006cb9..d36f715 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2200,6 +2200,7 @@ __metadata: "@types/node": 18.18.4 "@types/react": ^18.2.33 "@types/react-dom": ^18.2.14 + "@types/throttle-debounce": ^5.0.2 "@vitejs/plugin-react": latest antd: ^5.10.3 base64url: ^3.0.1 @@ -2235,6 +2236,7 @@ __metadata: react-hotkeys-hook: latest storybook: ^7.5.2 tauri-plugin-store-api: "git+https://github.com/tauri-apps/tauri-plugin-store" + throttle-debounce: ^5.0.2 tslib: latest typescript: latest vite: ^4.5.0 @@ -5517,6 +5519,13 @@ __metadata: languageName: node linkType: hard +"@types/throttle-debounce@npm:^5.0.2": + version: 5.0.2 + resolution: "@types/throttle-debounce@npm:5.0.2" + checksum: cf2bdd03e7c7348a4ee0046b53b163490fd669b83ad467df0e9fd135fd6e1bdc3c589b444e86b509d650db4c2657f20a3410fa3786f6696eea50313d477b7a74 + languageName: node + linkType: hard + "@types/unist@npm:^2.0.0": version: 2.0.6 resolution: "@types/unist@npm:2.0.6" @@ -14384,6 +14393,13 @@ __metadata: languageName: node linkType: hard +"throttle-debounce@npm:^5.0.2": + version: 5.0.2 + resolution: "throttle-debounce@npm:5.0.2" + checksum: 90d026691bfedf692d9a5addd1d5b30460c6a87a9c588ae05779402e3bfd042bad2bf828edb05512f2e9e601566e8663443d929cf963a998207e193fb1d7eff8 + languageName: node + linkType: hard + "through2@npm:^2.0.3": version: 2.0.5 resolution: "through2@npm:2.0.5" From 2288a526cc566dcc6f25474625bec09c73a05a81 Mon Sep 17 00:00:00 2001 From: Peter Gassner Date: Thu, 6 Mar 2025 14:14:42 +0100 Subject: [PATCH 5/5] docs: Inform about need to use older Rust (For the time being) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7aaa585..5044b5d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Without the following projects, Code Expert Sync would not be possible: ## Prerequisites - [Tauri environment](https://tauri.app/v1/guides/getting-started/prerequisites) + - Make sure to install `rustc` 1.74.x: `rustup install 1.74.1 & rustup default 1.74.1` - [Node.js](https://nodejs.org/en) - [Yarn](https://yarnpkg.com/getting-started/install)