refactor: minor performance improvements on teams related operations

This commit is contained in:
Andrew Bastin
2023-09-18 18:50:57 +05:30
parent bcc1147f81
commit 185b575e5b
16 changed files with 519 additions and 202 deletions

View File

@@ -0,0 +1,238 @@
import { describe, expect, vi, it, beforeEach, afterEach } from "vitest"
import { TestContainer } from "dioc/testing"
import { WorkspaceService } from "../workspace.service"
import { setPlatformDef } from "~/platform"
import { BehaviorSubject } from "rxjs"
import { effectScope, nextTick } from "vue"
const listAdapterMock = vi.hoisted(() => ({
isInitialized: false,
initialize: vi.fn(() => {
listAdapterMock.isInitialized = true
}),
dispose: vi.fn(() => {
listAdapterMock.isInitialized = false
}),
fetchList: vi.fn(),
}))
vi.mock("~/helpers/teams/TeamListAdapter", () => ({
default: class {
isInitialized = listAdapterMock.isInitialized
initialize = listAdapterMock.initialize
dispose = listAdapterMock.dispose
fetchList = listAdapterMock.fetchList
},
}))
describe("WorkspaceService", () => {
const platformMock = {
auth: {
getCurrentUserStream: vi.fn(),
getCurrentUser: vi.fn(),
},
}
beforeEach(() => {
// @ts-expect-error - We're mocking the platform
setPlatformDef(platformMock)
platformMock.auth.getCurrentUserStream.mockReturnValue(
new BehaviorSubject(null)
)
platformMock.auth.getCurrentUser.mockReturnValue(null)
})
describe("Initialization", () => {
it("should initialize with the personal workspace selected", () => {
const container = new TestContainer()
const service = container.bind(WorkspaceService)
expect(service.currentWorkspace.value).toEqual({ type: "personal" })
})
})
describe("updateWorkspaceTeamName", () => {
it("should update the workspace team name if the current workspace is a team workspace", () => {
const container = new TestContainer()
const service = container.bind(WorkspaceService)
service.changeWorkspace({
type: "team",
teamID: "test",
teamName: "before update",
})
service.updateWorkspaceTeamName("test")
expect(service.currentWorkspace.value).toEqual({
type: "team",
teamID: "test",
teamName: "test",
})
})
it("should not update the workspace team name if the current workspace is a personal workspace", () => {
const container = new TestContainer()
const service = container.bind(WorkspaceService)
service.changeWorkspace({
type: "personal",
})
service.updateWorkspaceTeamName("test")
expect(service.currentWorkspace.value).toEqual({ type: "personal" })
})
})
describe("changeWorkspace", () => {
it("updates the current workspace value to the given workspace", () => {
const container = new TestContainer()
const service = container.bind(WorkspaceService)
service.changeWorkspace({
type: "team",
teamID: "test",
teamName: "test",
})
expect(service.currentWorkspace.value).toEqual({
type: "team",
teamID: "test",
teamName: "test",
})
})
})
describe("acquireTeamListAdapter", () => {
beforeEach(() => {
vi.useFakeTimers()
listAdapterMock.fetchList.mockClear()
})
afterEach(() => {
vi.clearAllTimers()
})
it("should not poll if the polling time is null", () => {
const container = new TestContainer()
listAdapterMock.isInitialized = true // We need to initialize the list adapter before we can use it
const service = container.bind(WorkspaceService)
service.acquireTeamListAdapter(null)
vi.advanceTimersByTime(100000)
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
})
it("should not poll if the polling time is not null and user not logged in", async () => {
const container = new TestContainer()
const service = container.bind(WorkspaceService)
service.acquireTeamListAdapter(100)
await nextTick()
vi.advanceTimersByTime(110)
platformMock.auth.getCurrentUser.mockReturnValue(null)
platformMock.auth.getCurrentUserStream.mockReturnValue(
new BehaviorSubject(null)
)
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
})
it("should poll if the polling time is not null and the user is logged in", async () => {
const container = new TestContainer()
listAdapterMock.isInitialized = true // We need to initialize the list adapter before we can use it
platformMock.auth.getCurrentUser.mockReturnValue({
id: "test",
})
platformMock.auth.getCurrentUserStream.mockReturnValue(
new BehaviorSubject({ id: "test" })
)
const service = container.bind(WorkspaceService)
const adapter = service.acquireTeamListAdapter(100)
await nextTick()
vi.advanceTimersByTime(100)
expect(adapter!.fetchList).toHaveBeenCalledOnce()
})
it("emits 'managed-team-list-adapter-polled' when the service polls the adapter", async () => {
const container = new TestContainer()
listAdapterMock.isInitialized = true
platformMock.auth.getCurrentUser.mockReturnValue({
id: "test",
})
platformMock.auth.getCurrentUserStream.mockReturnValue(
new BehaviorSubject({ id: "test" })
)
const service = container.bind(WorkspaceService)
const eventFn = vi.fn()
const sub = service.getEventStream().subscribe(eventFn)
service.acquireTeamListAdapter(100)
await nextTick()
vi.advanceTimersByTime(100)
expect(eventFn).toHaveBeenCalledOnce()
expect(eventFn).toHaveBeenCalledWith({
type: "managed-team-list-adapter-polled",
})
sub.unsubscribe()
})
it("stops polling when the Vue effect scope is disposed and there is no more polling locks", async () => {
const container = new TestContainer()
listAdapterMock.isInitialized = true
platformMock.auth.getCurrentUser.mockReturnValue({
id: "test",
})
platformMock.auth.getCurrentUserStream.mockReturnValue(
new BehaviorSubject({ id: "test" })
)
const service = container.bind(WorkspaceService)
listAdapterMock.fetchList.mockClear() // Reset the counters
const scopeHandle = effectScope()
scopeHandle.run(() => {
service.acquireTeamListAdapter(100)
})
await nextTick()
vi.advanceTimersByTime(100)
expect(listAdapterMock.fetchList).toHaveBeenCalledOnce()
listAdapterMock.fetchList.mockClear()
scopeHandle.stop()
await nextTick()
vi.advanceTimersByTime(100)
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
})
})
})

View File

@@ -25,8 +25,7 @@ import {
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { hoppWorkspaceStore } from "~/newstore/workspace"
import { changeWorkspace } from "~/newstore/workspace"
import { WorkspaceService } from "~/services/workspace.service"
import { invokeAction } from "~/helpers/actions"
/**
@@ -46,6 +45,7 @@ export class CollectionsSpotlightSearcherService
public searcherSectionTitle = this.t("collection.my_collections")
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
constructor() {
super()
@@ -284,8 +284,8 @@ export class CollectionsSpotlightSearcherService
const folderPath = path.split("/").map((x) => parseInt(x))
const reqIndex = folderPath.pop()!
if (hoppWorkspaceStore.value.workspace.type !== "personal") {
changeWorkspace({
if (this.workspaceService.currentWorkspace.value.type !== "personal") {
this.workspaceService.changeWorkspace({
type: "personal",
})
}

View File

@@ -25,16 +25,15 @@ import { Service } from "dioc"
import * as E from "fp-ts/Either"
import MiniSearch from "minisearch"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import { useStreamStatic } from "~/composables/stream"
import { runGQLQuery } from "~/helpers/backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { workspaceStatus$ } from "~/newstore/workspace"
import { platform } from "~/platform"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import { WorkspaceService } from "~/services/workspace.service"
type Doc = {
text: string | string[]
@@ -58,14 +57,9 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
public searcherSectionTitle = this.t("spotlight.workspace.title")
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
private workspace = useStreamStatic(
workspaceStatus$,
{ type: "personal" },
() => {
/* noop */
}
)[0]
private workspace = this.workspaceService.currentWorkspace
private isTeamSelected = computed(
() =>
@@ -170,6 +164,7 @@ export class SwitchWorkspaceSpotlightSearcherService
public searcherSectionTitle = this.t("workspace.title")
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
constructor() {
super()
@@ -197,13 +192,7 @@ export class SwitchWorkspaceSpotlightSearcherService
})
}
private workspace = useStreamStatic(
workspaceStatus$,
{ type: "personal" },
() => {
/* noop */
}
)[0]
private workspace = this.workspaceService.currentWorkspace
createSearchSession(
query: Readonly<Ref<string>>

View File

@@ -0,0 +1,135 @@
import { tryOnScopeDispose, useIntervalFn } from "@vueuse/core"
import { Service } from "dioc"
import { computed, reactive, ref, watch, readonly } from "vue"
import { useStreamStatic } from "~/composables/stream"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { platform } from "~/platform"
import { min } from "lodash-es"
/**
* Defines a workspace and its information
*/
export type Workspace =
| { type: "personal" }
| { type: "team"; teamID: string; teamName: string }
export type WorkspaceServiceEvent = {
type: "managed-team-list-adapter-polled"
}
/**
* This services manages workspace related data and actions in Hoppscotch.
*/
export class WorkspaceService extends Service<WorkspaceServiceEvent> {
public static readonly ID = "WORKSPACE_SERVICE"
private _currentWorkspace = ref<Workspace>({ type: "personal" })
/**
* A readonly reference to the currently selected workspace
*/
public currentWorkspace = readonly(this._currentWorkspace)
private teamListAdapterLocks = reactive(new Map<number, number | null>())
private teamListAdapterLockTicker = 0 // Used to generate unique lock IDs
private managedTeamListAdapter = new TeamListAdapter(true, false)
private currentUser = useStreamStatic(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser(),
() => {
/* noop */
}
)[0]
private readonly pollingTime = computed(
() =>
min(Array.from(this.teamListAdapterLocks.values()).filter((x) => !!x)) ??
-1
)
constructor() {
super()
// Dispose the managed team list adapter when the user logs out
// and initialize it when the user logs in
watch(
this.currentUser,
(user) => {
if (!user && this.managedTeamListAdapter.isInitialized) {
this.managedTeamListAdapter.dispose()
}
if (user && !this.managedTeamListAdapter.isInitialized) {
this.managedTeamListAdapter.initialize()
}
},
{ immediate: true }
)
// Poll the managed team list adapter if the polling time is defined
const { pause: pauseListPoll, resume: resumeListPoll } = useIntervalFn(
() => {
if (this.managedTeamListAdapter.isInitialized) {
this.managedTeamListAdapter.fetchList()
this.emit({ type: "managed-team-list-adapter-polled" })
}
},
this.pollingTime,
{ immediate: true }
)
// Pause and resume the polling when the polling time changes
watch(
this.pollingTime,
(pollingTime) => {
if (pollingTime === -1) {
pauseListPoll()
} else {
resumeListPoll()
}
},
{ immediate: true }
)
}
// TODO: Update this function, its existence is pretty weird
/**
* Updates the name of the current workspace if it is a team workspace.
* @param newTeamName The new name of the team
*/
public updateWorkspaceTeamName(newTeamName: string) {
if (this._currentWorkspace.value.type === "team") {
this._currentWorkspace.value = {
...this._currentWorkspace.value,
teamName: newTeamName,
}
}
}
/**
* Changes the current workspace to the given workspace.
* @param workspace The new workspace
*/
public changeWorkspace(workspace: Workspace) {
this._currentWorkspace.value = workspace
}
/**
* Acquires a team list adapter that is managed by the workspace service.
* The team list adapter is associated with a Vue Scope and will be disposed
* when the scope is disposed.
* @param pollDuration The duration between polls in milliseconds. If null, the team list adapter will not poll.
*/
public acquireTeamListAdapter(pollDuration: number | null) {
const lockID = this.teamListAdapterLockTicker++
this.teamListAdapterLocks.set(lockID, pollDuration)
tryOnScopeDispose(() => {
this.teamListAdapterLocks.delete(lockID)
})
return this.managedTeamListAdapter
}
}