@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
@@ -347,6 +347,9 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
347
347
  data,
348
348
  { cache: false },
349
349
  );
350
+ // Bust every cached application list (unscoped + per-workspace) so the
351
+ // new application appears on the next `getApplications()` read.
352
+ this._invalidateApplicationLists();
350
353
  return res.application;
351
354
  } catch (error) {
352
355
  throw this.handleError(error);
@@ -387,6 +390,10 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
387
390
  data,
388
391
  { cache: false },
389
392
  );
393
+ // Bust the cached detail and every list (which embeds application
394
+ // fields) so neither serves the pre-update snapshot.
395
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
396
+ this._invalidateApplicationLists();
390
397
  return res.application;
391
398
  } catch (error) {
392
399
  throw this.handleError(error);
@@ -399,12 +406,18 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
399
406
  */
400
407
  async deleteApplication(applicationId: string): Promise<ApplicationSuccessResult> {
401
408
  try {
402
- return await this.makeRequest<ApplicationSuccessResult>(
409
+ const result = await this.makeRequest<ApplicationSuccessResult>(
403
410
  'DELETE',
404
411
  `/applications/${applicationId}`,
405
412
  undefined,
406
413
  { cache: false },
407
414
  );
415
+ // Bust every cached representation of the deleted application.
416
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
417
+ this.clearCacheEntry(`GET:/applications/${applicationId}/members`);
418
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
419
+ this._invalidateApplicationLists();
420
+ return result;
408
421
  } catch (error) {
409
422
  throw this.handleError(error);
410
423
  }
@@ -446,6 +459,7 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
446
459
  data,
447
460
  { cache: false },
448
461
  );
462
+ this._invalidateApplicationMembership(applicationId);
449
463
  return res.member;
450
464
  } catch (error) {
451
465
  throw this.handleError(error);
@@ -470,6 +484,7 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
470
484
  data,
471
485
  { cache: false },
472
486
  );
487
+ this._invalidateApplicationMembership(applicationId);
473
488
  return res.member;
474
489
  } catch (error) {
475
490
  throw this.handleError(error);
@@ -486,12 +501,14 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
486
501
  memberId: string,
487
502
  ): Promise<ApplicationSuccessResult> {
488
503
  try {
489
- return await this.makeRequest<ApplicationSuccessResult>(
504
+ const result = await this.makeRequest<ApplicationSuccessResult>(
490
505
  'DELETE',
491
506
  `/applications/${applicationId}/members/${memberId}`,
492
507
  undefined,
493
508
  { cache: false },
494
509
  );
510
+ this._invalidateApplicationMembership(applicationId);
511
+ return result;
495
512
  } catch (error) {
496
513
  throw this.handleError(error);
497
514
  }
@@ -508,12 +525,17 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
508
525
  data: TransferApplicationOwnershipInput,
509
526
  ): Promise<ApplicationSuccessResult> {
510
527
  try {
511
- return await this.makeRequest<ApplicationSuccessResult>(
528
+ const result = await this.makeRequest<ApplicationSuccessResult>(
512
529
  'POST',
513
530
  `/applications/${applicationId}/transfer-ownership`,
514
531
  data,
515
532
  { cache: false },
516
533
  );
534
+ // Ownership change alters roles in the member list AND the detail, and
535
+ // can change which applications the caller "owns" in the list view.
536
+ this._invalidateApplicationMembership(applicationId);
537
+ this._invalidateApplicationLists();
538
+ return result;
517
539
  } catch (error) {
518
540
  throw this.handleError(error);
519
541
  }
@@ -548,12 +570,14 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
548
570
  data: CreateApplicationCredentialInput,
549
571
  ): Promise<ApplicationCredentialWithSecret> {
550
572
  try {
551
- return await this.makeRequest<ApplicationCredentialWithSecret>(
573
+ const result = await this.makeRequest<ApplicationCredentialWithSecret>(
552
574
  'POST',
553
575
  `/applications/${applicationId}/credentials`,
554
576
  data,
555
577
  { cache: false },
556
578
  );
579
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
580
+ return result;
557
581
  } catch (error) {
558
582
  throw this.handleError(error);
559
583
  }
@@ -572,12 +596,16 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
572
596
  credentialId: string,
573
597
  ): Promise<RotateApplicationCredentialResult> {
574
598
  try {
575
- return await this.makeRequest<RotateApplicationCredentialResult>(
599
+ const result = await this.makeRequest<RotateApplicationCredentialResult>(
576
600
  'POST',
577
601
  `/applications/${applicationId}/credentials/${credentialId}/rotate`,
578
602
  undefined,
579
603
  { cache: false },
580
604
  );
605
+ // Rotation changes credential status/audit fields surfaced by the
606
+ // credentials list (`rotatedFrom`, grace window, new active credential).
607
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
608
+ return result;
581
609
  } catch (error) {
582
610
  throw this.handleError(error);
583
611
  }
@@ -594,12 +622,15 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
594
622
  credentialId: string,
595
623
  ): Promise<ApplicationSuccessResult> {
596
624
  try {
597
- return await this.makeRequest<ApplicationSuccessResult>(
625
+ const result = await this.makeRequest<ApplicationSuccessResult>(
598
626
  'DELETE',
599
627
  `/applications/${applicationId}/credentials/${credentialId}`,
600
628
  undefined,
601
629
  { cache: false },
602
630
  );
631
+ // Revocation flips the credential's status in the cached list.
632
+ this.clearCacheEntry(`GET:/applications/${applicationId}/credentials`);
633
+ return result;
603
634
  } catch (error) {
604
635
  throw this.handleError(error);
605
636
  }
@@ -625,5 +656,39 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
625
656
  throw this.handleError(error);
626
657
  }
627
658
  }
659
+
660
+ /**
661
+ * Bust every cached application list. `getApplications(workspaceId?)` keys
662
+ * the unscoped list as `GET:/applications` and each workspace-scoped list as
663
+ * `GET:/applications?workspaceId=<id>` (the query string is part of the URL
664
+ * path). A change to list membership (create/delete/ownership transfer)
665
+ * invalidates all of them, so we clear the unscoped entry plus every
666
+ * `?workspaceId=` variant via a prefix sweep. The prefix `GET:/applications?`
667
+ * matches only the query-string list variants, never the `GET:/applications/<id>…`
668
+ * detail/sub-resource keys.
669
+ *
670
+ * Internal helper (leading underscore); not part of the supported public
671
+ * surface. Public rather than `private` because mixins compose into an
672
+ * exported anonymous class, where TypeScript cannot represent a private
673
+ * member in the emitted declaration file (TS4094).
674
+ */
675
+ _invalidateApplicationLists(): void {
676
+ this.clearCacheEntry('GET:/applications');
677
+ this.clearCacheByPrefix('GET:/applications?');
678
+ }
679
+
680
+ /**
681
+ * Bust the cached member list and detail for an application after a
682
+ * membership mutation. The member list (`getApplicationMembers`) and the
683
+ * detail (`getApplication`, which can embed member counts) both go stale
684
+ * when the member set or a member's role changes.
685
+ *
686
+ * Internal helper (leading underscore); see `_invalidateApplicationLists`
687
+ * for why this is public rather than `private`.
688
+ */
689
+ _invalidateApplicationMembership(applicationId: string): void {
690
+ this.clearCacheEntry(`GET:/applications/${applicationId}/members`);
691
+ this.clearCacheEntry(`GET:/applications/${applicationId}`);
692
+ }
628
693
  };
629
694
  }
@@ -345,7 +345,10 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
345
345
  */
346
346
  async assetRestore(fileId: string): Promise<any> {
347
347
  try {
348
- return await this.makeRequest('POST', `/assets/${fileId}/restore`, undefined, { cache: false });
348
+ const result = await this.makeRequest('POST', `/assets/${fileId}/restore`, undefined, { cache: false });
349
+ // The asset metadata (trash state) changed — bust its cached read.
350
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
351
+ return result;
349
352
  } catch (error) {
350
353
  throw this.handleError(error);
351
354
  }
@@ -357,7 +360,11 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
357
360
  async assetDelete(fileId: string, force: boolean = false): Promise<any> {
358
361
  try {
359
362
  const params: any = force ? { force: 'true' } : undefined;
360
- return await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
363
+ const result = await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
364
+ // Bust the cached metadata and every cached URL variant for the asset.
365
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
366
+ this.clearCacheByPrefix(`GET:/assets/${fileId}/url`);
367
+ return result;
361
368
  } catch (error) {
362
369
  throw this.handleError(error);
363
370
  }
@@ -380,9 +387,15 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
380
387
  */
381
388
  async assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<any> {
382
389
  try {
383
- return await this.makeRequest('PATCH', `/assets/${fileId}/visibility`, {
390
+ const result = await this.makeRequest('PATCH', `/assets/${fileId}/visibility`, {
384
391
  visibility
385
392
  }, { cache: false });
393
+ // Visibility changes both the asset metadata and the resolved URL
394
+ // (public CDN vs signed). Bust the metadata read and every cached URL
395
+ // variant (keyed on variant/expiresIn params).
396
+ this.clearCacheEntry(`GET:/assets/${fileId}`);
397
+ this.clearCacheByPrefix(`GET:/assets/${fileId}/url`);
398
+ return result;
386
399
  } catch (error) {
387
400
  throw this.handleError(error);
388
401
  }
@@ -159,10 +159,14 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
159
159
  */
160
160
  async subscribe(planId: string, paymentMethodId?: string): Promise<SubscriptionResult> {
161
161
  return this.withAuthRetry(async () => {
162
- return await this.makeRequest<SubscriptionResult>('POST', '/subscriptions/subscribe', {
162
+ const result = await this.makeRequest<SubscriptionResult>('POST', '/subscriptions/subscribe', {
163
163
  planId,
164
164
  paymentMethodId,
165
165
  }, { cache: false });
166
+ // The current subscription changed — bust its cached read so
167
+ // `getCurrentSubscription()` reflects the new plan immediately.
168
+ this.clearCacheEntry('GET:/subscriptions/current');
169
+ return result;
166
170
  }, 'subscribe');
167
171
  }
168
172
 
@@ -171,10 +175,12 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
171
175
  */
172
176
  async subscribeToFeature(featureId: string, paymentMethodId?: string): Promise<SubscriptionResult> {
173
177
  return this.withAuthRetry(async () => {
174
- return await this.makeRequest<SubscriptionResult>('POST', '/subscriptions/features/subscribe', {
178
+ const result = await this.makeRequest<SubscriptionResult>('POST', '/subscriptions/features/subscribe', {
175
179
  featureId,
176
180
  paymentMethodId,
177
181
  }, { cache: false });
182
+ this.clearCacheEntry('GET:/subscriptions/current');
183
+ return result;
178
184
  }, 'subscribeToFeature');
179
185
  }
180
186
 
@@ -186,6 +192,7 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
186
192
  await this.makeRequest('POST', `/subscriptions/${subscriptionId}/cancel`, undefined, {
187
193
  cache: false,
188
194
  });
195
+ this.clearCacheEntry('GET:/subscriptions/current');
189
196
  }, 'cancelSubscription');
190
197
  }
191
198
 
@@ -197,6 +204,7 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
197
204
  await this.makeRequest('POST', `/subscriptions/${subscriptionId}/reactivate`, undefined, {
198
205
  cache: false,
199
206
  });
207
+ this.clearCacheEntry('GET:/subscriptions/current');
200
208
  }, 'reactivateSubscription');
201
209
  }
202
210
 
@@ -248,45 +256,65 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
248
256
  }
249
257
 
250
258
  /**
251
- * Save an item
259
+ * Save an item.
260
+ *
261
+ * Busts the cached own saved-items list (`GET /saves`, ~short TTL) so a
262
+ * follow-up `getSavedItems()` observes the new item. The `userId`-scoped
263
+ * variant (`/users/<id>/saves`) is another user's list and is not
264
+ * affected by the caller's own save.
252
265
  */
253
266
  async saveItem(itemId: string, itemType: string, collectionId?: string): Promise<SavedItem> {
254
267
  return this.withAuthRetry(async () => {
255
- return await this.makeRequest<SavedItem>('POST', '/saves', {
268
+ const result = await this.makeRequest<SavedItem>('POST', '/saves', {
256
269
  itemId,
257
270
  itemType,
258
271
  collectionId,
259
272
  }, { cache: false });
273
+ this.clearCacheEntry('GET:/saves');
274
+ return result;
260
275
  }, 'saveItem');
261
276
  }
262
277
 
263
278
  /**
264
- * Remove an item from saves
279
+ * Remove an item from saves.
280
+ *
281
+ * Busts the cached own saved-items list so the removed item is gone on
282
+ * the next read (see `saveItem`).
265
283
  */
266
284
  async removeSavedItem(saveId: string): Promise<void> {
267
285
  return this.withAuthRetry(async () => {
268
286
  await this.makeRequest('DELETE', `/saves/${saveId}`, undefined, { cache: false });
287
+ this.clearCacheEntry('GET:/saves');
269
288
  }, 'removeSavedItem');
270
289
  }
271
290
 
272
291
  /**
273
- * Create a collection
292
+ * Create a collection.
293
+ *
294
+ * Busts the cached own collections list (`GET /collections`) so the new
295
+ * collection appears on the next read.
274
296
  */
275
297
  async createCollection(name: string, description?: string): Promise<Collection> {
276
298
  return this.withAuthRetry(async () => {
277
- return await this.makeRequest<Collection>('POST', '/collections', {
299
+ const result = await this.makeRequest<Collection>('POST', '/collections', {
278
300
  name,
279
301
  description,
280
302
  }, { cache: false });
303
+ this.clearCacheEntry('GET:/collections');
304
+ return result;
281
305
  }, 'createCollection');
282
306
  }
283
307
 
284
308
  /**
285
- * Delete a collection
309
+ * Delete a collection.
310
+ *
311
+ * Busts the cached own collections list so the deleted collection is
312
+ * gone on the next read (see `createCollection`).
286
313
  */
287
314
  async deleteCollection(collectionId: string): Promise<void> {
288
315
  return this.withAuthRetry(async () => {
289
316
  await this.makeRequest('DELETE', `/collections/${collectionId}`, undefined, { cache: false });
317
+ this.clearCacheEntry('GET:/collections');
290
318
  }, 'deleteCollection');
291
319
  }
292
320
 
@@ -330,20 +358,29 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
330
358
  }
331
359
 
332
360
  /**
333
- * Clear user history
361
+ * Clear user history.
362
+ *
363
+ * `getUserHistory` caches per (limit, offset) page, so its cache key
364
+ * carries serialized params (`GET:/history` and `GET:/history:<params>`).
365
+ * A prefix sweep busts every cached page of the own history at once.
334
366
  */
335
367
  async clearUserHistory(): Promise<void> {
336
368
  return this.withAuthRetry(async () => {
337
369
  await this.makeRequest('DELETE', '/history', undefined, { cache: false });
370
+ this.clearCacheByPrefix('GET:/history');
338
371
  }, 'clearUserHistory');
339
372
  }
340
373
 
341
374
  /**
342
- * Delete a history item
375
+ * Delete a history item.
376
+ *
377
+ * Busts every cached page of the own history (see `clearUserHistory`)
378
+ * so the removed item no longer appears on the next read.
343
379
  */
344
380
  async deleteHistoryItem(itemId: string): Promise<void> {
345
381
  return this.withAuthRetry(async () => {
346
382
  await this.makeRequest('DELETE', `/history/${itemId}`, undefined, { cache: false });
383
+ this.clearCacheByPrefix('GET:/history');
347
384
  }, 'deleteHistoryItem');
348
385
  }
349
386
 
@@ -41,13 +41,17 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
41
41
  * Create a new managed account (sub-account).
42
42
  *
43
43
  * The server creates a User document with `isManagedAccount: true` and links
44
- * it to the authenticated user as owner.
44
+ * it to the authenticated user as owner. Invalidates the cached
45
+ * `GET /managed-accounts` list (~2-minute TTL, identity-scoped) so the next
46
+ * read includes the newly created account.
45
47
  */
46
48
  async createManagedAccount(data: CreateManagedAccountInput): Promise<ManagedAccount> {
47
49
  try {
48
- return await this.makeRequest<ManagedAccount>('POST', '/managed-accounts', data, {
50
+ const result = await this.makeRequest<ManagedAccount>('POST', '/managed-accounts', data, {
49
51
  cache: false,
50
52
  });
53
+ this.clearCacheEntry('GET:/managed-accounts');
54
+ return result;
51
55
  } catch (error) {
52
56
  throw this.handleError(error);
53
57
  }
@@ -84,12 +88,19 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
84
88
  /**
85
89
  * Update a managed account's profile data.
86
90
  * Requires owner or admin role.
91
+ *
92
+ * Invalidates both the cached detail (`GET /managed-accounts/<id>`) and the
93
+ * cached list (`GET /managed-accounts`, which embeds account profile data)
94
+ * so neither serves the pre-update snapshot within their ~2-minute TTL.
87
95
  */
88
96
  async updateManagedAccount(accountId: string, data: Partial<CreateManagedAccountInput>): Promise<ManagedAccount> {
89
97
  try {
90
- return await this.makeRequest<ManagedAccount>('PUT', `/managed-accounts/${accountId}`, data, {
98
+ const result = await this.makeRequest<ManagedAccount>('PUT', `/managed-accounts/${accountId}`, data, {
91
99
  cache: false,
92
100
  });
101
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
102
+ this.clearCacheEntry('GET:/managed-accounts');
103
+ return result;
93
104
  } catch (error) {
94
105
  throw this.handleError(error);
95
106
  }
@@ -98,12 +109,17 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
98
109
  /**
99
110
  * Delete a managed account permanently.
100
111
  * Requires owner role.
112
+ *
113
+ * Invalidates the cached detail and list responses so the deleted account
114
+ * is not served from cache.
101
115
  */
102
116
  async deleteManagedAccount(accountId: string): Promise<void> {
103
117
  try {
104
118
  await this.makeRequest<void>('DELETE', `/managed-accounts/${accountId}`, undefined, {
105
119
  cache: false,
106
120
  });
121
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
122
+ this.clearCacheEntry('GET:/managed-accounts');
107
123
  } catch (error) {
108
124
  throw this.handleError(error);
109
125
  }
@@ -113,6 +129,9 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
113
129
  * Add a manager to a managed account.
114
130
  * Requires owner or admin role on the account.
115
131
  *
132
+ * Mutates the account's `managers[]`, which is returned by the detail and
133
+ * list reads — invalidate both so they re-fetch the updated manager set.
134
+ *
116
135
  * @param accountId - The managed account to add the manager to
117
136
  * @param userId - The user to grant management access
118
137
  * @param role - The role to assign: 'admin' or 'editor'
@@ -122,6 +141,8 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
122
141
  await this.makeRequest<void>('POST', `/managed-accounts/${accountId}/managers`, { userId, role }, {
123
142
  cache: false,
124
143
  });
144
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
145
+ this.clearCacheEntry('GET:/managed-accounts');
125
146
  } catch (error) {
126
147
  throw this.handleError(error);
127
148
  }
@@ -131,6 +152,9 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
131
152
  * Remove a manager from a managed account.
132
153
  * Requires owner role.
133
154
  *
155
+ * Invalidates the detail and list responses so the updated `managers[]`
156
+ * is observed on the next read (see `addManager`).
157
+ *
134
158
  * @param accountId - The managed account
135
159
  * @param userId - The manager to remove
136
160
  */
@@ -139,6 +163,8 @@ export function OxyServicesManagedAccountsMixin<T extends typeof OxyServicesBase
139
163
  await this.makeRequest<void>('DELETE', `/managed-accounts/${accountId}/managers/${userId}`, undefined, {
140
164
  cache: false,
141
165
  });
166
+ this.clearCacheEntry(`GET:/managed-accounts/${accountId}`);
167
+ this.clearCacheEntry('GET:/managed-accounts');
142
168
  } catch (error) {
143
169
  throw this.handleError(error);
144
170
  }
@@ -63,7 +63,13 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
63
63
  }
64
64
 
65
65
  /**
66
- * Block a user
66
+ * Block a user.
67
+ *
68
+ * Invalidates the cached `GET /privacy/blocked` response after the write.
69
+ * `getBlockedUsers` caches for ~1 minute (identity-scoped); without busting
70
+ * that entry, a consumer that re-reads the blocked list within the TTL
71
+ * window would not see the user it just blocked. `clearCacheEntry` deletes
72
+ * every identity-scoped variant of the key.
67
73
  * @param userId - The user ID to block
68
74
  * @returns Success message
69
75
  */
@@ -72,16 +78,21 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
72
78
  if (!userId) {
73
79
  throw new Error('User ID is required');
74
80
  }
75
- return await this.makeRequest<{ message: string }>('POST', `/privacy/blocked/${userId}`, undefined, {
81
+ const result = await this.makeRequest<{ message: string }>('POST', `/privacy/blocked/${userId}`, undefined, {
76
82
  cache: false,
77
83
  });
84
+ this.clearCacheEntry('GET:/privacy/blocked');
85
+ return result;
78
86
  } catch (error) {
79
87
  throw this.handleError(error);
80
88
  }
81
89
  }
82
90
 
83
91
  /**
84
- * Unblock a user
92
+ * Unblock a user.
93
+ *
94
+ * Busts the cached `GET /privacy/blocked` response so a remount reads the
95
+ * fresh list without the just-unblocked user (see `blockUser`).
85
96
  * @param userId - The user ID to unblock
86
97
  * @returns Success message
87
98
  */
@@ -90,9 +101,11 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
90
101
  if (!userId) {
91
102
  throw new Error('User ID is required');
92
103
  }
93
- return await this.makeRequest<{ message: string }>('DELETE', `/privacy/blocked/${userId}`, undefined, {
104
+ const result = await this.makeRequest<{ message: string }>('DELETE', `/privacy/blocked/${userId}`, undefined, {
94
105
  cache: false,
95
106
  });
107
+ this.clearCacheEntry('GET:/privacy/blocked');
108
+ return result;
96
109
  } catch (error) {
97
110
  throw this.handleError(error);
98
111
  }
@@ -131,7 +144,13 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
131
144
  }
132
145
 
133
146
  /**
134
- * Restrict a user (limit their interactions without fully blocking)
147
+ * Restrict a user (limit their interactions without fully blocking).
148
+ *
149
+ * Invalidates the cached `GET /privacy/restricted` response after the write.
150
+ * `getRestrictedUsers` caches for ~1 minute (identity-scoped); without
151
+ * busting that entry, a consumer that re-reads the restricted list within
152
+ * the TTL window would not see the user it just restricted.
153
+ * `clearCacheEntry` deletes every identity-scoped variant of the key.
135
154
  * @param userId - The user ID to restrict
136
155
  * @returns Success message
137
156
  */
@@ -140,16 +159,21 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
140
159
  if (!userId) {
141
160
  throw new Error('User ID is required');
142
161
  }
143
- return await this.makeRequest<{ message: string }>('POST', `/privacy/restricted/${userId}`, undefined, {
162
+ const result = await this.makeRequest<{ message: string }>('POST', `/privacy/restricted/${userId}`, undefined, {
144
163
  cache: false,
145
164
  });
165
+ this.clearCacheEntry('GET:/privacy/restricted');
166
+ return result;
146
167
  } catch (error) {
147
168
  throw this.handleError(error);
148
169
  }
149
170
  }
150
171
 
151
172
  /**
152
- * Unrestrict a user
173
+ * Unrestrict a user.
174
+ *
175
+ * Busts the cached `GET /privacy/restricted` response so a remount reads the
176
+ * fresh list without the just-unrestricted user (see `restrictUser`).
153
177
  * @param userId - The user ID to unrestrict
154
178
  * @returns Success message
155
179
  */
@@ -158,9 +182,11 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
158
182
  if (!userId) {
159
183
  throw new Error('User ID is required');
160
184
  }
161
- return await this.makeRequest<{ message: string }>('DELETE', `/privacy/restricted/${userId}`, undefined, {
185
+ const result = await this.makeRequest<{ message: string }>('DELETE', `/privacy/restricted/${userId}`, undefined, {
162
186
  cache: false,
163
187
  });
188
+ this.clearCacheEntry('GET:/privacy/restricted');
189
+ return result;
164
190
  } catch (error) {
165
191
  throw this.handleError(error);
166
192
  }
@@ -124,9 +124,13 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
124
124
  }
125
125
  ): Promise<TopicData> {
126
126
  try {
127
- return await this.makeRequest('PATCH', `/topics/${slug}`, data, {
127
+ const result = await this.makeRequest<TopicData>('PATCH', `/topics/${slug}`, data, {
128
128
  cache: false,
129
129
  });
130
+ // Bust the cached topic detail so `getTopicBySlug(slug)` reflects the
131
+ // updated metadata immediately (it caches at the LONG TTL).
132
+ this.clearCacheEntry(`GET:/topics/${slug}`);
133
+ return result;
130
134
  } catch (error) {
131
135
  throw this.handleError(error);
132
136
  }
@@ -482,16 +482,25 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
482
482
  }
483
483
 
484
484
  /**
485
- * Update privacy settings
485
+ * Update privacy settings.
486
+ *
487
+ * Invalidates the cached `GET /privacy/<id>/privacy` response (the exact
488
+ * key `getPrivacySettings` reads, scoped to the same `id`) after the write.
489
+ * `getPrivacySettings` caches for ~2 minutes (identity-scoped); without
490
+ * busting that entry, a follow-up read within the TTL window returns the
491
+ * pre-update settings. `clearCacheEntry` deletes every identity-scoped
492
+ * variant of the key.
486
493
  * @param settings - Partial privacy settings object
487
494
  * @param userId - The user ID (defaults to current user)
488
495
  */
489
496
  async updatePrivacySettings(settings: Partial<PrivacySettings>, userId?: string): Promise<PrivacySettings> {
490
497
  try {
491
498
  const id = userId || (await this.getCurrentUser()).id;
492
- return await this.makeRequest<PrivacySettings>('PATCH', `/privacy/${id}/privacy`, settings, {
499
+ const result = await this.makeRequest<PrivacySettings>('PATCH', `/privacy/${id}/privacy`, settings, {
493
500
  cache: false,
494
501
  });
502
+ this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
503
+ return result;
495
504
  } catch (error) {
496
505
  throw this.handleError(error);
497
506
  }