@oxyhq/core 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +10 -0
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/HttpService.js +18 -4
  4. package/dist/cjs/OxyServices.base.js +15 -1
  5. package/dist/cjs/mixins/OxyServices.applications.js +69 -6
  6. package/dist/cjs/mixins/OxyServices.assets.js +16 -3
  7. package/dist/cjs/mixins/OxyServices.features.js +47 -10
  8. package/dist/cjs/mixins/OxyServices.managedAccounts.js +29 -3
  9. package/dist/cjs/mixins/OxyServices.privacy.js +34 -8
  10. package/dist/cjs/mixins/OxyServices.topics.js +5 -1
  11. package/dist/cjs/mixins/OxyServices.user.js +11 -2
  12. package/dist/cjs/mixins/OxyServices.workspaces.js +38 -3
  13. package/dist/cjs/utils/cache.js +9 -2
  14. package/dist/esm/.tsbuildinfo +1 -1
  15. package/dist/esm/HttpService.js +18 -4
  16. package/dist/esm/OxyServices.base.js +15 -1
  17. package/dist/esm/mixins/OxyServices.applications.js +69 -6
  18. package/dist/esm/mixins/OxyServices.assets.js +16 -3
  19. package/dist/esm/mixins/OxyServices.features.js +47 -10
  20. package/dist/esm/mixins/OxyServices.managedAccounts.js +29 -3
  21. package/dist/esm/mixins/OxyServices.privacy.js +34 -8
  22. package/dist/esm/mixins/OxyServices.topics.js +5 -1
  23. package/dist/esm/mixins/OxyServices.user.js +11 -2
  24. package/dist/esm/mixins/OxyServices.workspaces.js +38 -3
  25. package/dist/esm/utils/cache.js +9 -2
  26. package/dist/types/.tsbuildinfo +1 -1
  27. package/dist/types/HttpService.d.ts +9 -0
  28. package/dist/types/OxyServices.base.d.ts +12 -0
  29. package/dist/types/mixins/OxyServices.applications.d.ts +26 -0
  30. package/dist/types/mixins/OxyServices.features.d.ts +27 -6
  31. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +16 -1
  32. package/dist/types/mixins/OxyServices.privacy.d.ts +22 -4
  33. package/dist/types/mixins/OxyServices.user.d.ts +8 -1
  34. package/dist/types/mixins/OxyServices.workspaces.d.ts +12 -0
  35. package/dist/types/models/interfaces.d.ts +12 -0
  36. package/dist/types/utils/cache.d.ts +4 -1
  37. package/package.json +1 -4
  38. package/src/HttpService.ts +28 -4
  39. package/src/OxyServices.base.ts +15 -1
  40. package/src/__tests__/httpServiceCache.test.ts +68 -0
  41. package/src/__tests__/linkedClient.test.ts +61 -0
  42. package/src/mixins/OxyServices.applications.ts +71 -6
  43. package/src/mixins/OxyServices.assets.ts +16 -3
  44. package/src/mixins/OxyServices.features.ts +47 -10
  45. package/src/mixins/OxyServices.managedAccounts.ts +29 -3
  46. package/src/mixins/OxyServices.privacy.ts +34 -8
  47. package/src/mixins/OxyServices.topics.ts +5 -1
  48. package/src/mixins/OxyServices.user.ts +11 -2
  49. package/src/mixins/OxyServices.workspaces.ts +39 -3
  50. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +147 -0
  51. package/src/models/interfaces.ts +13 -1
  52. package/src/utils/cache.ts +9 -2
  53. package/dist/cjs/mixins/OxyServices.popup.js +0 -263
  54. package/dist/esm/mixins/OxyServices.popup.js +0 -261
  55. package/dist/types/mixins/OxyServices.popup.d.ts +0 -170
@@ -164,9 +164,19 @@ class HttpService {
164
164
  this.baseURL = config.baseURL;
165
165
  this.tokenStore = new TokenStore();
166
166
  this.logger = new requestUtils_1.SimpleLogger(config.enableLogging || false, config.logLevel || 'error', 'HttpService');
167
- // Initialize performance infrastructure
168
- this.cache = new cache_1.TTLCache(config.cacheTTL || 5 * 60 * 1000);
169
- (0, cache_1.registerCacheForCleanup)(this.cache);
167
+ // Initialize performance infrastructure. The per-instance GET response
168
+ // cache is disabled when the consumer explicitly opts out
169
+ // (`enableCache: false`) or asks for a non-positive TTL (`cacheTTL <= 0`).
170
+ // When disabled, nothing is ever stored, so there is no reason to register
171
+ // the cache for the global cleanup interval. Default (config unset) keeps
172
+ // caching ON with the 5-minute TTL — unchanged for existing consumers.
173
+ this.cacheDisabled =
174
+ config.enableCache === false ||
175
+ (typeof config.cacheTTL === 'number' && config.cacheTTL <= 0);
176
+ this.cache = new cache_1.TTLCache(config.cacheTTL && config.cacheTTL > 0 ? config.cacheTTL : 5 * 60 * 1000);
177
+ if (!this.cacheDisabled) {
178
+ (0, cache_1.registerCacheForCleanup)(this.cache);
179
+ }
170
180
  this.deduplicator = new requestUtils_1.RequestDeduplicator();
171
181
  this.requestQueue = new requestUtils_1.RequestQueue(config.maxConcurrentRequests || 10, config.requestQueueSize || 100);
172
182
  }
@@ -241,7 +251,11 @@ class HttpService {
241
251
  * Main request method - handles everything in one place
242
252
  */
243
253
  async request(config) {
244
- const { method, url, data, params, timeout = this.config.requestTimeout || DEFAULT_REQUEST_TIMEOUT_MS, signal, cache = method === 'GET', cacheTTL, deduplicate = true, retry = this.config.enableRetry !== false, maxRetries = this.config.maxRetries || 3, } = config;
254
+ const { method, url, data, params, timeout = this.config.requestTimeout || DEFAULT_REQUEST_TIMEOUT_MS, signal, cache: cacheRequested = method === 'GET', cacheTTL, deduplicate = true, retry = this.config.enableRetry !== false, maxRetries = this.config.maxRetries || 3, } = config;
255
+ // A per-instance disabled cache (`enableCache:false` / `cacheTTL<=0`)
256
+ // overrides any per-request `cache:true`: nothing is read from nor written
257
+ // to the response cache. Request deduplication below is unaffected.
258
+ const cache = cacheRequested && !this.cacheDisabled;
245
259
  // Generate cache key (optimized for large objects)
246
260
  const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;
247
261
  // Check cache first
@@ -96,9 +96,23 @@ class OxyServicesBase {
96
96
  * OxyServices instance mounted in OxyProvider. The returned client has its own
97
97
  * base URL, cache and request queue, but its bearer token is kept in lockstep
98
98
  * with this session and its 401 refresh path delegates back to this session.
99
+ *
100
+ * **GET response caching is OFF by default for linked clients.** The SDK's
101
+ * per-instance GET cache is only safe where the SDK OWNS invalidation: on the
102
+ * canonical OxyServices client, every mutation (`updateProfile`, `followUser`,
103
+ * `blockUser`, …) busts the matching cached GET. A linked client targets the
104
+ * consuming app's OWN backend (`api.mention.earth`, `api.syra.fm`, …), whose
105
+ * resources and write endpoints the SDK has no knowledge of — so it cannot
106
+ * invalidate them, and a cached GET there would silently serve stale data
107
+ * after the app mutates its own data. Caching is therefore unsafe-by-construction
108
+ * here and is left to the consumer's own layer (React Query / stores), which
109
+ * owns its invalidation. Pass `createLinkedClient({ baseURL, enableCache: true })`
110
+ * to explicitly opt back in when the consumer accepts that responsibility.
99
111
  */
100
112
  createLinkedClient(config) {
101
- const client = new HttpService_1.HttpService(config);
113
+ // Default the GET cache OFF unless the caller explicitly opts in (see the
114
+ // method doc): the SDK cannot invalidate the consumer backend's resources.
115
+ const client = new HttpService_1.HttpService({ ...config, enableCache: config.enableCache ?? false });
102
116
  const syncToken = (accessToken) => {
103
117
  const currentAccessToken = client.getAccessToken();
104
118
  if (accessToken) {
@@ -54,6 +54,9 @@ function OxyServicesApplicationsMixin(Base) {
54
54
  async createApplication(data) {
55
55
  try {
56
56
  const res = await this.makeRequest('POST', '/applications', data, { cache: false });
57
+ // Bust every cached application list (unscoped + per-workspace) so the
58
+ // new application appears on the next `getApplications()` read.
59
+ this._invalidateApplicationLists();
57
60
  return res.application;
58
61
  }
59
62
  catch (error) {
@@ -81,6 +84,10 @@ function OxyServicesApplicationsMixin(Base) {
81
84
  async updateApplication(applicationId, data) {
82
85
  try {
83
86
  const res = await this.makeRequest('PATCH', `/applications/${applicationId}`, data, { cache: false });
87
+ // Bust the cached detail and every list (which embeds application
88
+ // fields) so neither serves the pre-update snapshot.
89
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
90
+ this._invalidateApplicationLists();
84
91
  return res.application;
85
92
  }
86
93
  catch (error) {
@@ -93,7 +100,13 @@ function OxyServicesApplicationsMixin(Base) {
93
100
  */
94
101
  async deleteApplication(applicationId) {
95
102
  try {
96
- return await this.makeRequest('DELETE', `/applications/${applicationId}`, undefined, { cache: false });
103
+ const result = await this.makeRequest('DELETE', `/applications/${applicationId}`, undefined, { cache: false });
104
+ // Bust every cached representation of the deleted application.
105
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
106
+ this.clearCacheEntry(`GET:/applications/${applicationId}/members`);
107
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
108
+ this._invalidateApplicationLists();
109
+ return result;
97
110
  }
98
111
  catch (error) {
99
112
  throw this.handleError(error);
@@ -122,6 +135,7 @@ function OxyServicesApplicationsMixin(Base) {
122
135
  async inviteApplicationMember(applicationId, data) {
123
136
  try {
124
137
  const res = await this.makeRequest('POST', `/applications/${applicationId}/members`, data, { cache: false });
138
+ this._invalidateApplicationMembership(applicationId);
125
139
  return res.member;
126
140
  }
127
141
  catch (error) {
@@ -137,6 +151,7 @@ function OxyServicesApplicationsMixin(Base) {
137
151
  async updateApplicationMember(applicationId, memberId, data) {
138
152
  try {
139
153
  const res = await this.makeRequest('PATCH', `/applications/${applicationId}/members/${memberId}`, data, { cache: false });
154
+ this._invalidateApplicationMembership(applicationId);
140
155
  return res.member;
141
156
  }
142
157
  catch (error) {
@@ -150,7 +165,9 @@ function OxyServicesApplicationsMixin(Base) {
150
165
  */
151
166
  async removeApplicationMember(applicationId, memberId) {
152
167
  try {
153
- return await this.makeRequest('DELETE', `/applications/${applicationId}/members/${memberId}`, undefined, { cache: false });
168
+ const result = await this.makeRequest('DELETE', `/applications/${applicationId}/members/${memberId}`, undefined, { cache: false });
169
+ this._invalidateApplicationMembership(applicationId);
170
+ return result;
154
171
  }
155
172
  catch (error) {
156
173
  throw this.handleError(error);
@@ -164,7 +181,12 @@ function OxyServicesApplicationsMixin(Base) {
164
181
  */
165
182
  async transferApplicationOwnership(applicationId, data) {
166
183
  try {
167
- return await this.makeRequest('POST', `/applications/${applicationId}/transfer-ownership`, data, { cache: false });
184
+ const result = await this.makeRequest('POST', `/applications/${applicationId}/transfer-ownership`, data, { cache: false });
185
+ // Ownership change alters roles in the member list AND the detail, and
186
+ // can change which applications the caller "owns" in the list view.
187
+ this._invalidateApplicationMembership(applicationId);
188
+ this._invalidateApplicationLists();
189
+ return result;
168
190
  }
169
191
  catch (error) {
170
192
  throw this.handleError(error);
@@ -191,7 +213,9 @@ function OxyServicesApplicationsMixin(Base) {
191
213
  */
192
214
  async createApplicationCredential(applicationId, data) {
193
215
  try {
194
- return await this.makeRequest('POST', `/applications/${applicationId}/credentials`, data, { cache: false });
216
+ const result = await this.makeRequest('POST', `/applications/${applicationId}/credentials`, data, { cache: false });
217
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
218
+ return result;
195
219
  }
196
220
  catch (error) {
197
221
  throw this.handleError(error);
@@ -207,7 +231,11 @@ function OxyServicesApplicationsMixin(Base) {
207
231
  */
208
232
  async rotateApplicationCredential(applicationId, credentialId) {
209
233
  try {
210
- return await this.makeRequest('POST', `/applications/${applicationId}/credentials/${credentialId}/rotate`, undefined, { cache: false });
234
+ const result = await this.makeRequest('POST', `/applications/${applicationId}/credentials/${credentialId}/rotate`, undefined, { cache: false });
235
+ // Rotation changes credential status/audit fields surfaced by the
236
+ // credentials list (`rotatedFrom`, grace window, new active credential).
237
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
238
+ return result;
211
239
  }
212
240
  catch (error) {
213
241
  throw this.handleError(error);
@@ -221,7 +249,10 @@ function OxyServicesApplicationsMixin(Base) {
221
249
  */
222
250
  async revokeApplicationCredential(applicationId, credentialId) {
223
251
  try {
224
- return await this.makeRequest('DELETE', `/applications/${applicationId}/credentials/${credentialId}`, undefined, { cache: false });
252
+ const result = await this.makeRequest('DELETE', `/applications/${applicationId}/credentials/${credentialId}`, undefined, { cache: false });
253
+ // Revocation flips the credential's status in the cached list.
254
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
255
+ return result;
225
256
  }
226
257
  catch (error) {
227
258
  throw this.handleError(error);
@@ -240,5 +271,37 @@ function OxyServicesApplicationsMixin(Base) {
240
271
  throw this.handleError(error);
241
272
  }
242
273
  }
274
+ /**
275
+ * Bust every cached application list. `getApplications(workspaceId?)` keys
276
+ * the unscoped list as `GET:/applications` and each workspace-scoped list as
277
+ * `GET:/applications?workspaceId=<id>` (the query string is part of the URL
278
+ * path). A change to list membership (create/delete/ownership transfer)
279
+ * invalidates all of them, so we clear the unscoped entry plus every
280
+ * `?workspaceId=` variant via a prefix sweep. The prefix `GET:/applications?`
281
+ * matches only the query-string list variants, never the `GET:/applications/<id>…`
282
+ * detail/sub-resource keys.
283
+ *
284
+ * Internal helper (leading underscore); not part of the supported public
285
+ * surface. Public rather than `private` because mixins compose into an
286
+ * exported anonymous class, where TypeScript cannot represent a private
287
+ * member in the emitted declaration file (TS4094).
288
+ */
289
+ _invalidateApplicationLists() {
290
+ this.clearCacheEntry('GET:/applications');
291
+ this.clearCacheByPrefix('GET:/applications?');
292
+ }
293
+ /**
294
+ * Bust the cached member list and detail for an application after a
295
+ * membership mutation. The member list (`getApplicationMembers`) and the
296
+ * detail (`getApplication`, which can embed member counts) both go stale
297
+ * when the member set or a member's role changes.
298
+ *
299
+ * Internal helper (leading underscore); see `_invalidateApplicationLists`
300
+ * for why this is public rather than `private`.
301
+ */
302
+ _invalidateApplicationMembership(applicationId) {
303
+ this.clearCacheEntry(`GET:/applications/${applicationId}/members`);
304
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
305
+ }
243
306
  };
244
307
  }
@@ -325,7 +325,10 @@ function OxyServicesAssetsMixin(Base) {
325
325
  */
326
326
  async assetRestore(fileId) {
327
327
  try {
328
- return await this.makeRequest('POST', `/assets/${fileId}/restore`, undefined, { cache: false });
328
+ const result = await this.makeRequest('POST', `/assets/${fileId}/restore`, undefined, { cache: false });
329
+ // The asset metadata (trash state) changed — bust its cached read.
330
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
331
+ return result;
329
332
  }
330
333
  catch (error) {
331
334
  throw this.handleError(error);
@@ -337,7 +340,11 @@ function OxyServicesAssetsMixin(Base) {
337
340
  async assetDelete(fileId, force = false) {
338
341
  try {
339
342
  const params = force ? { force: 'true' } : undefined;
340
- return await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
343
+ const result = await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
344
+ // Bust the cached metadata and every cached URL variant for the asset.
345
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
346
+ this.clearCacheByPrefix(`GET:/assets/${fileId}/url`);
347
+ return result;
341
348
  }
342
349
  catch (error) {
343
350
  throw this.handleError(error);
@@ -360,9 +367,15 @@ function OxyServicesAssetsMixin(Base) {
360
367
  */
361
368
  async assetUpdateVisibility(fileId, visibility) {
362
369
  try {
363
- return await this.makeRequest('PATCH', `/assets/${fileId}/visibility`, {
370
+ const result = await this.makeRequest('PATCH', `/assets/${fileId}/visibility`, {
364
371
  visibility
365
372
  }, { cache: false });
373
+ // Visibility changes both the asset metadata and the resolved URL
374
+ // (public CDN vs signed). Bust the metadata read and every cached URL
375
+ // variant (keyed on variant/expiresIn params).
376
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
377
+ this.clearCacheByPrefix(`GET:/assets/${fileId}/url`);
378
+ return result;
366
379
  }
367
380
  catch (error) {
368
381
  throw this.handleError(error);
@@ -59,10 +59,14 @@ function OxyServicesFeaturesMixin(Base) {
59
59
  */
60
60
  async subscribe(planId, paymentMethodId) {
61
61
  return this.withAuthRetry(async () => {
62
- return await this.makeRequest('POST', '/subscriptions/subscribe', {
62
+ const result = await this.makeRequest('POST', '/subscriptions/subscribe', {
63
63
  planId,
64
64
  paymentMethodId,
65
65
  }, { cache: false });
66
+ // The current subscription changed — bust its cached read so
67
+ // `getCurrentSubscription()` reflects the new plan immediately.
68
+ this.clearCacheEntry('GET:/subscriptions/current');
69
+ return result;
66
70
  }, 'subscribe');
67
71
  }
68
72
  /**
@@ -70,10 +74,12 @@ function OxyServicesFeaturesMixin(Base) {
70
74
  */
71
75
  async subscribeToFeature(featureId, paymentMethodId) {
72
76
  return this.withAuthRetry(async () => {
73
- return await this.makeRequest('POST', '/subscriptions/features/subscribe', {
77
+ const result = await this.makeRequest('POST', '/subscriptions/features/subscribe', {
74
78
  featureId,
75
79
  paymentMethodId,
76
80
  }, { cache: false });
81
+ this.clearCacheEntry('GET:/subscriptions/current');
82
+ return result;
77
83
  }, 'subscribeToFeature');
78
84
  }
79
85
  /**
@@ -84,6 +90,7 @@ function OxyServicesFeaturesMixin(Base) {
84
90
  await this.makeRequest('POST', `/subscriptions/${subscriptionId}/cancel`, undefined, {
85
91
  cache: false,
86
92
  });
93
+ this.clearCacheEntry('GET:/subscriptions/current');
87
94
  }, 'cancelSubscription');
88
95
  }
89
96
  /**
@@ -94,6 +101,7 @@ function OxyServicesFeaturesMixin(Base) {
94
101
  await this.makeRequest('POST', `/subscriptions/${subscriptionId}/reactivate`, undefined, {
95
102
  cache: false,
96
103
  });
104
+ this.clearCacheEntry('GET:/subscriptions/current');
97
105
  }, 'reactivateSubscription');
98
106
  }
99
107
  /**
@@ -142,42 +150,62 @@ function OxyServicesFeaturesMixin(Base) {
142
150
  }, 'getCollections');
143
151
  }
144
152
  /**
145
- * Save an item
153
+ * Save an item.
154
+ *
155
+ * Busts the cached own saved-items list (`GET /saves`, ~short TTL) so a
156
+ * follow-up `getSavedItems()` observes the new item. The `userId`-scoped
157
+ * variant (`/users/<id>/saves`) is another user's list and is not
158
+ * affected by the caller's own save.
146
159
  */
147
160
  async saveItem(itemId, itemType, collectionId) {
148
161
  return this.withAuthRetry(async () => {
149
- return await this.makeRequest('POST', '/saves', {
162
+ const result = await this.makeRequest('POST', '/saves', {
150
163
  itemId,
151
164
  itemType,
152
165
  collectionId,
153
166
  }, { cache: false });
167
+ this.clearCacheEntry('GET:/saves');
168
+ return result;
154
169
  }, 'saveItem');
155
170
  }
156
171
  /**
157
- * Remove an item from saves
172
+ * Remove an item from saves.
173
+ *
174
+ * Busts the cached own saved-items list so the removed item is gone on
175
+ * the next read (see `saveItem`).
158
176
  */
159
177
  async removeSavedItem(saveId) {
160
178
  return this.withAuthRetry(async () => {
161
179
  await this.makeRequest('DELETE', `/saves/${saveId}`, undefined, { cache: false });
180
+ this.clearCacheEntry('GET:/saves');
162
181
  }, 'removeSavedItem');
163
182
  }
164
183
  /**
165
- * Create a collection
184
+ * Create a collection.
185
+ *
186
+ * Busts the cached own collections list (`GET /collections`) so the new
187
+ * collection appears on the next read.
166
188
  */
167
189
  async createCollection(name, description) {
168
190
  return this.withAuthRetry(async () => {
169
- return await this.makeRequest('POST', '/collections', {
191
+ const result = await this.makeRequest('POST', '/collections', {
170
192
  name,
171
193
  description,
172
194
  }, { cache: false });
195
+ this.clearCacheEntry('GET:/collections');
196
+ return result;
173
197
  }, 'createCollection');
174
198
  }
175
199
  /**
176
- * Delete a collection
200
+ * Delete a collection.
201
+ *
202
+ * Busts the cached own collections list so the deleted collection is
203
+ * gone on the next read (see `createCollection`).
177
204
  */
178
205
  async deleteCollection(collectionId) {
179
206
  return this.withAuthRetry(async () => {
180
207
  await this.makeRequest('DELETE', `/collections/${collectionId}`, undefined, { cache: false });
208
+ this.clearCacheEntry('GET:/collections');
181
209
  }, 'deleteCollection');
182
210
  }
183
211
  // ==================
@@ -218,19 +246,28 @@ function OxyServicesFeaturesMixin(Base) {
218
246
  }, 'getUserHistory');
219
247
  }
220
248
  /**
221
- * Clear user history
249
+ * Clear user history.
250
+ *
251
+ * `getUserHistory` caches per (limit, offset) page, so its cache key
252
+ * carries serialized params (`GET:/history` and `GET:/history:<params>`).
253
+ * A prefix sweep busts every cached page of the own history at once.
222
254
  */
223
255
  async clearUserHistory() {
224
256
  return this.withAuthRetry(async () => {
225
257
  await this.makeRequest('DELETE', '/history', undefined, { cache: false });
258
+ this.clearCacheByPrefix('GET:/history');
226
259
  }, 'clearUserHistory');
227
260
  }
228
261
  /**
229
- * Delete a history item
262
+ * Delete a history item.
263
+ *
264
+ * Busts every cached page of the own history (see `clearUserHistory`)
265
+ * so the removed item no longer appears on the next read.
230
266
  */
231
267
  async deleteHistoryItem(itemId) {
232
268
  return this.withAuthRetry(async () => {
233
269
  await this.makeRequest('DELETE', `/history/${itemId}`, undefined, { cache: false });
270
+ this.clearCacheByPrefix('GET:/history');
234
271
  }, 'deleteHistoryItem');
235
272
  }
236
273
  // ==================
@@ -10,13 +10,17 @@ function OxyServicesManagedAccountsMixin(Base) {
10
10
  * Create a new managed account (sub-account).
11
11
  *
12
12
  * The server creates a User document with `isManagedAccount: true` and links
13
- * it to the authenticated user as owner.
13
+ * it to the authenticated user as owner. Invalidates the cached
14
+ * `GET /managed-accounts` list (~2-minute TTL, identity-scoped) so the next
15
+ * read includes the newly created account.
14
16
  */
15
17
  async createManagedAccount(data) {
16
18
  try {
17
- return await this.makeRequest('POST', '/managed-accounts', data, {
19
+ const result = await this.makeRequest('POST', '/managed-accounts', data, {
18
20
  cache: false,
19
21
  });
22
+ this.clearCacheEntry('GET:/managed-accounts');
23
+ return result;
20
24
  }
21
25
  catch (error) {
22
26
  throw this.handleError(error);
@@ -53,12 +57,19 @@ function OxyServicesManagedAccountsMixin(Base) {
53
57
  /**
54
58
  * Update a managed account's profile data.
55
59
  * Requires owner or admin role.
60
+ *
61
+ * Invalidates both the cached detail (`GET /managed-accounts/<id>`) and the
62
+ * cached list (`GET /managed-accounts`, which embeds account profile data)
63
+ * so neither serves the pre-update snapshot within their ~2-minute TTL.
56
64
  */
57
65
  async updateManagedAccount(accountId, data) {
58
66
  try {
59
- return await this.makeRequest('PUT', `/managed-accounts/${accountId}`, data, {
67
+ const result = await this.makeRequest('PUT', `/managed-accounts/${accountId}`, data, {
60
68
  cache: false,
61
69
  });
70
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
71
+ this.clearCacheEntry('GET:/managed-accounts');
72
+ return result;
62
73
  }
63
74
  catch (error) {
64
75
  throw this.handleError(error);
@@ -67,12 +78,17 @@ function OxyServicesManagedAccountsMixin(Base) {
67
78
  /**
68
79
  * Delete a managed account permanently.
69
80
  * Requires owner role.
81
+ *
82
+ * Invalidates the cached detail and list responses so the deleted account
83
+ * is not served from cache.
70
84
  */
71
85
  async deleteManagedAccount(accountId) {
72
86
  try {
73
87
  await this.makeRequest('DELETE', `/managed-accounts/${accountId}`, undefined, {
74
88
  cache: false,
75
89
  });
90
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
91
+ this.clearCacheEntry('GET:/managed-accounts');
76
92
  }
77
93
  catch (error) {
78
94
  throw this.handleError(error);
@@ -82,6 +98,9 @@ function OxyServicesManagedAccountsMixin(Base) {
82
98
  * Add a manager to a managed account.
83
99
  * Requires owner or admin role on the account.
84
100
  *
101
+ * Mutates the account's `managers[]`, which is returned by the detail and
102
+ * list reads — invalidate both so they re-fetch the updated manager set.
103
+ *
85
104
  * @param accountId - The managed account to add the manager to
86
105
  * @param userId - The user to grant management access
87
106
  * @param role - The role to assign: 'admin' or 'editor'
@@ -91,6 +110,8 @@ function OxyServicesManagedAccountsMixin(Base) {
91
110
  await this.makeRequest('POST', `/managed-accounts/${accountId}/managers`, { userId, role }, {
92
111
  cache: false,
93
112
  });
113
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
114
+ this.clearCacheEntry('GET:/managed-accounts');
94
115
  }
95
116
  catch (error) {
96
117
  throw this.handleError(error);
@@ -100,6 +121,9 @@ function OxyServicesManagedAccountsMixin(Base) {
100
121
  * Remove a manager from a managed account.
101
122
  * Requires owner role.
102
123
  *
124
+ * Invalidates the detail and list responses so the updated `managers[]`
125
+ * is observed on the next read (see `addManager`).
126
+ *
103
127
  * @param accountId - The managed account
104
128
  * @param userId - The manager to remove
105
129
  */
@@ -108,6 +132,8 @@ function OxyServicesManagedAccountsMixin(Base) {
108
132
  await this.makeRequest('DELETE', `/managed-accounts/${accountId}/managers/${userId}`, undefined, {
109
133
  cache: false,
110
134
  });
135
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
136
+ this.clearCacheEntry('GET:/managed-accounts');
111
137
  }
112
138
  catch (error) {
113
139
  throw this.handleError(error);
@@ -54,7 +54,13 @@ function OxyServicesPrivacyMixin(Base) {
54
54
  }
55
55
  }
56
56
  /**
57
- * Block a user
57
+ * Block a user.
58
+ *
59
+ * Invalidates the cached `GET /privacy/blocked` response after the write.
60
+ * `getBlockedUsers` caches for ~1 minute (identity-scoped); without busting
61
+ * that entry, a consumer that re-reads the blocked list within the TTL
62
+ * window would not see the user it just blocked. `clearCacheEntry` deletes
63
+ * every identity-scoped variant of the key.
58
64
  * @param userId - The user ID to block
59
65
  * @returns Success message
60
66
  */
@@ -63,16 +69,21 @@ function OxyServicesPrivacyMixin(Base) {
63
69
  if (!userId) {
64
70
  throw new Error('User ID is required');
65
71
  }
66
- return await this.makeRequest('POST', `/privacy/blocked/${userId}`, undefined, {
72
+ const result = await this.makeRequest('POST', `/privacy/blocked/${userId}`, undefined, {
67
73
  cache: false,
68
74
  });
75
+ this.clearCacheEntry('GET:/privacy/blocked');
76
+ return result;
69
77
  }
70
78
  catch (error) {
71
79
  throw this.handleError(error);
72
80
  }
73
81
  }
74
82
  /**
75
- * Unblock a user
83
+ * Unblock a user.
84
+ *
85
+ * Busts the cached `GET /privacy/blocked` response so a remount reads the
86
+ * fresh list without the just-unblocked user (see `blockUser`).
76
87
  * @param userId - The user ID to unblock
77
88
  * @returns Success message
78
89
  */
@@ -81,9 +92,11 @@ function OxyServicesPrivacyMixin(Base) {
81
92
  if (!userId) {
82
93
  throw new Error('User ID is required');
83
94
  }
84
- return await this.makeRequest('DELETE', `/privacy/blocked/${userId}`, undefined, {
95
+ const result = await this.makeRequest('DELETE', `/privacy/blocked/${userId}`, undefined, {
85
96
  cache: false,
86
97
  });
98
+ this.clearCacheEntry('GET:/privacy/blocked');
99
+ return result;
87
100
  }
88
101
  catch (error) {
89
102
  throw this.handleError(error);
@@ -116,7 +129,13 @@ function OxyServicesPrivacyMixin(Base) {
116
129
  }
117
130
  }
118
131
  /**
119
- * Restrict a user (limit their interactions without fully blocking)
132
+ * Restrict a user (limit their interactions without fully blocking).
133
+ *
134
+ * Invalidates the cached `GET /privacy/restricted` response after the write.
135
+ * `getRestrictedUsers` caches for ~1 minute (identity-scoped); without
136
+ * busting that entry, a consumer that re-reads the restricted list within
137
+ * the TTL window would not see the user it just restricted.
138
+ * `clearCacheEntry` deletes every identity-scoped variant of the key.
120
139
  * @param userId - The user ID to restrict
121
140
  * @returns Success message
122
141
  */
@@ -125,16 +144,21 @@ function OxyServicesPrivacyMixin(Base) {
125
144
  if (!userId) {
126
145
  throw new Error('User ID is required');
127
146
  }
128
- return await this.makeRequest('POST', `/privacy/restricted/${userId}`, undefined, {
147
+ const result = await this.makeRequest('POST', `/privacy/restricted/${userId}`, undefined, {
129
148
  cache: false,
130
149
  });
150
+ this.clearCacheEntry('GET:/privacy/restricted');
151
+ return result;
131
152
  }
132
153
  catch (error) {
133
154
  throw this.handleError(error);
134
155
  }
135
156
  }
136
157
  /**
137
- * Unrestrict a user
158
+ * Unrestrict a user.
159
+ *
160
+ * Busts the cached `GET /privacy/restricted` response so a remount reads the
161
+ * fresh list without the just-unrestricted user (see `restrictUser`).
138
162
  * @param userId - The user ID to unrestrict
139
163
  * @returns Success message
140
164
  */
@@ -143,9 +167,11 @@ function OxyServicesPrivacyMixin(Base) {
143
167
  if (!userId) {
144
168
  throw new Error('User ID is required');
145
169
  }
146
- return await this.makeRequest('DELETE', `/privacy/restricted/${userId}`, undefined, {
170
+ const result = await this.makeRequest('DELETE', `/privacy/restricted/${userId}`, undefined, {
147
171
  cache: false,
148
172
  });
173
+ this.clearCacheEntry('GET:/privacy/restricted');
174
+ return result;
149
175
  }
150
176
  catch (error) {
151
177
  throw this.handleError(error);
@@ -111,9 +111,13 @@ function OxyServicesTopicsMixin(Base) {
111
111
  */
112
112
  async updateTopicMetadata(slug, data) {
113
113
  try {
114
- return await this.makeRequest('PATCH', `/topics/${slug}`, data, {
114
+ const result = await this.makeRequest('PATCH', `/topics/${slug}`, data, {
115
115
  cache: false,
116
116
  });
117
+ // Bust the cached topic detail so `getTopicBySlug(slug)` reflects the
118
+ // updated metadata immediately (it caches at the LONG TTL).
119
+ this.clearCacheEntry(`GET:/topics/${slug}`);
120
+ return result;
117
121
  }
118
122
  catch (error) {
119
123
  throw this.handleError(error);
@@ -351,16 +351,25 @@ function OxyServicesUserMixin(Base) {
351
351
  }
352
352
  }
353
353
  /**
354
- * Update privacy settings
354
+ * Update privacy settings.
355
+ *
356
+ * Invalidates the cached `GET /privacy/<id>/privacy` response (the exact
357
+ * key `getPrivacySettings` reads, scoped to the same `id`) after the write.
358
+ * `getPrivacySettings` caches for ~2 minutes (identity-scoped); without
359
+ * busting that entry, a follow-up read within the TTL window returns the
360
+ * pre-update settings. `clearCacheEntry` deletes every identity-scoped
361
+ * variant of the key.
355
362
  * @param settings - Partial privacy settings object
356
363
  * @param userId - The user ID (defaults to current user)
357
364
  */
358
365
  async updatePrivacySettings(settings, userId) {
359
366
  try {
360
367
  const id = userId || (await this.getCurrentUser()).id;
361
- return await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
368
+ const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
362
369
  cache: false,
363
370
  });
371
+ this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
372
+ return result;
364
373
  }
365
374
  catch (error) {
366
375
  throw this.handleError(error);