@pmate/account-sdk 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +35 -28
  2. package/src/api/AccountService.ts +97 -0
  3. package/src/api/Api.ts +99 -0
  4. package/src/api/AppService.ts +71 -0
  5. package/src/api/EntityService.ts +41 -0
  6. package/src/api/ProfileService.ts +133 -0
  7. package/src/api/cacheInMem.ts +73 -0
  8. package/src/api/index.ts +5 -0
  9. package/src/atoms/accountAtom.ts +18 -0
  10. package/src/atoms/accountProfileAtom.ts +13 -0
  11. package/src/atoms/appConfigAtom.ts +24 -0
  12. package/src/atoms/atomWithLoadable.ts +48 -0
  13. package/src/atoms/createProfileAtom.ts +27 -0
  14. package/src/atoms/index.ts +17 -0
  15. package/src/atoms/learningLangAtom.ts +8 -0
  16. package/src/atoms/localStorageAtom.ts +39 -0
  17. package/src/atoms/loginAtom.ts +8 -0
  18. package/src/atoms/motherTongueAtom.ts +8 -0
  19. package/src/atoms/profileAtom.ts +24 -0
  20. package/src/atoms/profileDraftAtom.ts +7 -0
  21. package/src/atoms/profilesAtom.ts +20 -0
  22. package/src/atoms/sessionCheckAtom.ts +10 -0
  23. package/src/atoms/switchProfileAtom.ts +9 -0
  24. package/src/atoms/updateProfileAtom.ts +35 -0
  25. package/src/atoms/uploadAvatarAtom.ts +49 -0
  26. package/src/atoms/userLogoutAtom.ts +8 -0
  27. package/src/atoms/userSettingsAtom.ts +58 -0
  28. package/src/components/AuthProviderV2.tsx +300 -0
  29. package/src/components/Button.tsx +39 -0
  30. package/src/components/Drawer.tsx +80 -0
  31. package/src/components/index.ts +1 -0
  32. package/src/constants.ts +1 -0
  33. package/src/hooks/index.ts +5 -0
  34. package/src/hooks/useAppBackgroundStyle.ts +25 -0
  35. package/src/hooks/useAppConfig.ts +51 -0
  36. package/src/hooks/useAuthApp.ts +165 -0
  37. package/src/hooks/useAuthSnapshot.ts +84 -0
  38. package/src/hooks/useIsAuthenticated.ts +19 -0
  39. package/src/hooks/useProfileStepFlow.ts +78 -0
  40. package/src/i18n/index.ts +59 -0
  41. package/src/index.ts +9 -0
  42. package/src/locales/ar-SA.json +183 -0
  43. package/src/locales/de-DE.json +183 -0
  44. package/src/locales/el-GR.json +183 -0
  45. package/src/locales/en.json +183 -0
  46. package/src/locales/es-ES.json +183 -0
  47. package/src/locales/fi-FI.json +183 -0
  48. package/src/locales/fil-PH.json +183 -0
  49. package/src/locales/fr-FR.json +183 -0
  50. package/src/locales/hi-IN.json +183 -0
  51. package/src/locales/ja-JP.json +183 -0
  52. package/src/locales/ko-KR.json +183 -0
  53. package/src/locales/pt-BR.json +183 -0
  54. package/src/locales/pt-PT.json +183 -0
  55. package/src/locales/ru-RU.json +183 -0
  56. package/src/locales/ta-IN.json +183 -0
  57. package/src/locales/uk-UA.json +183 -0
  58. package/src/locales/zh-CN.json +183 -0
  59. package/src/locales/zh-TW.json +183 -0
  60. package/src/node/index.ts +271 -0
  61. package/src/types/account.types.ts +28 -0
  62. package/src/types/app.ts +22 -0
  63. package/src/types/profile.ts +6 -0
  64. package/src/utils/AccountManagerV2.ts +352 -0
  65. package/src/utils/Redirect.ts +17 -0
  66. package/src/utils/accountStorage.ts +46 -0
  67. package/src/utils/errors.ts +1 -0
  68. package/src/utils/index.ts +11 -0
  69. package/src/utils/location.ts +34 -0
  70. package/src/utils/profileStep.ts +26 -0
  71. package/src/utils/resolveAppId.ts +13 -0
  72. package/src/utils/selectedProfileStorage.ts +46 -0
  73. package/src/utils/tokenStorage.ts +47 -0
  74. package/dist/index.cjs.js +0 -22
  75. package/dist/index.cjs.js.map +0 -1
  76. package/dist/index.d.ts +0 -305
  77. package/dist/index.es.js +0 -8897
  78. package/dist/index.es.js.map +0 -1
package/package.json CHANGED
@@ -1,52 +1,59 @@
1
1
  {
2
2
  "name": "@pmate/account-sdk",
3
- "version": "0.5.5",
4
- "main": "./dist/index.cjs.js",
5
- "module": "./dist/index.es.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.es.js",
11
- "require": "./dist/index.cjs.js"
12
- }
13
- },
3
+ "version": "0.6.1",
4
+ "type": "module",
14
5
  "files": [
15
- "dist"
6
+ "src",
7
+ "!src/tests",
8
+ "!src/tests/**"
16
9
  ],
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./node": "./src/node/index.ts"
13
+ },
17
14
  "publishConfig": {
18
15
  "access": "public"
19
16
  },
17
+ "scripts": {
18
+ "npm:publish": "npm publish --registry https://registry.npmjs.org/",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest"
21
+ },
20
22
  "peerDependencies": {
21
23
  "jotai": "*",
22
24
  "jotai-family": "*",
23
25
  "react": "*"
24
26
  },
27
+ "peerDependenciesMeta": {
28
+ "jotai": {
29
+ "optional": true
30
+ },
31
+ "jotai-family": {
32
+ "optional": true
33
+ },
34
+ "react": {
35
+ "optional": true
36
+ }
37
+ },
25
38
  "dependencies": {
39
+ "@pmate/lang": "^1.0.2",
40
+ "@pmate/meta": "^1.1.3",
41
+ "@pmate/service-core": "^1.0.0",
42
+ "@pmate/utils": "^1.0.3",
26
43
  "browser-image-compression": "^2.0.2",
44
+ "elysia": "^1.4.19",
27
45
  "i18next": "^23.0.0",
28
46
  "react-i18next": "^13.0.0",
29
47
  "react-use": "^17.6.0"
30
48
  },
31
49
  "devDependencies": {
32
- "@types/react": "*",
33
- "@types/react-dom": "*",
50
+ "@types/react": "catalog:",
51
+ "@types/react-dom": "catalog:",
34
52
  "@vitejs/plugin-react": "^4.2.1",
35
53
  "jsdom": "^26.1.0",
36
54
  "react-dom": "*",
37
- "rollup": "^4.21.2",
38
- "rollup-plugin-dts": "^6.1.1",
39
55
  "typescript": "^5.2.2",
40
- "vite": "^7.3.1",
41
- "vitest": "^4.0.17",
42
- "@pmate/utils": "1.0.0",
43
- "@pmate/lang": "1.0.0",
44
- "@pmate/meta": "1.1.1"
45
- },
46
- "scripts": {
47
- "build": "vite build && rollup -c rollup.config.dts.ts",
48
- "npm:publish": "pnpm build && npm publish --registry https://registry.npmjs.org/",
49
- "test": "vitest run",
50
- "test:watch": "vitest"
56
+ "vite": "catalog:",
57
+ "vitest": "^4.0.17"
51
58
  }
52
- }
59
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ AuthLoginResponse,
3
+ AuthRequest,
4
+ AuthSession,
5
+ ServerResponse,
6
+ VCodeIssueRequest,
7
+ VCodeIssueResult,
8
+ } from "@pmate/meta"
9
+ import { lru } from "@pmate/utils"
10
+ import { getAuthToken } from "../utils/tokenStorage"
11
+
12
+ const AUTH_ENDPOINT = "https://auth-api-v2.pmate.chat"
13
+ type AuthRequestInit = RequestInit & { token?: string; app?: string }
14
+
15
+ export class AccountService {
16
+ private static sessionCached = lru(
17
+ (token?: string, app?: string) =>
18
+ AccountService.authRequest<AuthSession | null>("/session", {
19
+ method: "GET",
20
+ token,
21
+ app,
22
+ }),
23
+ {
24
+ ttl: 3_000,
25
+ key: (token, app) => token ?? getAuthToken(app) ?? `default:${app ?? ""}`,
26
+ }
27
+ )
28
+ public static async vcode(
29
+ payload: VCodeIssueRequest
30
+ ): Promise<VCodeIssueResult> {
31
+ return AccountService.authRequest<VCodeIssueResult>("/vcode", {
32
+ method: "POST",
33
+ body: JSON.stringify(payload),
34
+ })
35
+ }
36
+
37
+ public static async login(request: AuthRequest): Promise<AuthLoginResponse> {
38
+ return AccountService.authRequest<AuthLoginResponse>("/login", {
39
+ method: "POST",
40
+ body: JSON.stringify(request),
41
+ token: request.nonce,
42
+ app: request.app,
43
+ })
44
+ }
45
+
46
+ public static async logout(token?: string, app?: string) {
47
+ return AccountService.authRequest<{ success: boolean }>("/logout", {
48
+ method: "POST",
49
+ token,
50
+ app,
51
+ })
52
+ }
53
+
54
+ public static async logoutAll(token?: string, app?: string) {
55
+ return AccountService.authRequest<{ success: boolean }>("/logout-all", {
56
+ method: "POST",
57
+ token,
58
+ app,
59
+ })
60
+ }
61
+
62
+ public static async session(token?: string, app?: string) {
63
+ return AccountService.sessionCached(token, app)
64
+ }
65
+
66
+ public static clearSessionCache() {
67
+ AccountService.sessionCached.clean()
68
+ }
69
+
70
+ private static async authRequest<T>(path: string, init: AuthRequestInit) {
71
+ const { token, headers, app, ...rest } = init
72
+ const resolvedToken = token ?? getAuthToken(app)
73
+ const url = new URL(`${AUTH_ENDPOINT.replace(/\/+$/, "")}${path}`)
74
+ if (app) {
75
+ url.searchParams.set("app", app)
76
+ }
77
+ const response = await fetch(
78
+ url.toString(),
79
+ {
80
+ credentials: "include",
81
+ ...rest,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ ...(resolvedToken
85
+ ? { Authorization: `Bearer ${resolvedToken}` }
86
+ : {}),
87
+ ...(headers ?? {}),
88
+ },
89
+ }
90
+ )
91
+ const json = (await response.json()) as ServerResponse<T>
92
+ if (!response.ok || !json.success) {
93
+ throw json
94
+ }
95
+ return json.data
96
+ }
97
+ }
package/src/api/Api.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { Maybe, isMaybe } from "@pmate/utils"
2
+ import { ServerResponse } from "@pmate/meta"
3
+ import { cacheInMem } from "./cacheInMem"
4
+
5
+ const checkLink = async (url: string | Maybe<string>): Promise<boolean> => {
6
+ if (isMaybe(url)) {
7
+ if (url.isNothing()) {
8
+ return false
9
+ }
10
+ url = url.unwrap()
11
+ }
12
+ try {
13
+ const response = await fetch(url, { method: "HEAD" })
14
+ return response.ok
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+
20
+ const cachedLinkCheck = cacheInMem(checkLink, "link", 60 * 60 * 1000)
21
+
22
+ export class Api {
23
+ public static async get<T>(url: string): Promise<T | null> {
24
+ const response = await fetch(url)
25
+ if (!response.ok) {
26
+ return null
27
+ }
28
+ const json = (await response.json()) as ServerResponse<T>
29
+ return json.data ?? null
30
+ }
31
+
32
+ public static async post<T>(url: string, body: unknown): Promise<T> {
33
+ const response = await fetch(url, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify(body),
37
+ })
38
+ const json = (await response.json()) as ServerResponse<T>
39
+ if (!json.success) {
40
+ throw json
41
+ }
42
+ return json.data as T
43
+ }
44
+
45
+ public static async put<T>(url: string, body: unknown): Promise<Maybe<T>> {
46
+ const response = await fetch(url, {
47
+ method: "PUT",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify(body),
50
+ })
51
+ const json = (await response.json()) as ServerResponse<T>
52
+ if (!json.success) {
53
+ throw json
54
+ }
55
+ return Maybe.Just<T>(json.data)
56
+ }
57
+
58
+ public static async getFile<T>(url: string): Promise<T | null>
59
+ public static async getFile<T>(url: string, type: "json"): Promise<T | null>
60
+ public static async getFile(url: string, type: "text"): Promise<string | null>
61
+ public static async getFile<T>(url: string, type: "log"): Promise<Maybe<T[]>>
62
+ public static async getFile<T>(
63
+ url: string,
64
+ type: "json" | "text" | "log" = "json"
65
+ ): Promise<T | string | Maybe<T[]> | null> {
66
+ try {
67
+ const response = await fetch(url)
68
+ if (type === "log") {
69
+ if (!response.ok) {
70
+ return Maybe.Nothing()
71
+ }
72
+ try {
73
+ const text = await response.text()
74
+ const logs = text
75
+ .split("\n")
76
+ .filter(Boolean)
77
+ .map((line) => JSON.parse(line) as T)
78
+ return Maybe.Just(logs)
79
+ } catch {
80
+ return Maybe.Nothing()
81
+ }
82
+ }
83
+ if (!response.ok) {
84
+ return null
85
+ }
86
+ if (type === "text") {
87
+ return response.text()
88
+ }
89
+ return (await response.json()) as T
90
+ } catch (error) {
91
+ console.warn(error)
92
+ return null
93
+ }
94
+ }
95
+
96
+ public static exists(url: string | Maybe<string>): Promise<boolean> {
97
+ return cachedLinkCheck(url)
98
+ }
99
+ }
@@ -0,0 +1,71 @@
1
+ import { lru } from "@pmate/utils"
2
+ import type { AppConfig, ProfileStep } from "../types/app"
3
+
4
+ const APPS_ENDPOINT = "https://apps-api.pmate.chat"
5
+ const DEFAULT_BACKGROUND = "linear-gradient(180deg, #9ca3af 0%, #6b7280 100%)"
6
+ const DEFAULT_ICON = "https://parrot-static.pmate.chat/parrot-logo.png"
7
+
8
+ type AppRegistryRecord = {
9
+ id: string
10
+ name: string
11
+ icon: string
12
+ theme?: {
13
+ background?: string
14
+ themeColor?: string
15
+ welcomeText?: string
16
+ }
17
+ profileSchema?: ProfileStep[]
18
+ }
19
+
20
+ const buildDisplayNameFromAppId = (appId: string): string => {
21
+ const normalized = appId.replace(/^@/, "")
22
+ const segments = normalized.split("/").filter(Boolean)
23
+ const last = segments[segments.length - 1] || normalized
24
+ return last || "App"
25
+ }
26
+
27
+ const toAppConfig = (record: AppRegistryRecord): AppConfig => {
28
+ const fallbackName = buildDisplayNameFromAppId(record.id)
29
+ return {
30
+ id: record.id,
31
+ name: record.name || fallbackName,
32
+ icon: record.icon || DEFAULT_ICON,
33
+ background: record.theme?.background || DEFAULT_BACKGROUND,
34
+ themeColor: record.theme?.themeColor,
35
+ welcomeText: record.theme?.welcomeText || `Welcome to ${fallbackName}`,
36
+ profiles: record.profileSchema ?? [],
37
+ }
38
+ }
39
+
40
+ const getAppConfigCached = lru(
41
+ async (appId: string): Promise<AppConfig> => {
42
+ const response = await fetch(
43
+ `${APPS_ENDPOINT.replace(/\/+$/, "")}/apps/${encodeURIComponent(appId)}`
44
+ )
45
+ if (!response.ok) {
46
+ throw new Error(`Failed to load app config (${response.status})`)
47
+ }
48
+ const json = (await response.json()) as {
49
+ success?: boolean
50
+ data?: AppRegistryRecord
51
+ }
52
+ if (!json.success || !json.data) {
53
+ throw new Error("Failed to load app config")
54
+ }
55
+ return toAppConfig(json.data)
56
+ },
57
+ {
58
+ ttl: 60_000,
59
+ key: (appId) => appId,
60
+ }
61
+ )
62
+
63
+ export class AppService {
64
+ public static async getAppConfig(appId: string): Promise<AppConfig> {
65
+ return getAppConfigCached(appId)
66
+ }
67
+
68
+ public static clearAppConfigCache() {
69
+ getAppConfigCached.clean()
70
+ }
71
+ }
@@ -0,0 +1,41 @@
1
+ import { GroupInfo, Profile } from "@pmate/meta"
2
+ import { Api } from "./Api"
3
+
4
+ const endpoint = "https://api-global-qa.skedo.cn/account"
5
+
6
+ export class EntityService {
7
+ public static async entity<T>(id: string): Promise<T | null> {
8
+ return (await Api.get<T>(`${endpoint}/entity/${id}`)) || null
9
+ }
10
+
11
+ public static getProfile(id: string): Promise<Profile | null> {
12
+ return EntityService.entity<Profile>(id)
13
+ }
14
+
15
+ public static getGroup(id: string): Promise<GroupInfo | null> {
16
+ return EntityService.entity<GroupInfo>(id)
17
+ }
18
+
19
+ public static async entities<T extends { id?: string }>(
20
+ ids: string[]
21
+ ): Promise<Record<string, T>> {
22
+ if (!ids.length) {
23
+ return {}
24
+ }
25
+ const url = `${endpoint}/entities`
26
+ const list =
27
+ (await Api.post<T[]>(url, {
28
+ ids,
29
+ })) || []
30
+ return list.reduce<Record<string, T>>((acc, entity, index) => {
31
+ if (!entity) {
32
+ return acc
33
+ }
34
+ const key = entity.id ?? ids[index]
35
+ if (key) {
36
+ acc[key] = entity
37
+ }
38
+ return acc
39
+ }, {})
40
+ }
41
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ AccountState,
3
+ CreateProfileRequest,
4
+ Profile,
5
+ ProfileScope,
6
+ UpdateProfileRequest,
7
+ } from "@pmate/meta"
8
+ import { resolveAppId } from "../utils/resolveAppId"
9
+ import { Api } from "./Api"
10
+
11
+ const PROFILE_ENDPOINT = "https://auth-api-v2.pmate.chat"
12
+ const UPLOADER_SERVICE_URL = "https://fc-uploader.skedo.cn".replace(/\/+$/, "")
13
+
14
+ const EXTENSION_CONTENT_TYPE: Record<string, string> = {
15
+ png: "image/png",
16
+ jpg: "image/jpeg",
17
+ jpeg: "image/jpeg",
18
+ webp: "image/webp",
19
+ gif: "image/gif",
20
+ svg: "image/svg+xml",
21
+ ico: "image/x-icon",
22
+ bmp: "image/bmp",
23
+ pdf: "application/pdf",
24
+ mobi: "application/x-mobipocket-ebook",
25
+ epub: "application/epub+zip",
26
+ txt: "text/plain",
27
+ json: "application/json",
28
+ mp3: "audio/mpeg",
29
+ wav: "audio/wav",
30
+ webm: "audio/webm",
31
+ weba: "audio/webm",
32
+ m4a: "audio/mp4",
33
+ }
34
+
35
+ type UploadRequest = {
36
+ user: string
37
+ base64: string
38
+ filename?: string
39
+ text?: string
40
+ contentType?: string
41
+ }
42
+
43
+ export class ProfileService {
44
+ public static async createProfile(
45
+ req: CreateProfileRequest,
46
+ ): Promise<Profile> {
47
+ return Api.post<Profile>(`${PROFILE_ENDPOINT}/profile`, req)
48
+ }
49
+
50
+ public static async updateProfile(req: UpdateProfileRequest) {
51
+ await Api.put(`${PROFILE_ENDPOINT}/profile`, req)
52
+ }
53
+
54
+ public static async getProfiles(account: AccountState) {
55
+ const scope: ProfileScope = {
56
+ app: resolveAppId(account.app),
57
+ account: account.accountId,
58
+ }
59
+ return ProfileService.getProfilesByScope(scope)
60
+ }
61
+
62
+ public static async getProfilesByScope(scope: ProfileScope) {
63
+ const query = new URLSearchParams({
64
+ app: scope.app,
65
+ account: scope.account,
66
+ }).toString()
67
+ return (
68
+ (await Api.get<Profile[]>(`${PROFILE_ENDPOINT}/profiles?${query}`)) || []
69
+ )
70
+ }
71
+
72
+ public static async updateAvatar(req: {
73
+ user: string
74
+ base64: string
75
+ filename: string
76
+ }): Promise<string> {
77
+ return ProfileService.uploadToUploaderService("/avatar", req)
78
+ }
79
+
80
+ public static async uploadMsgImage(req: {
81
+ user: string
82
+ base64: string
83
+ filename: string
84
+ }): Promise<string> {
85
+ return ProfileService.uploadToUploaderService("/msg", req)
86
+ }
87
+
88
+ public static async updateMyVoice(req: {
89
+ user: string
90
+ base64: string
91
+ text: string
92
+ }): Promise<string> {
93
+ return ProfileService.uploadToUploaderService("/my-voice", {
94
+ ...req,
95
+ filename: "voice.wav",
96
+ contentType: "audio/wav",
97
+ })
98
+ }
99
+
100
+ public static async uploadUserFile(req: {
101
+ user: string
102
+ base64: string
103
+ filename: string
104
+ }) {
105
+ return ProfileService.uploadToUploaderService("/file", req)
106
+ }
107
+
108
+ private static async uploadToUploaderService(
109
+ path: string,
110
+ payload: UploadRequest,
111
+ ): Promise<string> {
112
+ const { filename, ...rest } = payload
113
+ const resolvedFilename = filename ?? "upload"
114
+ const ext = resolvedFilename.split(".").pop()?.toLowerCase() ?? ""
115
+ const contentType = payload.contentType ?? EXTENSION_CONTENT_TYPE[ext]
116
+ const response = await fetch(`${UPLOADER_SERVICE_URL}${path}`, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ ...(contentType ? { "x-file-content-type": contentType } : {}),
121
+ },
122
+ body: JSON.stringify({ ...rest, filename: resolvedFilename }),
123
+ })
124
+ if (!response.ok) {
125
+ throw new Error("Upload failed")
126
+ }
127
+ const json = (await response.json()) as { data?: string }
128
+ if (!json.data) {
129
+ throw new Error("Upload failed")
130
+ }
131
+ return json.data
132
+ }
133
+ }
@@ -0,0 +1,73 @@
1
+ type MaybeLike<T = unknown> = {
2
+ map: (fn: (value: T) => unknown) => MaybeLike
3
+ unwrapOr: (fallback: unknown) => unknown
4
+ }
5
+
6
+ const isMaybeLike = (value: unknown): value is MaybeLike => {
7
+ return (
8
+ typeof value === "object" &&
9
+ value !== null &&
10
+ "map" in value &&
11
+ typeof (value as MaybeLike).map === "function" &&
12
+ "unwrapOr" in value &&
13
+ typeof (value as MaybeLike).unwrapOr === "function"
14
+ )
15
+ }
16
+
17
+ const calculateSHA1Hash = async (value: string): Promise<string> => {
18
+ const data = new TextEncoder().encode(value)
19
+ const digest = await crypto.subtle.digest("SHA-1", data)
20
+ return Array.from(new Uint8Array(digest))
21
+ .map((byte) => byte.toString(16).padStart(2, "0"))
22
+ .join("")
23
+ }
24
+
25
+ export const cacheInMem = <T extends (...args: any[]) => Promise<any>>(
26
+ fn: T,
27
+ prefix: string,
28
+ timeout: number
29
+ ): T => {
30
+ const cache = new Map<string, { value: any; expiry: number }>()
31
+ const ongoingRequests = new Map<string, Promise<any>>()
32
+
33
+ return (async (...args: Parameters<T>): Promise<ReturnType<T>> => {
34
+ const hashRaw = args
35
+ .map((x) => {
36
+ if (isMaybeLike(x)) {
37
+ return x.map((item) => JSON.stringify(item)).unwrapOr("")
38
+ }
39
+ return JSON.stringify(x)
40
+ })
41
+ .join("--")
42
+
43
+ const hash = await calculateSHA1Hash(hashRaw)
44
+ const cacheKey = `${prefix}:${hash}`
45
+
46
+ const cached = cache.get(cacheKey)
47
+ if (cached && cached.expiry > Date.now()) {
48
+ return cached.value
49
+ }
50
+
51
+ const ongoingRequest = ongoingRequests.get(cacheKey)
52
+ if (ongoingRequest) {
53
+ return ongoingRequest
54
+ }
55
+
56
+ const requestPromise = (async () => {
57
+ try {
58
+ const result = await fn(...args)
59
+
60
+ if (result) {
61
+ cache.set(cacheKey, { value: result, expiry: Date.now() + timeout })
62
+ }
63
+ return result
64
+ } finally {
65
+ setTimeout(() => ongoingRequests.delete(cacheKey), 50)
66
+ }
67
+ })()
68
+
69
+ ongoingRequests.set(cacheKey, requestPromise)
70
+
71
+ return requestPromise
72
+ }) as T
73
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./Api"
2
+ export * from "./AccountService"
3
+ export * from "./AppService"
4
+ export * from "./ProfileService"
5
+ export * from "./EntityService"
@@ -0,0 +1,18 @@
1
+ import { atom } from "jotai"
2
+ import {
3
+ AccountLifecycleState,
4
+ AccountSnapshot as AccountSnapshotType,
5
+ } from "../types/account.types"
6
+
7
+ export type AccountSnapshot = AccountSnapshotType
8
+
9
+ const emptySnapshot: AccountSnapshot = {
10
+ state: AccountLifecycleState.Idle,
11
+ profiles: [],
12
+ profile: null,
13
+ accountId: null,
14
+ account: null,
15
+ error: null,
16
+ }
17
+
18
+ export const accountAtom = atom<AccountSnapshot>(emptySnapshot)
@@ -0,0 +1,13 @@
1
+ import { AccountState, Profile } from "@pmate/meta"
2
+ import { atom } from "jotai"
3
+ import { accountAtom } from "./accountAtom"
4
+
5
+ export const accountStateAtom = atom((get): AccountState | undefined => {
6
+ const account = get(accountAtom).account
7
+ return account ?? undefined
8
+ })
9
+
10
+ export const profileAtom = atom((get): Profile | undefined => {
11
+ const profile = get(accountAtom).profile
12
+ return profile ?? undefined
13
+ })
@@ -0,0 +1,24 @@
1
+ import { atom, type Atom } from "jotai"
2
+ import { atomFamily, type AtomFamily } from "jotai-family"
3
+ import { DEFAULT_APP_ID } from "../constants"
4
+ import { AppService } from "../api/AppService"
5
+ import type { AppConfig } from "../types/app"
6
+ import { resolveAppId } from "../utils/resolveAppId"
7
+
8
+ export const resolveAppConfigId = (app?: string | null) => {
9
+ return resolveAppId(app ?? DEFAULT_APP_ID)
10
+ }
11
+
12
+ type AppConfigAtomFamily = AtomFamily<string, Atom<Promise<AppConfig>>>
13
+
14
+ export const appConfigAtom: AppConfigAtomFamily = atomFamily((appId: string) =>
15
+ atom(async () => {
16
+ return AppService.getAppConfig(appId)
17
+ })
18
+ )
19
+
20
+ export const clearAppConfigAtoms = () => {
21
+ for (const appId of appConfigAtom.getParams()) {
22
+ appConfigAtom.remove(appId)
23
+ }
24
+ }