diff --git a/.gitignore b/.gitignore
index 118069a..46fc556 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,4 @@ docs/
# NestJS
uploads/
+.omx/
diff --git a/apps/backend/src/api/dto/api.dto.ts b/apps/backend/src/api/dto/api.dto.ts
index 98f0311..3cd297d 100644
--- a/apps/backend/src/api/dto/api.dto.ts
+++ b/apps/backend/src/api/dto/api.dto.ts
@@ -43,6 +43,9 @@ export class ApiDetailDto extends ApiBriefDto {
@Expose()
updatedAt: Date
+ @Expose()
+ currentVersionId?: string
+
@Expose()
@Transform(({ obj }) => {
const description = obj.currentVersion?.snapshot?.description
diff --git a/apps/frontend/src/components/workbench/WorkbenchSidebar.vue b/apps/frontend/src/components/workbench/WorkbenchSidebar.vue
index d398fe2..68479d0 100644
--- a/apps/frontend/src/components/workbench/WorkbenchSidebar.vue
+++ b/apps/frontend/src/components/workbench/WorkbenchSidebar.vue
@@ -175,7 +175,7 @@ function getEnvColor(envName: string): string {
-
+
导入 OpenAPI
diff --git a/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue b/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue
index 60f79fc..16ed207 100644
--- a/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue
+++ b/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue
@@ -4,9 +4,9 @@ import type { ApiDetail } from '@/types/api'
import { useRouteParams, useRouteQuery } from '@vueuse/router'
import { AlertCircle, FileText, GitBranch, Loader2, Pencil, Play, Settings2 } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
-import { apiApi } from '@/api/api'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
import ApiRunnerView from '../api-runner/ApiRunnerView.vue'
import VersionHistory from '../version/VersionHistory.vue'
import ApiDocView from './ApiDocView.vue'
@@ -34,36 +34,49 @@ watch(activeTab, (newV, oldV) => {
}
})
+const resourceStore = useWorkbenchResourceStore()
+
const apiDetail = ref(null)
const isLoading = ref(false)
const loadError = ref(null)
const isLoaded = computed(() => apiDetail.value !== null)
-async function fetchApiDetail() {
+function getCurrentApiRequestKey() {
+ return `${projectId.value}:${apiId.value}`
+}
+
+async function fetchApiDetail(options: { force?: boolean } = {}) {
+ const currentProjectId = projectId.value
+ const currentApiId = apiId.value
+ if (!currentProjectId || !currentApiId)
+ return
+
+ const requestKey = `${currentProjectId}:${currentApiId}`
isLoading.value = true
loadError.value = null
+
try {
- apiDetail.value = await apiApi.getApiDetail(projectId.value, apiId.value)
+ const detail = await resourceStore.getApiDetail(currentProjectId, currentApiId, options)
+ if (getCurrentApiRequestKey() !== requestKey)
+ return
+ apiDetail.value = detail
}
catch (error) {
+ if (getCurrentApiRequestKey() !== requestKey)
+ return
console.error('获取 API 详情失败:', error)
loadError.value = `获取 API 详情失败: ${error}`
}
finally {
- isLoading.value = false
+ if (getCurrentApiRequestKey() === requestKey) {
+ isLoading.value = false
+ }
}
}
async function refreshApiDetail() {
- loadError.value = null
- try {
- apiDetail.value = await apiApi.getApiDetail(projectId.value, apiId.value)
- }
- catch (error) {
- console.error('获取 API 详情失败:', error)
- loadError.value = `获取 API 详情失败: ${error}`
- }
+ await fetchApiDetail({ force: true })
}
// apiId 和 projectId 变化,刷新 API 详情
@@ -117,19 +130,19 @@ watch([apiId, projectId], ([curApiId, curProjectId]) => {
-
+
-
+
-
+
-
+
{
/>
-
+
diff --git a/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue b/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue
index 2c22b5b..7fc922e 100644
--- a/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue
+++ b/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue
@@ -10,7 +10,6 @@ import {
X,
} from 'lucide-vue-next'
import { computed, ref, toRaw, watch } from 'vue'
-import { versionApi } from '@/api/version'
import CodeBlock from '@/components/common/CodeBlock.vue'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -25,6 +24,7 @@ import {
import { methodBadgeColors } from '@/constants/api'
import { versionDiffFieldLabels, versionStatusColors, versionStatusLabels } from '@/constants/version'
import { cn } from '@/lib/utils'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
const props = defineProps<{
projectId: string
@@ -48,6 +48,7 @@ interface ComparisonResult {
}
const isOpen = defineModel('open', { required: true })
+const resourceStore = useWorkbenchResourceStore()
const comparison = ref(null)
const isLoading = ref(false)
@@ -124,47 +125,50 @@ function isComplexValue(value: unknown): boolean {
return typeof value === 'object' && value !== null
}
-async function fetchComparison() {
- if (!props.fromVersionId || !props.toVersionId)
- return
+function getComparisonRequestKey(
+ projectId = props.projectId,
+ apiId = props.apiId,
+ fromVersionId = props.fromVersionId,
+ toVersionId = props.toVersionId,
+) {
+ return `${projectId}:${apiId}:${fromVersionId ?? ''}:${toVersionId ?? ''}`
+}
+async function fetchComparison(projectId: string, apiId: string, fromVersionId: string, toVersionId: string) {
+ const requestKey = getComparisonRequestKey(projectId, apiId, fromVersionId, toVersionId)
isLoading.value = true
loadError.value = null
try {
- comparison.value = await versionApi.compareVersions(
- props.projectId,
- props.apiId,
- props.fromVersionId,
- props.toVersionId,
- )
+ const result = await resourceStore.getVersionComparison(projectId, apiId, fromVersionId, toVersionId)
+ if (!isOpen.value || getComparisonRequestKey() !== requestKey)
+ return
+ comparison.value = result
}
catch (error) {
+ if (!isOpen.value || getComparisonRequestKey() !== requestKey)
+ return
loadError.value = `获取比较数据失败: ${error}`
console.error('Failed to fetch comparison:', error)
}
finally {
- isLoading.value = false
+ if (!isOpen.value || getComparisonRequestKey() === requestKey) {
+ isLoading.value = false
+ }
}
}
watch(
- () => [props.fromVersionId, props.toVersionId],
- ([from, to]) => {
- if (from && to && isOpen.value) {
- fetchComparison()
- }
- else {
+ () => [isOpen.value, props.projectId, props.apiId, props.fromVersionId, props.toVersionId] as const,
+ ([open, projectId, apiId, from, to]) => {
+ if (!open || !from || !to) {
comparison.value = null
+ return
}
+ fetchComparison(projectId, apiId, from, to)
},
+ { immediate: true },
)
-
-watch(isOpen, (open) => {
- if (open && props.fromVersionId && props.toVersionId) {
- fetchComparison()
- }
-})
diff --git a/apps/frontend/src/components/workbench/version/VersionDetailSheet.vue b/apps/frontend/src/components/workbench/version/VersionDetailSheet.vue
index b6bb707..866f101 100644
--- a/apps/frontend/src/components/workbench/version/VersionDetailSheet.vue
+++ b/apps/frontend/src/components/workbench/version/VersionDetailSheet.vue
@@ -11,7 +11,6 @@ import {
Tag,
} from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
-import { versionApi } from '@/api/version'
import CodeBlock from '@/components/common/CodeBlock.vue'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -33,6 +32,7 @@ import {
} from '@/constants/version'
import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
const props = defineProps<{
projectId: string
@@ -50,6 +50,7 @@ const emits = defineEmits<{
}>()
const isOpen = defineModel('open', { required: true })
+const resourceStore = useWorkbenchResourceStore()
const versionDetail = ref(null)
const isLoading = ref(false)
@@ -90,48 +91,47 @@ const responsesPreview = computed(() => {
return JSON.stringify(versionDetail.value.responses, null, 2)
})
-async function fetchVersionDetail() {
- if (!props.versionId)
- return
+function getVersionDetailRequestKey(projectId = props.projectId, apiId = props.apiId, versionId = props.versionId) {
+ return `${projectId}:${apiId}:${versionId ?? ''}`
+}
+async function fetchVersionDetail(projectId: string, apiId: string, versionId: string) {
+ const requestKey = getVersionDetailRequestKey(projectId, apiId, versionId)
isLoading.value = true
loadError.value = null
try {
- const detail = await versionApi.getVersionDetail(
- props.projectId,
- props.apiId,
- props.versionId,
- )
+ const detail = await resourceStore.getVersionDetail(projectId, apiId, versionId)
+ if (!isOpen.value || getVersionDetailRequestKey() !== requestKey)
+ return
versionDetail.value = detail
emits('update:versionData', detail)
}
catch (error) {
+ if (!isOpen.value || getVersionDetailRequestKey() !== requestKey)
+ return
loadError.value = `获取版本详情失败: ${error}`
console.error('Failed to fetch version detail:', error)
}
finally {
- isLoading.value = false
+ if (!isOpen.value || getVersionDetailRequestKey() === requestKey) {
+ isLoading.value = false
+ }
}
}
watch(
- () => props.versionId,
- (newId) => {
- if (newId && isOpen.value) {
- fetchVersionDetail()
- }
- else {
+ () => [isOpen.value, props.projectId, props.apiId, props.versionId] as const,
+ ([open, projectId, apiId, versionId]) => {
+ if (!open || !versionId) {
versionDetail.value = null
+ emits('update:versionData', null)
+ return
}
+ fetchVersionDetail(projectId, apiId, versionId)
},
+ { immediate: true },
)
-
-watch(isOpen, (open) => {
- if (open && props.versionId) {
- fetchVersionDetail()
- }
-})
diff --git a/apps/frontend/src/components/workbench/version/VersionHistory.vue b/apps/frontend/src/components/workbench/version/VersionHistory.vue
index 7351499..7436e88 100644
--- a/apps/frontend/src/components/workbench/version/VersionHistory.vue
+++ b/apps/frontend/src/components/workbench/version/VersionHistory.vue
@@ -21,6 +21,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { usePermission } from '@/composables/usePermission'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
import PublishVersionDialog from '../dialogs/PublishVersionDialog.vue'
import RollbackConfirmDialog from '../dialogs/RollbackConfirmDialog.vue'
import VersionCompareSheet from './VersionCompareSheet.vue'
@@ -38,6 +39,7 @@ const emits = defineEmits<{
}>()
const { canPublishApi } = usePermission()
+const workbenchResourceStore = useWorkbenchResourceStore()
const VERSION_OPTIONS = [
{ label: '全部', value: 'ALL' },
@@ -79,20 +81,35 @@ const selectedVersion = computed(() => {
return versions.value.find(v => v.id === selectedVersionId.value) ?? null
})
-async function fetchVersions() {
+function getVersionListRequestKey(projectId = props.projectId, apiId = props.apiId) {
+ return `${projectId}:${apiId}`
+}
+
+async function fetchVersions(options: { force?: boolean } = {}) {
+ const { projectId, apiId } = props
+ if (!projectId || !apiId)
+ return
+
+ const requestKey = getVersionListRequestKey(projectId, apiId)
isLoading.value = true
loadError.value = null
try {
- const res = await versionApi.getVersionList(props.projectId, props.apiId)
- versions.value = res.versions
+ const versionList = await workbenchResourceStore.getVersionList(projectId, apiId, options)
+ if (getVersionListRequestKey() !== requestKey)
+ return
+ versions.value = versionList
}
catch (error) {
+ if (getVersionListRequestKey() !== requestKey)
+ return
loadError.value = `获取版本列表失败: ${error}`
console.error('Failed to fetch versions:', error)
}
finally {
- isLoading.value = false
+ if (getVersionListRequestKey() === requestKey) {
+ isLoading.value = false
+ }
}
}
@@ -132,17 +149,24 @@ async function handleConfirmPublish(data: PublishVersionReq) {
if (!publishTargetVersion.value)
return
+ const publishedVersionId = publishTargetVersion.value.id
+
try {
await versionApi.publishVersion(
props.projectId,
props.apiId,
- publishTargetVersion.value.id,
+ publishedVersionId,
data,
)
+ workbenchResourceStore.invalidateApiDetail(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionList(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionDetailsByApi(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionComparisonsByApi(props.projectId, props.apiId)
+
toast.success(`版本 ${data.version} 发布成功`)
publishDialogOpen.value = false
publishTargetVersion.value = null
- await fetchVersions()
+ await fetchVersions({ force: true })
emits('versionChanged')
}
catch (error) {
@@ -179,8 +203,13 @@ const suggestedNextVersion = computed(() => {
async function handleArchiveVersion(version: ApiVersionBrief) {
try {
await versionApi.archiveVersion(props.projectId, props.apiId, version.id)
+
+ workbenchResourceStore.invalidateVersionList(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionDetail(props.projectId, props.apiId, version.id)
+ workbenchResourceStore.invalidateVersionComparisonsByApi(props.projectId, props.apiId)
+
toast.success(`版本 v${version.version} 已归档`)
- await fetchVersions()
+ await fetchVersions({ force: true })
}
catch (error) {
console.error('Failed to archive version:', error)
@@ -197,15 +226,21 @@ async function handleConfirmRollback() {
return
try {
+ const rollbackVersionId = rollbackTargetVersion.value.id
await versionApi.rollbackToVersion(
props.projectId,
props.apiId,
- rollbackTargetVersion.value.id,
+ rollbackVersionId,
)
+
+ workbenchResourceStore.invalidateApiDetail(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionList(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionDetailsByApi(props.projectId, props.apiId)
+ workbenchResourceStore.invalidateVersionComparisonsByApi(props.projectId, props.apiId)
toast.success(`已回滚到版本 v${rollbackTargetVersion.value.version}`)
rollbackDialogOpen.value = false
rollbackTargetVersion.value = null
- await fetchVersions()
+ await fetchVersions({ force: true })
emits('versionChanged')
}
catch (error) {
@@ -222,11 +257,14 @@ watchEffect(() => !isCompareSheetOpen.value && (compareVersionIds.value = [null,
const detailVersionData = ref(null)
watch(
- () => props.apiId,
- () => {
- if (props.apiId) {
+ () => [props.projectId, props.apiId] as const,
+ ([projectId, apiId]) => {
+ if (projectId && apiId) {
fetchVersions()
}
+ else {
+ versions.value = []
+ }
},
{ immediate: true },
)
@@ -268,7 +306,7 @@ defineExpose({
size="icon"
class="h-7 w-7"
:disabled="isLoading"
- @click="fetchVersions"
+ @click="fetchVersions({ force: true })"
>
@@ -308,7 +346,7 @@ defineExpose({
diff --git a/apps/frontend/src/components/workbench/version/VersionQuickPanel.vue b/apps/frontend/src/components/workbench/version/VersionQuickPanel.vue
index 0218ab7..d5aa45c 100644
--- a/apps/frontend/src/components/workbench/version/VersionQuickPanel.vue
+++ b/apps/frontend/src/components/workbench/version/VersionQuickPanel.vue
@@ -2,13 +2,13 @@
import type { ApiVersionBrief } from '@/types/version'
import { GitBranch, History, Loader2 } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
-import { versionApi } from '@/api/version'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { versionStatusDotColors, versionStatusLabels } from '@/constants/version'
import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
const props = defineProps<{
projectId: string
@@ -20,35 +20,56 @@ const emits = defineEmits<{
(e: 'openHistory'): void
}>()
+const workbenchResourceStore = useWorkbenchResourceStore()
+
const recentVersions = ref([])
const isLoading = ref(false)
const totalCount = ref(0)
const currentVersion = computed(() => recentVersions.value.find(v => v.id === props.currentVersionId))
+function getVersionListRequestKey(projectId = props.projectId, apiId = props.apiId) {
+ return `${projectId}:${apiId}`
+}
+
async function fetchRecentVersions() {
+ const { projectId, apiId } = props
+ if (!projectId || !apiId)
+ return
+
+ const requestKey = getVersionListRequestKey(projectId, apiId)
isLoading.value = true
try {
- const res = await versionApi.getVersionList(props.projectId, props.apiId)
- totalCount.value = res.versions.length
- recentVersions.value = res.versions.slice(0, 3)
+ const versions = await workbenchResourceStore.getVersionList(projectId, apiId)
+ if (getVersionListRequestKey() !== requestKey)
+ return
+ totalCount.value = versions.length
+ recentVersions.value = versions.slice(0, 3)
}
catch (error) {
+ if (getVersionListRequestKey() !== requestKey)
+ return
console.error('Failed to fetch recent versions:', error)
}
finally {
- isLoading.value = false
+ if (getVersionListRequestKey() === requestKey) {
+ isLoading.value = false
+ }
}
}
const formatRelativeTime = (dateStr: string) => dayjs(dateStr).fromNow()
watch(
- () => props.apiId,
- () => {
- if (props.apiId) {
+ () => [props.projectId, props.apiId] as const,
+ ([projectId, apiId]) => {
+ if (projectId && apiId) {
fetchRecentVersions()
}
+ else {
+ recentVersions.value = []
+ totalCount.value = 0
+ }
},
{ immediate: true },
)
diff --git a/apps/frontend/src/layouts/WorkbenchLayout.vue b/apps/frontend/src/layouts/WorkbenchLayout.vue
index dea96e6..8dd4196 100644
--- a/apps/frontend/src/layouts/WorkbenchLayout.vue
+++ b/apps/frontend/src/layouts/WorkbenchLayout.vue
@@ -6,16 +6,19 @@ import WorkbenchSidebar from '@/components/workbench/WorkbenchSidebar.vue'
import { useApiTreeStore } from '@/stores/useApiTreeStore'
import { useProjectStore } from '@/stores/useProjectStore'
import { useTabStore } from '@/stores/useTabStore'
+import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore'
const projectId = useRouteParams('projectId')
const tabStore = useTabStore()
const apiTreeStore = useApiTreeStore()
const projectStore = useProjectStore()
+const workbenchResourceStore = useWorkbenchResourceStore()
// projectId 变化,更新项目数据
-watch(projectId, (newV) => {
- if (newV) {
+watch(projectId, (newV, oldV) => {
+ if (newV && newV !== oldV) {
+ workbenchResourceStore.reset()
apiTreeStore.setProjectId(newV)
projectStore.setProjectId(newV)
projectStore.init()
@@ -27,6 +30,7 @@ onUnmounted(() => {
apiTreeStore.reset()
tabStore.reset()
projectStore.reset()
+ workbenchResourceStore.reset()
})
diff --git a/apps/frontend/src/stores/useApiEditorStore.ts b/apps/frontend/src/stores/useApiEditorStore.ts
index 7cad3be..c4f8fd4 100644
--- a/apps/frontend/src/stores/useApiEditorStore.ts
+++ b/apps/frontend/src/stores/useApiEditorStore.ts
@@ -14,6 +14,7 @@ import { extractPathParamNames } from '@/lib/utils'
import { apiEditorDataSchema } from '@/validators/api'
import { useApiTreeStore } from './useApiTreeStore'
import { useTabStore } from './useTabStore'
+import { useWorkbenchResourceStore } from './useWorkbenchResourceStore'
/** 单个 API 的编辑器数据 */
export interface ApiEditorData {
@@ -638,6 +639,8 @@ export const useApiEditorStore = defineStore('apiEditor', () => {
if (!currentApiId.value || isSaving.value)
return false
+ const apiId = currentApiId.value
+
const validation = validate()
if (!validation.valid) {
toast.error(validation.message ?? '验证失败')
@@ -654,14 +657,17 @@ export const useApiEditorStore = defineStore('apiEditor', () => {
try {
const req = buildUpdateRequest()
- await apiApi.updateApi(projectId, currentApiId.value, req)
+ await apiApi.updateApi(projectId, apiId, req)
+
+ const workbenchResourceStore = useWorkbenchResourceStore()
+ workbenchResourceStore.invalidateApi(projectId, apiId)
const apiTreeStore = useApiTreeStore()
await apiTreeStore.refreshTree()
const tabStore = useTabStore()
const cache = getCurrentCache()
- tabStore.updateTabTitle(currentApiId.value, cache.data.basicInfo.name)
+ tabStore.updateTabTitle(apiId, cache.data.basicInfo.name)
toast.success('保存成功')
diff --git a/apps/frontend/src/stores/useApiTreeStore.ts b/apps/frontend/src/stores/useApiTreeStore.ts
index 1e8ebf7..d188fb3 100644
--- a/apps/frontend/src/stores/useApiTreeStore.ts
+++ b/apps/frontend/src/stores/useApiTreeStore.ts
@@ -2,6 +2,7 @@ import type { ApiBrief, CloneApiReq, GroupNodeWithApis, HttpMethod } from '@/typ
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { apiApi, groupApi } from '@/api/api'
+import { useWorkbenchResourceStore } from './useWorkbenchResourceStore'
type LoadingStatus = 'start' | 'loading' | 'end'
@@ -307,6 +308,10 @@ export const useApiTreeStore = defineStore('apiTree', () => {
return
await apiApi.deleteApi(projectId.value, apiId)
+
+ const workbenchResourceStore = useWorkbenchResourceStore()
+ workbenchResourceStore.invalidateApi(projectId.value, apiId)
+
await refreshTree()
if (selectedNodeId.value === apiId) {
diff --git a/apps/frontend/src/stores/useWorkbenchResourceStore.ts b/apps/frontend/src/stores/useWorkbenchResourceStore.ts
new file mode 100644
index 0000000..f711d9d
--- /dev/null
+++ b/apps/frontend/src/stores/useWorkbenchResourceStore.ts
@@ -0,0 +1,213 @@
+import type { ApiDetail } from '@/types/api'
+import type { ApiVersionBrief, ApiVersionComparison, ApiVersionDetail } from '@/types/version'
+import { defineStore } from 'pinia'
+import { apiApi } from '@/api/api'
+import { versionApi } from '@/api/version'
+
+interface GetResourceOptions {
+ force?: boolean
+}
+
+interface ResourceBucket {
+ cache: Map
+ inflight: Map>
+ versions: Map
+}
+
+function createResourceBucket(): ResourceBucket {
+ return {
+ cache: new Map(),
+ inflight: new Map>(),
+ versions: new Map(),
+ }
+}
+
+function bumpResourceVersion(bucket: ResourceBucket, key: string) {
+ const nextVersion = (bucket.versions.get(key) ?? 0) + 1
+ bucket.versions.set(key, nextVersion)
+ return nextVersion
+}
+
+function invalidateResource(bucket: ResourceBucket, key: string) {
+ bucket.cache.delete(key)
+ bucket.inflight.delete(key)
+ bumpResourceVersion(bucket, key)
+}
+
+function resetResource(bucket: ResourceBucket) {
+ bucket.cache.clear()
+ bucket.inflight.clear()
+ bucket.versions.clear()
+}
+
+function invalidateResourcesByPrefix(bucket: ResourceBucket, prefix: string) {
+ const keys = new Set([
+ ...bucket.cache.keys(),
+ ...bucket.inflight.keys(),
+ ])
+
+ for (const key of keys) {
+ if (key.startsWith(prefix)) {
+ invalidateResource(bucket, key)
+ }
+ }
+}
+
+export const useWorkbenchResourceStore = defineStore('workbenchResource', () => {
+ const apiDetailCache = createResourceBucket()
+ const versionListCache = createResourceBucket()
+ const versionDetailCache = createResourceBucket()
+ const versionComparisonCache = createResourceBucket()
+
+ function apiDetailKey(projectId: string, apiId: string) {
+ return `api-detail:${projectId}:${apiId}`
+ }
+
+ function versionListKey(projectId: string, apiId: string) {
+ return `version-list:${projectId}:${apiId}`
+ }
+
+ function versionDetailKey(projectId: string, apiId: string, versionId: string) {
+ return `version-detail:${projectId}:${apiId}:${versionId}`
+ }
+
+ function versionComparisonKey(projectId: string, apiId: string, fromVersionId: string, toVersionId: string) {
+ return `version-compare:${projectId}:${apiId}:${fromVersionId}:${toVersionId}`
+ }
+
+ async function getResource(
+ bucket: ResourceBucket,
+ key: string,
+ fetcher: () => Promise,
+ options: GetResourceOptions = {},
+ ): Promise {
+ if (!options.force && bucket.cache.has(key)) {
+ return bucket.cache.get(key)!
+ }
+
+ const inflight = bucket.inflight.get(key)
+ if (!options.force && inflight) {
+ return inflight
+ }
+
+ const requestVersion = options.force
+ ? bumpResourceVersion(bucket, key)
+ : bucket.versions.get(key) ?? 0
+
+ const request = fetcher()
+ .then((data) => {
+ if ((bucket.versions.get(key) ?? 0) === requestVersion) {
+ bucket.cache.set(key, data)
+ }
+ return data
+ })
+ .finally(() => {
+ if (bucket.inflight.get(key) === request) {
+ bucket.inflight.delete(key)
+ }
+ })
+
+ if (!options.force) {
+ bucket.inflight.set(key, request)
+ }
+
+ return request
+ }
+
+ function getApiDetail(projectId: string, apiId: string, options?: GetResourceOptions) {
+ return getResource(
+ apiDetailCache,
+ apiDetailKey(projectId, apiId),
+ () => apiApi.getApiDetail(projectId, apiId),
+ options,
+ )
+ }
+
+ function getVersionList(projectId: string, apiId: string, options?: GetResourceOptions) {
+ return getResource(
+ versionListCache,
+ versionListKey(projectId, apiId),
+ async () => {
+ const response = await versionApi.getVersionList(projectId, apiId)
+ return response.versions
+ },
+ options,
+ )
+ }
+
+ function getVersionDetail(projectId: string, apiId: string, versionId: string, options?: GetResourceOptions) {
+ return getResource(
+ versionDetailCache,
+ versionDetailKey(projectId, apiId, versionId),
+ () => versionApi.getVersionDetail(projectId, apiId, versionId),
+ options,
+ )
+ }
+
+ function getVersionComparison(
+ projectId: string,
+ apiId: string,
+ fromVersionId: string,
+ toVersionId: string,
+ options?: GetResourceOptions,
+ ) {
+ return getResource(
+ versionComparisonCache,
+ versionComparisonKey(projectId, apiId, fromVersionId, toVersionId),
+ () => versionApi.compareVersions(projectId, apiId, fromVersionId, toVersionId),
+ options,
+ )
+ }
+
+ function invalidateApiDetail(projectId: string, apiId: string) {
+ invalidateResource(apiDetailCache, apiDetailKey(projectId, apiId))
+ }
+
+ function invalidateVersionList(projectId: string, apiId: string) {
+ invalidateResource(versionListCache, versionListKey(projectId, apiId))
+ }
+
+ function invalidateVersionDetail(projectId: string, apiId: string, versionId: string) {
+ invalidateResource(versionDetailCache, versionDetailKey(projectId, apiId, versionId))
+ }
+
+ function invalidateVersionDetailsByApi(projectId: string, apiId: string) {
+ invalidateResourcesByPrefix(versionDetailCache, `version-detail:${projectId}:${apiId}:`)
+ }
+
+ function invalidateVersionComparisonsByApi(projectId: string, apiId: string) {
+ invalidateResourcesByPrefix(versionComparisonCache, `version-compare:${projectId}:${apiId}:`)
+ }
+
+ function invalidateApi(projectId: string, apiId: string) {
+ invalidateApiDetail(projectId, apiId)
+ invalidateVersionList(projectId, apiId)
+ invalidateVersionDetailsByApi(projectId, apiId)
+ invalidateVersionComparisonsByApi(projectId, apiId)
+ }
+
+ function reset() {
+ resetResource(apiDetailCache)
+ resetResource(versionListCache)
+ resetResource(versionDetailCache)
+ resetResource(versionComparisonCache)
+ }
+
+ return {
+ apiDetailKey,
+ versionListKey,
+ versionDetailKey,
+ versionComparisonKey,
+ getApiDetail,
+ getVersionList,
+ getVersionDetail,
+ getVersionComparison,
+ invalidateApiDetail,
+ invalidateVersionList,
+ invalidateVersionDetail,
+ invalidateVersionDetailsByApi,
+ invalidateVersionComparisonsByApi,
+ invalidateApi,
+ reset,
+ }
+})