From 339b43dc256be9922efb854a5e7ef39074214a8c Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 15 Jul 2025 10:17:18 +0200 Subject: [PATCH] wip --- gatsby-config.ts | 9 ++++++- .../contracts/index.ts | 6 ++++- src/api-4markdown/cache.ts | 13 ++++++++-- src/api-4markdown/guards.ts | 11 -------- src/api-4markdown/index.ts | 3 +-- src/api-4markdown/use-api.ts | 17 +++++++++++-- src/core/models.ts | 9 +++++++ src/core/use-auth.ts | 25 ++++++++++++++++--- .../utils/get-profile-id.ts | 9 ++++++- 9 files changed, 79 insertions(+), 23 deletions(-) delete mode 100644 src/api-4markdown/guards.ts create mode 100644 src/core/models.ts diff --git a/gatsby-config.ts b/gatsby-config.ts index 2740840f0..97f2d5df3 100644 --- a/gatsby-config.ts +++ b/gatsby-config.ts @@ -1,13 +1,20 @@ import type { GatsbyConfig } from "gatsby"; import { meta } from "./meta"; import { seoPlugins } from "./seo-plugins"; +import { CacheVersion } from "api-4markdown-contracts"; +import { SiteMetadata } from "core/models"; require(`dotenv`).config({ path: `.env.${process.env.NODE_ENV}`, }); +const siteMetadata: SiteMetadata = { + ...meta, + buildStamp: new Date().toISOString() as CacheVersion, +}; + const config: GatsbyConfig = { - siteMetadata: meta, + siteMetadata, // More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense. // If you use VSCode you can also use the GraphQL plugin // Learn more at: https://gatsby.dev/graphql-typegen diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index cf10a4011..945371742 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -1,4 +1,4 @@ -import { type Prettify } from "development-kit/utility-types"; +import { Brand, type Prettify } from "development-kit/utility-types"; import type { Base64, Date, Id, Url, UserProfileId } from "../atoms"; import type { DocumentDto, @@ -288,8 +288,11 @@ type API4MarkdownResult = dto: API4MarkdownDto; }; +type CacheVersion = Brand; + type API4MarkdownCacheSignature = { __expiry__: number; + __version__: CacheVersion; value: API4MarkdownDto | null; }; @@ -368,4 +371,5 @@ export type { KnownError, NoInternetError, ClientError, + CacheVersion, }; diff --git a/src/api-4markdown/cache.ts b/src/api-4markdown/cache.ts index 6d1ed0822..94137439f 100644 --- a/src/api-4markdown/cache.ts +++ b/src/api-4markdown/cache.ts @@ -3,6 +3,7 @@ import type { API4MarkdownContractKey, API4MarkdownDto, } from "api-4markdown-contracts"; +import { getCacheVersion } from "./use-api"; const hasValidSignature = ( parsed: unknown, @@ -10,7 +11,9 @@ const hasValidSignature = ( return ( parsed !== null && typeof parsed === `object` && - typeof (parsed as API4MarkdownCacheSignature).__expiry__ === `number` + typeof (parsed as API4MarkdownCacheSignature).__expiry__ === + `number` && + typeof (parsed as API4MarkdownCacheSignature).__version__ === `string` ); }; @@ -20,11 +23,13 @@ const setCache = ( ttlInMinutes = 960, ): void => { try { + const version = getCacheVersion(); localStorage.setItem( key, JSON.stringify({ value: dto, __expiry__: new Date().getTime() + ttlInMinutes * 60 * 1000, + __version__: version, }), ); } catch {} @@ -40,6 +45,7 @@ const getCache = ( key: TKey, ): API4MarkdownDto | null => { try { + const version = getCacheVersion(); const raw = localStorage.getItem(key); if (!raw) return null; @@ -51,7 +57,10 @@ const getCache = ( return null; } - if (parsed.__expiry__ < new Date().getTime()) { + if ( + parsed.__expiry__ < new Date().getTime() || + parsed.__version__ !== version + ) { removeCache(key); return null; } diff --git a/src/api-4markdown/guards.ts b/src/api-4markdown/guards.ts deleted file mode 100644 index 30035f7a5..000000000 --- a/src/api-4markdown/guards.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { UserProfileId } from "api-4markdown-contracts"; - -const asUserProfileId = (value: unknown): UserProfileId => { - if (typeof value !== `string` || value.length === 0) { - throw new Error(`User profile ID must be a non-empty string`); - } - - return value as UserProfileId; -}; - -export { asUserProfileId }; diff --git a/src/api-4markdown/index.ts b/src/api-4markdown/index.ts index ed6d83968..efcecbb69 100644 --- a/src/api-4markdown/index.ts +++ b/src/api-4markdown/index.ts @@ -1,5 +1,4 @@ export { parseError } from "./parse-error"; export { observe, emit, unobserveAll } from "./observer"; -export { initializeAPI, getAPI } from "./use-api"; +export { initializeAPI, getAPI, type API4Markdown } from "./use-api"; export { getCache, removeCache, setCache } from "./cache"; -export * from "./guards"; diff --git a/src/api-4markdown/use-api.ts b/src/api-4markdown/use-api.ts index 26f10ed4b..6bf24682f 100644 --- a/src/api-4markdown/use-api.ts +++ b/src/api-4markdown/use-api.ts @@ -2,6 +2,7 @@ import type { API4MarkdownContractCall, API4MarkdownContractKey, API4MarkdownDto, + CacheVersion, NoInternetError, } from "api-4markdown-contracts"; import { type FirebaseOptions, initializeApp } from "firebase/app"; @@ -38,13 +39,16 @@ type API4Markdown = { let instance: API4Markdown | null = null; let functions: Functions | null = null; +let cacheVersion: CacheVersion | null = null; const isOffline = (): boolean => typeof window !== `undefined` && !navigator?.onLine; class NoInternetException extends Error {} -const initializeAPI = (): API4Markdown => { +const initializeAPI = (version: CacheVersion): API4Markdown => { + cacheVersion = version; + const config: FirebaseOptions = { apiKey: process.env.GATSBY_API_KEY, authDomain: process.env.GATSBY_AUTH_DOMAIN, @@ -138,4 +142,13 @@ const getAPI = (): API4Markdown => { return instance; }; -export { initializeAPI, getAPI }; +const getCacheVersion = (): CacheVersion => { + if (!cacheVersion) { + throw Error(`Cache version is not initialized`); + } + + return cacheVersion; +}; + +export type { API4Markdown }; +export { initializeAPI, getAPI, getCacheVersion }; diff --git a/src/core/models.ts b/src/core/models.ts new file mode 100644 index 000000000..c0dbc04b7 --- /dev/null +++ b/src/core/models.ts @@ -0,0 +1,9 @@ +import { type CacheVersion } from "api-4markdown-contracts"; +import { type meta } from "../../meta"; +import { type Prettify } from "development-kit/utility-types"; + +export type SiteMetadata = Prettify< + typeof meta & { + buildStamp: CacheVersion; + } +>; diff --git a/src/core/use-auth.ts b/src/core/use-auth.ts index 82de2a50d..905d10446 100644 --- a/src/core/use-auth.ts +++ b/src/core/use-auth.ts @@ -3,13 +3,33 @@ import { authStoreActions } from "store/auth/auth.store"; import { docManagementStoreActions } from "store/doc-management/doc-management.store"; import { docStoreActions } from "store/doc/doc.store"; import { docsStoreActions } from "store/docs/docs.store"; -import { initializeAPI } from "api-4markdown"; import { useYourUserProfileState } from "store/your-user-profile"; import { useMindmapCreatorState } from "store/mindmap-creator"; import { useYourAccountState } from "store/your-account"; +import { initializeAPI } from "api-4markdown"; +import { graphql, useStaticQuery } from "gatsby"; +import { SiteMetadata } from "./models"; + +type SiteMetadataQuery = { + site: { + siteMetadata: Pick; + }; +}; const useAuth = () => { - const [api] = React.useState(initializeAPI); + const data = useStaticQuery(graphql` + query BuildStampQuery { + site { + siteMetadata { + buildStamp + } + } + } + `); + + const [api] = React.useState(() => + initializeAPI(data.site.siteMetadata.buildStamp), + ); React.useEffect(() => { const unsubscribe = api.onAuthChange((user) => { @@ -35,7 +55,6 @@ const useAuth = () => { return () => { unsubscribe(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; diff --git a/src/features/user-profile-preview/utils/get-profile-id.ts b/src/features/user-profile-preview/utils/get-profile-id.ts index 4d91ada18..9eaa9dead 100644 --- a/src/features/user-profile-preview/utils/get-profile-id.ts +++ b/src/features/user-profile-preview/utils/get-profile-id.ts @@ -1,6 +1,13 @@ -import { asUserProfileId } from "api-4markdown"; import { UserProfileId } from "api-4markdown-contracts"; +const asUserProfileId = (value: unknown): UserProfileId => { + if (typeof value !== `string` || value.length === 0) { + throw new Error(`User profile ID must be a non-empty string`); + } + + return value as UserProfileId; +}; + const getProfileId = (): UserProfileId => { const params = new URLSearchParams(window.location.search); return asUserProfileId(params.get(`profileId`));