From 0ae3a82339f6a51986cbfa7c09565aa67a493632 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 21 May 2026 17:21:33 +0530 Subject: [PATCH] feat: drive IndexedDB cache layer from a single registry Introduces cacheableModels.js as the source of truth for which models the client caches in IDB and how to dispatch their revalidate actions. DataManager derives modelsToSync from the registry and creates object stores idempotently, so adding new cached models needs only one place to change plus a one-time DATA_VERSION bump. Also fixes two latent bugs that the upcoming boot-hydration relies on: setCacheKeys now awaits all per-key writes (was async-forEach with no await), and replace awaits clear before push (avoids ConstraintError on rapid refetches). --- .../helper/CacheHelper/DataManager.js | 27 +++++++----- .../helper/CacheHelper/cacheableModels.js | 41 +++++++++++++++++++ .../dashboard/helper/CacheHelper/version.js | 12 ++++-- 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 app/javascript/dashboard/helper/CacheHelper/cacheableModels.js diff --git a/app/javascript/dashboard/helper/CacheHelper/DataManager.js b/app/javascript/dashboard/helper/CacheHelper/DataManager.js index 23beaa971..909301e70 100644 --- a/app/javascript/dashboard/helper/CacheHelper/DataManager.js +++ b/app/javascript/dashboard/helper/CacheHelper/DataManager.js @@ -1,9 +1,10 @@ import { openDB } from 'idb'; import { DATA_VERSION } from './version'; +import { cacheableModels, cacheableModelNames } from './cacheableModels'; export class DataManager { constructor(accountId) { - this.modelsToSync = ['inbox', 'label', 'team']; + this.modelsToSync = cacheableModelNames; this.accountId = accountId; this.db = null; } @@ -11,12 +12,16 @@ export class DataManager { async initDb() { if (this.db) return this.db; const dbName = `cw-store-${this.accountId}`; - this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, { + this.db = await openDB(dbName, DATA_VERSION, { upgrade(db) { - db.createObjectStore('cache-keys'); - db.createObjectStore('inbox', { keyPath: 'id' }); - db.createObjectStore('label', { keyPath: 'id' }); - db.createObjectStore('team', { keyPath: 'id' }); + if (!db.objectStoreNames.contains('cache-keys')) { + db.createObjectStore('cache-keys'); + } + cacheableModels.forEach(model => { + if (!db.objectStoreNames.contains(model.name)) { + db.createObjectStore(model.name, { keyPath: 'id' }); + } + }); }, }); @@ -41,7 +46,7 @@ export class DataManager { async replace({ modelName, data }) { this.validateModel(modelName); - this.db.clear(modelName); + await this.db.clear(modelName); return this.push({ modelName, data }); } @@ -65,9 +70,11 @@ export class DataManager { } async setCacheKeys(cacheKeys) { - Object.keys(cacheKeys).forEach(async modelName => { - this.db.put('cache-keys', cacheKeys[modelName], modelName); - }); + await Promise.all( + Object.entries(cacheKeys).map(([modelName, value]) => + this.db.put('cache-keys', value, modelName) + ) + ); } async getCacheKey(modelName) { diff --git a/app/javascript/dashboard/helper/CacheHelper/cacheableModels.js b/app/javascript/dashboard/helper/CacheHelper/cacheableModels.js new file mode 100644 index 000000000..9eec7f354 --- /dev/null +++ b/app/javascript/dashboard/helper/CacheHelper/cacheableModels.js @@ -0,0 +1,41 @@ +// Single source of truth for IDB-cached workspace config. +// +// Each entry must keep `name` equal to the Rails `Model.name.underscore` value +// so the server's `cache_keys` payload (and the IDB object store name) lines up +// with what the client looks up. +// +// `dispatchPath` is the full Vuex dispatch path for the revalidate action. +// `setMutation` is the full commit path used by hydrateStoresFromCache to seed +// Vuex from IDB. `clearMutation` (optional) is committed BEFORE `setMutation` +// for modules whose SET_* mutation merges-by-id instead of replacing — without +// it, rows deleted server-side between sessions would survive as phantoms. +export const cacheableModels = [ + { + name: 'inbox', + dispatchPath: 'inboxes/revalidate', + setMutation: 'inboxes/SET_INBOXES', + }, + { + name: 'label', + dispatchPath: 'labels/revalidate', + setMutation: 'labels/SET_LABELS', + }, + { + name: 'team', + dispatchPath: 'teams/revalidate', + setMutation: 'teams/SET_TEAMS', + clearMutation: 'teams/CLEAR_TEAMS', + }, + { + name: 'canned_response', + dispatchPath: 'revalidateCannedResponses', + setMutation: 'SET_CANNED', + }, + { + name: 'account_user', + dispatchPath: 'agents/revalidate', + setMutation: 'agents/SET_AGENTS', + }, +]; + +export const cacheableModelNames = cacheableModels.map(model => model.name); diff --git a/app/javascript/dashboard/helper/CacheHelper/version.js b/app/javascript/dashboard/helper/CacheHelper/version.js index 07bd897f5..8cd958863 100644 --- a/app/javascript/dashboard/helper/CacheHelper/version.js +++ b/app/javascript/dashboard/helper/CacheHelper/version.js @@ -1,3 +1,9 @@ -// Monday, 13 March 2023 -// Change this version if you want to invalidate old data -export const DATA_VERSION = '1678706392'; +// Bump DATA_VERSION whenever a new object store is added to the IDB schema +// (the `upgrade()` callback in DataManager only runs when the stored DB +// version is less than the requested version). The upgrade body itself is +// idempotent — it conditionally creates each store via +// `objectStoreNames.contains(name)` — so a single bump is enough to add any +// number of new stores without losing existing data. +// +// Wednesday, 21 May 2026 — bumped to add canned_response + account_user stores +export const DATA_VERSION = '1747785600';