feat: cache canned responses and agents in IndexedDB

Converts the canned-response and agent API clients to CacheEnabledApiClient
subclasses and wires revalidate actions into the corresponding Vuex modules,
so settings pages and pickers stop fetching the same lists on every visit.
The canned-response search box continues to bypass the cache and hit the
network directly, so server-side ordering stays authoritative.

Also fixes a pre-existing gap in teams/revalidate: SET_TEAMS merges into a
record dict by id and never removes missing entries, so a team deleted
server-side would survive a reconnect-driven revalidate. Now matches the
get action by emitting CLEAR_TEAMS first.

Each affected spec clears its IDB store before every case, since the
preflight-throws-on-axios-reject pattern they relied on is going away in the
next commit.
This commit is contained in:
Shivam Mishra
2026-05-21 17:21:53 +05:30
parent 1a61e8102e
commit fe5c353106
9 changed files with 100 additions and 8 deletions
+17 -2
View File
@@ -1,12 +1,27 @@
/* global axios */
import ApiClient from './ApiClient';
import CacheEnabledApiClient from './CacheEnabledApiClient';
class Agents extends ApiClient {
class Agents extends CacheEnabledApiClient {
constructor() {
super('agents', { accountScoped: true });
}
// eslint-disable-next-line class-methods-use-this
get cacheModelName() {
return 'account_user';
}
// eslint-disable-next-line class-methods-use-this
extractDataFromResponse(response) {
return response.data;
}
// eslint-disable-next-line class-methods-use-this
marshallData(dataToParse) {
return { data: dataToParse };
}
bulkInvite({ emails }) {
return axios.post(`${this.url}/bulk_create`, {
emails,
+22 -5
View File
@@ -1,15 +1,32 @@
/* global axios */
import ApiClient from './ApiClient';
import CacheEnabledApiClient from './CacheEnabledApiClient';
class CannedResponse extends ApiClient {
class CannedResponse extends CacheEnabledApiClient {
constructor() {
super('canned_responses', { accountScoped: true });
}
get({ searchKey }) {
const url = searchKey ? `${this.url}?search=${searchKey}` : this.url;
return axios.get(url);
// eslint-disable-next-line class-methods-use-this
get cacheModelName() {
return 'canned_response';
}
// eslint-disable-next-line class-methods-use-this
extractDataFromResponse(response) {
return response.data;
}
// eslint-disable-next-line class-methods-use-this
marshallData(dataToParse) {
return { data: dataToParse };
}
get({ searchKey } = {}) {
if (searchKey) {
return axios.get(`${this.url}?search=${searchKey}`);
}
return super.get(true);
}
}
@@ -44,13 +44,24 @@ export const actions = {
get: async ({ commit }) => {
commit(types.default.SET_AGENT_FETCHING_STATUS, true);
try {
const response = await AgentAPI.get();
const response = await AgentAPI.get(true);
commit(types.default.SET_AGENT_FETCHING_STATUS, false);
commit(types.default.SET_AGENTS, response.data);
} catch (error) {
commit(types.default.SET_AGENT_FETCHING_STATUS, false);
}
},
revalidate: async ({ commit }, { newKey }) => {
try {
const isExistingKeyValid = await AgentAPI.validateCacheKey(newKey);
if (!isExistingKeyValid) {
const response = await AgentAPI.refetchAndCommit(newKey);
commit(types.default.SET_AGENTS, response.data);
}
} catch (error) {
// Ignore error
}
},
create: async ({ commit }, agentInfo) => {
commit(types.default.SET_AGENT_CREATING_STATUS, true);
try {
@@ -47,6 +47,22 @@ const actions = {
}
},
revalidateCannedResponses: async function revalidateCannedResponses(
{ commit },
{ newKey }
) {
try {
const isExistingKeyValid =
await CannedResponseAPI.validateCacheKey(newKey);
if (!isExistingKeyValid) {
const response = await CannedResponseAPI.refetchAndCommit(newKey);
commit(types.default.SET_CANNED, response.data);
}
} catch (error) {
// Ignore error
}
},
createCannedResponse: async function createCannedResponse(
{ commit },
cannedObj
@@ -1,6 +1,7 @@
import axios from 'axios';
import { actions } from '../../agents';
import * as types from '../../../mutation-types';
import AgentAPI from '../../../../api/agents';
import agentList from './fixtures';
const commit = vi.fn();
@@ -8,6 +9,13 @@ const dispatch = vi.fn();
global.axios = axios;
vi.mock('axios');
// Clear the IDB-backed cache between tests so each case starts from a known
// empty state and isn't affected by data persisted by a previous test.
beforeEach(async () => {
await AgentAPI.dataManager.initDb();
await AgentAPI.dataManager.db.clear(AgentAPI.cacheModelName);
});
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
@@ -1,12 +1,20 @@
import axios from 'axios';
import { actions } from '../../inboxes';
import * as types from '../../../mutation-types';
import InboxesAPI from '../../../../api/inboxes';
import inboxList from './fixtures';
const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
// Clear the IDB-backed cache between tests so each case starts from a known
// empty state and isn't affected by data persisted by a previous test.
beforeEach(async () => {
await InboxesAPI.dataManager.initDb();
await InboxesAPI.dataManager.db.clear(InboxesAPI.cacheModelName);
});
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
@@ -1,12 +1,20 @@
import axios from 'axios';
import { actions } from '../../labels';
import * as types from '../../../mutation-types';
import LabelsAPI from '../../../../api/labels';
import labelsList from './fixtures';
const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
// Clear the IDB-backed cache between tests so each case starts from a known
// empty state and isn't affected by data persisted by a previous test.
beforeEach(async () => {
await LabelsAPI.dataManager.initDb();
await LabelsAPI.dataManager.db.clear(LabelsAPI.cacheModelName);
});
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
@@ -8,12 +8,20 @@ import {
EDIT_TEAM,
DELETE_TEAM,
} from '../../teams/types';
import TeamsAPI from '../../../../api/teams';
import teamsList from './fixtures';
const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
// Clear the IDB-backed cache between tests so each case starts from a known
// empty state and isn't affected by data persisted by a previous test.
beforeEach(async () => {
await TeamsAPI.dataManager.initDb();
await TeamsAPI.dataManager.db.clear(TeamsAPI.cacheModelName);
});
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
@@ -27,6 +27,7 @@ export const actions = {
const isExistingKeyValid = await TeamsAPI.validateCacheKey(newKey);
if (!isExistingKeyValid) {
const response = await TeamsAPI.refetchAndCommit(newKey);
commit(CLEAR_TEAMS);
commit(SET_TEAMS, response.data);
}
} catch (error) {