feat(gpt-runner-web): move server state to storage api

This commit is contained in:
JinmingYang
2023-06-07 22:48:47 +08:00
parent 05287551ce
commit 5ce0bd6541
21 changed files with 246 additions and 163 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
.nuxt
.output
.temp
.gradle
.qodana
build
*.local
*.log
*.vsix

View File

@@ -21,6 +21,7 @@ export default defineBuildConfig({
clean: true,
declaration: true,
externals: [
'@kvs/node-localstorage',
'unconfig',
'express',
'debug',
@@ -29,5 +30,14 @@ export default defineBuildConfig({
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: {
compilerOptions: {
baseUrl: '.',
paths: {
// fix types error
'@kvs/storage': ['./node_modules/@kvs/storage/'],
},
},
},
},
})

View File

@@ -56,6 +56,8 @@
"stub": "unbuild --stub"
},
"peerDependencies": {
"@kvs/node-localstorage": "*",
"cachedir": "*",
"debug": "*",
"find-free-ports": "*",
"ip": "*",
@@ -63,6 +65,9 @@
"zod": "*"
},
"dependencies": {
"@kvs/storage": "^2.1.3",
"@kvs/node-localstorage": "^2.1.3",
"cachedir": "^2.3.0",
"debug": "^4.3.4",
"find-free-ports": "^3.1.1",
"ip": "^1.1.8",

View File

@@ -21,3 +21,8 @@ export enum GptFileTreeItemType {
File = 'file',
Chat = 'chat',
}
export enum ServerStorageName {
FrontendState = 'frontend-state',
WebPreset = 'web-preset',
}

View File

@@ -1,4 +1,5 @@
import type { GptFileInfo, GptFileInfoTree, SingleChatMessage, SingleFileConfig, UserConfig } from './config'
import type { ServerStorageName } from './enum'
export interface BaseResponse<T = any> {
type: 'Success' | 'Fail'
@@ -35,20 +36,22 @@ export interface GetUserConfigResData {
userConfig: UserConfig
}
export interface GetStateReqParams {
export interface GetStorageReqParams {
storageName: ServerStorageName
key: string
}
export type FrontendState = Record<string, any> | null | undefined
export type ServerStorageValue = Record<string, any> | null | undefined
export interface GetStateResData {
state: FrontendState
export interface GetStorageResData {
value: ServerStorageValue
cacheDir: string
}
export interface SaveStateReqParams {
export interface SaveStorageReqParams {
storageName: ServerStorageName
key: string
state: FrontendState
value?: ServerStorageValue
}
export type SaveStateResData = null
export type SaveStorageResData = null

View File

@@ -1,5 +1,5 @@
import { z } from 'zod'
import { ChatMessageStatus, ChatRole, ClientEventName, GptFileTreeItemType } from '../types'
import { ChatMessageStatus, ChatRole, ClientEventName, GptFileTreeItemType, ServerStorageName } from '../types'
export const ChatRoleSchema = z.nativeEnum(ChatRole)
@@ -8,3 +8,5 @@ export const ChatMessageStatusSchema = z.nativeEnum(ChatMessageStatus)
export const ClientEventNameSchema = z.nativeEnum(ClientEventName)
export const GptFileTreeItemTypeSchema = z.nativeEnum(GptFileTreeItemType)
export const ServerStorageNameSchema = z.nativeEnum(ServerStorageName)

View File

@@ -1,6 +1,7 @@
import { z } from 'zod'
import type { ChatStreamReqParams, GetGptFilesReqParams, GetUserConfigReqParams } from '../types'
import type { ChatStreamReqParams, GetGptFilesReqParams, GetStorageReqParams, GetUserConfigReqParams, SaveStorageReqParams } from '../types'
import { SingleChatMessageSchema, SingleFileConfigSchema } from './config.zod'
import { ServerStorageNameSchema } from './enum.zod'
export const ChatStreamReqParamsSchema = z.object({
messages: z.array(SingleChatMessageSchema),
@@ -18,11 +19,13 @@ export const GetUserConfigReqParamsSchema = z.object({
rootPath: z.string(),
}) satisfies z.ZodType<GetUserConfigReqParams>
export const GetStateReqParamsSchema = z.object({
export const GetStorageReqParamsSchema = z.object({
storageName: ServerStorageNameSchema,
key: z.string(),
})
}) satisfies z.ZodType<GetStorageReqParams>
export const SaveStateReqParamsSchema = z.object({
export const SaveStorageReqParamsSchema = z.object({
storageName: ServerStorageNameSchema,
key: z.string(),
state: z.record(z.any()).nullable().optional(),
})
value: z.record(z.any()).nullable().optional(),
}) satisfies z.ZodType<SaveStorageReqParams>

View File

@@ -0,0 +1,19 @@
import fs from 'node:fs'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
import getCacheDir from 'cachedir'
import { PathUtils } from './path-utils'
export async function getGlobalCacheDir(name: string) {
const cacheDir = getCacheDir(name)
await createCacheDir(cacheDir)
return cacheDir
}
async function createCacheDir(cacheDir: string) {
if (await PathUtils.isAccessible(cacheDir, 'W'))
return
await fs.promises.mkdir(cacheDir, { recursive: true })
}

View File

@@ -0,0 +1,18 @@
import { kvsLocalStorage } from '@kvs/node-localstorage'
import type { ServerStorageName } from '../../common/types'
import { getGlobalCacheDir } from './get-cache-dir'
export async function getStorage(storageName: ServerStorageName) {
const cacheFolder = await getGlobalCacheDir('gpt-runner-server')
const storage = await kvsLocalStorage<Record<string, Record<string, any> | null>>({
name: storageName,
storeFilePath: cacheFolder,
version: 1,
})
return {
cacheDir: cacheFolder,
storage,
}
}

View File

@@ -1,4 +1,6 @@
export * from './file-utils'
export * from './get-cache-dir'
export * from './get-storage'
export * from './network'
export * from './path-utils'
export * from './request'
export * from './server'

View File

@@ -0,0 +1,39 @@
import type { BaseResponse, GetStorageReqParams, GetStorageResData, SaveStorageReqParams, SaveStorageResData } from '@nicepkg/gpt-runner-shared/common'
import { getGlobalConfig } from '../helpers/global-config'
export interface GetServerStorageParams extends GetStorageReqParams {
}
export type GetServerStorageRes = BaseResponse<GetStorageResData>
export async function getServerStorage(params: GetServerStorageParams): Promise<GetServerStorageRes> {
const { storageName, key } = params
const res = await fetch(`${getGlobalConfig().serverBaseUrl}/api/storage?storageName=${storageName}&key=${key}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json()
return data
}
export interface SaveServerStorageParams extends SaveStorageReqParams {
}
export type SaveServerStorageRes = BaseResponse<SaveStorageResData>
export async function saveServerStorage(params: SaveServerStorageParams): Promise<SaveServerStorageRes> {
const { storageName, key, value } = params
const res = await fetch(`${getGlobalConfig().serverBaseUrl}/api/storage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ storageName, key, value }),
})
const data = await res.json()
return data
}

View File

@@ -1,39 +0,0 @@
import type { BaseResponse, GetStateReqParams, GetStateResData, SaveStateReqParams, SaveStateResData } from '@nicepkg/gpt-runner-shared/common'
import { getGlobalConfig } from '../helpers/global-config'
export interface FetchStateParams extends GetStateReqParams {
}
export type FetchStateRes = BaseResponse<GetStateResData>
export async function fetchState(params: FetchStateParams): Promise<FetchStateRes> {
const { key } = params
const res = await fetch(`${getGlobalConfig().serverBaseUrl}/api/state?key=${key}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json()
return data
}
export interface SaveStateParams extends SaveStateReqParams {
}
export type SaveStateRes = BaseResponse<SaveStateResData>
export async function saveState(params: SaveStateParams): Promise<SaveStateRes> {
const { key, state } = params
const res = await fetch(`${getGlobalConfig().serverBaseUrl}/api/state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key, state }),
})
const data = await res.json()
return data
}

View File

@@ -1,27 +1,36 @@
import type { StateStorage } from 'zustand/middleware'
import type { FrontendState } from '@nicepkg/gpt-runner-shared/common'
import { debounce, runOnceByKey, tryParseJson } from '@nicepkg/gpt-runner-shared/common'
import type { ServerStorageValue } from '@nicepkg/gpt-runner-shared/common'
import { ServerStorageName, debounce, tryParseJson } from '@nicepkg/gpt-runner-shared/common'
import { getGlobalConfig } from '../../helpers/global-config'
import { fetchState, saveState } from '../../networks/state'
import { getServerStorage, saveServerStorage } from '../../networks/server-storage'
let hasUpdateStateFromRemote = false
const debounceSaveState = debounce(async (key: string, state: FrontendState) => {
if (!hasUpdateStateFromRemote)
// just only request once onload
let hasUpdateStateFromRemote: 'pending' | 'finish' | false = false
async function getStateFromServerOnce(key: string) {
if (hasUpdateStateFromRemote !== false)
return
return await saveState({ key, state })
}, 1000)
function updateStateFromRemoteOnce(key: string) {
return runOnceByKey(async (key: string) => {
const res = await fetchState({ key })
hasUpdateStateFromRemote = true
return res
}, key)(key)
hasUpdateStateFromRemote = 'pending'
const res = await getServerStorage({
storageName: ServerStorageName.FrontendState,
key,
})
hasUpdateStateFromRemote = 'finish'
return res?.data?.value
}
// will save each action state to server
const debounceSaveStateToServer = debounce(async (key: string, value: ServerStorageValue) => {
if (hasUpdateStateFromRemote !== 'finish')
return
return await saveServerStorage({
storageName: ServerStorageName.FrontendState,
key,
value,
})
}, 1000)
export class CustomStorage implements StateStorage {
#storage: Storage
@@ -36,7 +45,7 @@ export class CustomStorage implements StateStorage {
getItem = async (key: string) => {
const finalKey = CustomStorage.prefixKey + key
const remoteSourceValue = (await updateStateFromRemoteOnce(finalKey))?.data?.state
const remoteSourceValue = await getStateFromServerOnce(finalKey)
if (remoteSourceValue !== undefined) {
const remoteString = JSON.stringify(remoteSourceValue)
@@ -52,7 +61,7 @@ export class CustomStorage implements StateStorage {
const finalKey = CustomStorage.prefixKey + key
// save to server
debounceSaveState(finalKey, tryParseJson(value))
debounceSaveStateToServer(finalKey, tryParseJson(value))
return this.#storage.setItem(finalKey, value)
}
@@ -61,7 +70,7 @@ export class CustomStorage implements StateStorage {
const finalKey = CustomStorage.prefixKey + key
// save to server
debounceSaveState(finalKey, null)
debounceSaveStateToServer(finalKey, null)
return this.#storage.removeItem(finalKey)
}

View File

@@ -62,13 +62,11 @@
"stub": "unbuild --stub"
},
"dependencies": {
"@kvs/node-localstorage": "^2.1.3",
"@microsoft/fetch-event-source": "^2.0.1",
"@nicepkg/gpt-runner-core": "workspace:*",
"@nicepkg/gpt-runner-shared": "workspace:*",
"@tanstack/react-query": "^4.29.11",
"@vscode/webview-ui-toolkit": "^1.2.2",
"cachedir": "^2.3.0",
"clsx": "^1.2.1",
"commander": "^10.0.1",
"connect-history-api-fallback": "^2.0.0",

View File

@@ -3,14 +3,14 @@ import type { Controller, ControllerConfig } from '../types'
import { chatgptControllers } from './chatgpt.controller'
import { configControllers } from './config.controller'
import { gptFilesControllers } from './gpt-files.controller'
import { stateControllers } from './state.controller'
import { storageControllers } from './storage.controller'
export function processControllers(router: Router) {
const allControllersConfig: ControllerConfig[] = [
chatgptControllers,
configControllers,
gptFilesControllers,
stateControllers,
storageControllers,
]
allControllersConfig.forEach((controllerConfig) => {

View File

@@ -1,79 +0,0 @@
import { sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import type { GetStateReqParams, GetStateResData, SaveStateReqParams, SaveStateResData } from '@nicepkg/gpt-runner-shared/common'
import { Debug, GetStateReqParamsSchema, SaveStateReqParamsSchema } from '@nicepkg/gpt-runner-shared/common'
import { kvsLocalStorage } from '@kvs/node-localstorage'
import { getGlobalCacheDir } from '../helpers/get-cache-dir'
import type { ControllerConfig } from '../types'
const debug = new Debug('state.controller')
async function getStorage() {
const cacheFolder = await getGlobalCacheDir('gpt-runner-server')
debug.log('cacheFolder', cacheFolder)
const storage = await kvsLocalStorage<Record<string, Record<string, any> | null>>({
name: 'frontend-state',
storeFilePath: cacheFolder,
version: 1,
})
return {
cacheDir: cacheFolder,
storage,
}
}
export const stateControllers: ControllerConfig = {
namespacePath: '/state',
controllers: [
{
url: '/',
method: 'get',
handler: async (req, res) => {
const query = req.query as GetStateReqParams
verifyParamsByZod(query, GetStateReqParamsSchema)
const { key } = query
const { storage, cacheDir } = await getStorage()
const state = await storage.get(key)
sendSuccessResponse(res, {
data: {
state,
cacheDir,
} satisfies GetStateResData,
})
},
},
{
url: '/',
method: 'post',
handler: async (req, res) => {
const body = req.body as SaveStateReqParams
verifyParamsByZod(body, SaveStateReqParamsSchema)
const { key, state } = body
const { storage } = await getStorage()
switch (state) {
case undefined:
// remove
await storage.delete(key)
break
default:
// set
await storage.set(key, state)
break
}
sendSuccessResponse(res, {
data: null satisfies SaveStateResData,
})
},
},
],
}

View File

@@ -0,0 +1,59 @@
import { getStorage, sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import type { GetStorageReqParams, GetStorageResData, SaveStorageReqParams, SaveStorageResData } from '@nicepkg/gpt-runner-shared/common'
import { GetStorageReqParamsSchema, SaveStorageReqParamsSchema } from '@nicepkg/gpt-runner-shared/common'
import type { ControllerConfig } from '../types'
export const storageControllers: ControllerConfig = {
namespacePath: '/storage',
controllers: [
{
url: '/',
method: 'get',
handler: async (req, res) => {
const query = req.query as GetStorageReqParams
verifyParamsByZod(query, GetStorageReqParamsSchema)
const { key, storageName } = query
const { storage, cacheDir } = await getStorage(storageName)
const value = await storage.get(key)
sendSuccessResponse(res, {
data: {
value,
cacheDir,
} satisfies GetStorageResData,
})
},
},
{
url: '/',
method: 'post',
handler: async (req, res) => {
const body = req.body as SaveStorageReqParams
verifyParamsByZod(body, SaveStorageReqParamsSchema)
const { storageName, key, value } = body
const { storage } = await getStorage(storageName)
switch (value) {
case undefined:
// remove
await storage.delete(key)
break
default:
// set
await storage.set(key, value)
break
}
sendSuccessResponse(res, {
data: null satisfies SaveStorageResData,
})
},
},
],
}

View File

@@ -0,0 +1,22 @@
import { kvsLocalStorage } from '@kvs/node-localstorage'
import { getGlobalCacheDir } from './get-cache-dir'
export enum StorageName {
FrontendState = 'frontend-state',
WebPreset = 'web-preset',
}
export async function getStorage(storageName: StorageName) {
const cacheFolder = await getGlobalCacheDir('gpt-runner-server')
const storage = await kvsLocalStorage<Record<string, Record<string, any> | null>>({
name: storageName,
storeFilePath: cacheFolder,
version: 1,
})
return {
cacheDir: cacheFolder,
storage,
}
}

15
pnpm-lock.yaml generated
View File

@@ -199,6 +199,15 @@ importers:
packages/gpt-runner-shared:
dependencies:
'@kvs/node-localstorage':
specifier: ^2.1.3
version: 2.1.3
'@kvs/storage':
specifier: ^2.1.3
version: 2.1.3
cachedir:
specifier: ^2.3.0
version: 2.3.0
debug:
specifier: ^4.3.4
version: 4.3.4
@@ -252,9 +261,6 @@ importers:
packages/gpt-runner-web:
dependencies:
'@kvs/node-localstorage':
specifier: ^2.1.3
version: 2.1.3
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
@@ -270,9 +276,6 @@ importers:
'@vscode/webview-ui-toolkit':
specifier: ^1.2.2
version: 1.2.2(react@18.2.0)
cachedir:
specifier: ^2.3.0
version: 2.3.0
clsx:
specifier: ^1.2.1
version: 1.2.1

View File

@@ -6,6 +6,7 @@
"moduleResolution": "node",
"useDefineForClassFields": true,
"esModuleInterop": true,
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,