@oxyhq/core 3.8.1 → 3.8.2

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