@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
@@ -46,6 +46,15 @@ export declare class HttpService {
46
46
  private baseURL;
47
47
  private tokenStore;
48
48
  private cache;
49
+ /**
50
+ * When true, the per-instance GET response cache is OFF: GET responses are
51
+ * never read from nor written to {@link cache}, so every request hits the
52
+ * network. Set from `config.enableCache === false` OR `config.cacheTTL <= 0`.
53
+ * A disabled instance does not register its cache for the global cleanup
54
+ * interval (nothing ever lands in it). Request deduplication is unaffected —
55
+ * concurrent identical in-flight requests still collapse into one.
56
+ */
57
+ private readonly cacheDisabled;
49
58
  private deduplicator;
50
59
  private requestQueue;
51
60
  private logger;
@@ -363,6 +363,32 @@ export declare function OxyServicesApplicationsMixin<T extends typeof OxyService
363
363
  * @param period - Time window (defaults to the server default).
364
364
  */
365
365
  getApplicationUsage(applicationId: string, period?: ApplicationUsagePeriod): Promise<ApplicationUsageStats>;
366
+ /**
367
+ * Bust every cached application list. `getApplications(workspaceId?)` keys
368
+ * the unscoped list as `GET:/applications` and each workspace-scoped list as
369
+ * `GET:/applications?workspaceId=<id>` (the query string is part of the URL
370
+ * path). A change to list membership (create/delete/ownership transfer)
371
+ * invalidates all of them, so we clear the unscoped entry plus every
372
+ * `?workspaceId=` variant via a prefix sweep. The prefix `GET:/applications?`
373
+ * matches only the query-string list variants, never the `GET:/applications/<id>…`
374
+ * detail/sub-resource keys.
375
+ *
376
+ * Internal helper (leading underscore); not part of the supported public
377
+ * surface. Public rather than `private` because mixins compose into an
378
+ * exported anonymous class, where TypeScript cannot represent a private
379
+ * member in the emitted declaration file (TS4094).
380
+ */
381
+ _invalidateApplicationLists(): void;
382
+ /**
383
+ * Bust the cached member list and detail for an application after a
384
+ * membership mutation. The member list (`getApplicationMembers`) and the
385
+ * detail (`getApplication`, which can embed member counts) both go stale
386
+ * when the member set or a member's role changes.
387
+ *
388
+ * Internal helper (leading underscore); see `_invalidateApplicationLists`
389
+ * for why this is public rather than `private`.
390
+ */
391
+ _invalidateApplicationMembership(applicationId: string): void;
366
392
  httpService: import("../HttpService").HttpService;
367
393
  cloudURL: string;
368
394
  config: import("../OxyServices.base").OxyConfig;
@@ -129,19 +129,33 @@ export declare function OxyServicesFeaturesMixin<T extends typeof OxyServicesBas
129
129
  */
130
130
  getCollections(userId?: string): Promise<Collection[]>;
131
131
  /**
132
- * Save an item
132
+ * Save an item.
133
+ *
134
+ * Busts the cached own saved-items list (`GET /saves`, ~short TTL) so a
135
+ * follow-up `getSavedItems()` observes the new item. The `userId`-scoped
136
+ * variant (`/users/<id>/saves`) is another user's list and is not
137
+ * affected by the caller's own save.
133
138
  */
134
139
  saveItem(itemId: string, itemType: string, collectionId?: string): Promise<SavedItem>;
135
140
  /**
136
- * Remove an item from saves
141
+ * Remove an item from saves.
142
+ *
143
+ * Busts the cached own saved-items list so the removed item is gone on
144
+ * the next read (see `saveItem`).
137
145
  */
138
146
  removeSavedItem(saveId: string): Promise<void>;
139
147
  /**
140
- * Create a collection
148
+ * Create a collection.
149
+ *
150
+ * Busts the cached own collections list (`GET /collections`) so the new
151
+ * collection appears on the next read.
141
152
  */
142
153
  createCollection(name: string, description?: string): Promise<Collection>;
143
154
  /**
144
- * Delete a collection
155
+ * Delete a collection.
156
+ *
157
+ * Busts the cached own collections list so the deleted collection is
158
+ * gone on the next read (see `createCollection`).
145
159
  */
146
160
  deleteCollection(collectionId: string): Promise<void>;
147
161
  /**
@@ -153,11 +167,18 @@ export declare function OxyServicesFeaturesMixin<T extends typeof OxyServicesBas
153
167
  */
154
168
  getUserHistory(userId?: string, limit?: number, offset?: number): Promise<HistoryItem[]>;
155
169
  /**
156
- * Clear user history
170
+ * Clear user history.
171
+ *
172
+ * `getUserHistory` caches per (limit, offset) page, so its cache key
173
+ * carries serialized params (`GET:/history` and `GET:/history:<params>`).
174
+ * A prefix sweep busts every cached page of the own history at once.
157
175
  */
158
176
  clearUserHistory(): Promise<void>;
159
177
  /**
160
- * Delete a history item
178
+ * Delete a history item.
179
+ *
180
+ * Busts every cached page of the own history (see `clearUserHistory`)
181
+ * so the removed item no longer appears on the next read.
161
182
  */
162
183
  deleteHistoryItem(itemId: string): Promise<void>;
163
184
  /**
@@ -36,7 +36,9 @@ export declare function OxyServicesManagedAccountsMixin<T extends typeof OxyServ
36
36
  * Create a new managed account (sub-account).
37
37
  *
38
38
  * The server creates a User document with `isManagedAccount: true` and links
39
- * it to the authenticated user as owner.
39
+ * it to the authenticated user as owner. Invalidates the cached
40
+ * `GET /managed-accounts` list (~2-minute TTL, identity-scoped) so the next
41
+ * read includes the newly created account.
40
42
  */
41
43
  createManagedAccount(data: CreateManagedAccountInput): Promise<ManagedAccount>;
42
44
  /**
@@ -50,17 +52,27 @@ export declare function OxyServicesManagedAccountsMixin<T extends typeof OxyServ
50
52
  /**
51
53
  * Update a managed account's profile data.
52
54
  * Requires owner or admin role.
55
+ *
56
+ * Invalidates both the cached detail (`GET /managed-accounts/<id>`) and the
57
+ * cached list (`GET /managed-accounts`, which embeds account profile data)
58
+ * so neither serves the pre-update snapshot within their ~2-minute TTL.
53
59
  */
54
60
  updateManagedAccount(accountId: string, data: Partial<CreateManagedAccountInput>): Promise<ManagedAccount>;
55
61
  /**
56
62
  * Delete a managed account permanently.
57
63
  * Requires owner role.
64
+ *
65
+ * Invalidates the cached detail and list responses so the deleted account
66
+ * is not served from cache.
58
67
  */
59
68
  deleteManagedAccount(accountId: string): Promise<void>;
60
69
  /**
61
70
  * Add a manager to a managed account.
62
71
  * Requires owner or admin role on the account.
63
72
  *
73
+ * Mutates the account's `managers[]`, which is returned by the detail and
74
+ * list reads — invalidate both so they re-fetch the updated manager set.
75
+ *
64
76
  * @param accountId - The managed account to add the manager to
65
77
  * @param userId - The user to grant management access
66
78
  * @param role - The role to assign: 'admin' or 'editor'
@@ -70,6 +82,9 @@ export declare function OxyServicesManagedAccountsMixin<T extends typeof OxyServ
70
82
  * Remove a manager from a managed account.
71
83
  * Requires owner role.
72
84
  *
85
+ * Invalidates the detail and list responses so the updated `managers[]`
86
+ * is observed on the next read (see `addManager`).
87
+ *
73
88
  * @param accountId - The managed account
74
89
  * @param userId - The manager to remove
75
90
  */
@@ -27,7 +27,13 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
27
27
  */
28
28
  getBlockedUsers(): Promise<BlockedUser[]>;
29
29
  /**
30
- * Block a user
30
+ * Block a user.
31
+ *
32
+ * Invalidates the cached `GET /privacy/blocked` response after the write.
33
+ * `getBlockedUsers` caches for ~1 minute (identity-scoped); without busting
34
+ * that entry, a consumer that re-reads the blocked list within the TTL
35
+ * window would not see the user it just blocked. `clearCacheEntry` deletes
36
+ * every identity-scoped variant of the key.
31
37
  * @param userId - The user ID to block
32
38
  * @returns Success message
33
39
  */
@@ -35,7 +41,10 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
35
41
  message: string;
36
42
  }>;
37
43
  /**
38
- * Unblock a user
44
+ * Unblock a user.
45
+ *
46
+ * Busts the cached `GET /privacy/blocked` response so a remount reads the
47
+ * fresh list without the just-unblocked user (see `blockUser`).
39
48
  * @param userId - The user ID to unblock
40
49
  * @returns Success message
41
50
  */
@@ -54,7 +63,13 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
54
63
  */
55
64
  getRestrictedUsers(): Promise<RestrictedUser[]>;
56
65
  /**
57
- * Restrict a user (limit their interactions without fully blocking)
66
+ * Restrict a user (limit their interactions without fully blocking).
67
+ *
68
+ * Invalidates the cached `GET /privacy/restricted` response after the write.
69
+ * `getRestrictedUsers` caches for ~1 minute (identity-scoped); without
70
+ * busting that entry, a consumer that re-reads the restricted list within
71
+ * the TTL window would not see the user it just restricted.
72
+ * `clearCacheEntry` deletes every identity-scoped variant of the key.
58
73
  * @param userId - The user ID to restrict
59
74
  * @returns Success message
60
75
  */
@@ -62,7 +77,10 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
62
77
  message: string;
63
78
  }>;
64
79
  /**
65
- * Unrestrict a user
80
+ * Unrestrict a user.
81
+ *
82
+ * Busts the cached `GET /privacy/restricted` response so a remount reads the
83
+ * fresh list without the just-unrestricted user (see `restrictUser`).
66
84
  * @param userId - The user ID to unrestrict
67
85
  * @returns Success message
68
86
  */
@@ -182,7 +182,14 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
182
182
  */
183
183
  getPrivacySettings(userId?: string): Promise<PrivacySettings>;
184
184
  /**
185
- * Update privacy settings
185
+ * Update privacy settings.
186
+ *
187
+ * Invalidates the cached `GET /privacy/<id>/privacy` response (the exact
188
+ * key `getPrivacySettings` reads, scoped to the same `id`) after the write.
189
+ * `getPrivacySettings` caches for ~2 minutes (identity-scoped); without
190
+ * busting that entry, a follow-up read within the TTL window returns the
191
+ * pre-update settings. `clearCacheEntry` deletes every identity-scoped
192
+ * variant of the key.
186
193
  * @param settings - Partial privacy settings object
187
194
  * @param userId - The user ID (defaults to current user)
188
195
  */
@@ -151,6 +151,18 @@ export declare function OxyServicesWorkspacesMixin<T extends typeof OxyServicesB
151
151
  * @param data - Target user id.
152
152
  */
153
153
  transferWorkspaceOwnership(workspaceId: string, data: TransferWorkspaceOwnershipInput): Promise<WorkspaceSuccessResult>;
154
+ /**
155
+ * Bust the cached member list and detail for a workspace after a membership
156
+ * mutation. The member list (`getWorkspaceMembers`) and the detail
157
+ * (`getWorkspace`, which can embed member counts) both go stale when the
158
+ * member set or a member's role changes.
159
+ *
160
+ * Internal helper (leading underscore); not part of the supported public
161
+ * surface. Public rather than `private` because mixins compose into an
162
+ * exported anonymous class, where TypeScript cannot represent a private
163
+ * member in the emitted declaration file (TS4094).
164
+ */
165
+ _invalidateWorkspaceMembership(workspaceId: string): void;
154
166
  httpService: import("../HttpService").HttpService;
155
167
  cloudURL: string;
156
168
  config: import("../OxyServices.base").OxyConfig;
@@ -27,7 +27,19 @@ export interface OxyConfig {
27
27
  * bounce (which uses the RP origin, not this registered client id).
28
28
  */
29
29
  clientId?: string;
30
+ /**
31
+ * Enable the per-instance GET response cache. Defaults to `true` (5-minute
32
+ * TTL). Set to `false` to disable caching entirely for this instance — GET
33
+ * responses are then never stored and never served from cache, so every read
34
+ * hits the network. Useful for a linked backend client where another layer
35
+ * (e.g. React Query) is the single cache authority and the SDK's own cache
36
+ * would otherwise serve stale data after a write.
37
+ */
30
38
  enableCache?: boolean;
39
+ /**
40
+ * Cache TTL in milliseconds (default: 5 minutes). A value `<= 0` disables the
41
+ * per-instance GET response cache, equivalent to `enableCache: false`.
42
+ */
31
43
  cacheTTL?: number;
32
44
  enableRequestDeduplication?: boolean;
33
45
  enableRetry?: boolean;
@@ -59,7 +59,10 @@ export declare class TTLCache<T> {
59
59
  * Set a value in cache
60
60
  * @param key Cache key
61
61
  * @param data Data to cache
62
- * @param ttl Optional TTL override (uses default if not provided)
62
+ * @param ttl Optional TTL override (uses default if not provided). An
63
+ * explicit `0` or negative value is honored as "already expired" — the
64
+ * entry is stored with `expiresAt <= now`, so the next `get`/`has` treats
65
+ * it as a miss — rather than silently falling back to the default TTL.
63
66
  */
64
67
  set(key: string, data: T, ttl?: number): void;
65
68
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -125,9 +125,6 @@
125
125
  },
126
126
  "express": {
127
127
  "optional": true
128
- },
129
- "express-rate-limit": {
130
- "optional": true
131
128
  }
132
129
  },
133
130
  "devDependencies": {
@@ -202,6 +202,15 @@ export class HttpService {
202
202
  private baseURL: string;
203
203
  private tokenStore: TokenStore;
204
204
  private cache: TTLCache<any>;
205
+ /**
206
+ * When true, the per-instance GET response cache is OFF: GET responses are
207
+ * never read from nor written to {@link cache}, so every request hits the
208
+ * network. Set from `config.enableCache === false` OR `config.cacheTTL <= 0`.
209
+ * A disabled instance does not register its cache for the global cleanup
210
+ * interval (nothing ever lands in it). Request deduplication is unaffected —
211
+ * concurrent identical in-flight requests still collapse into one.
212
+ */
213
+ private readonly cacheDisabled: boolean;
205
214
  private deduplicator: RequestDeduplicator;
206
215
  private requestQueue: RequestQueue;
207
216
  private logger: SimpleLogger;
@@ -252,9 +261,19 @@ export class HttpService {
252
261
  'HttpService'
253
262
  );
254
263
 
255
- // Initialize performance infrastructure
256
- this.cache = new TTLCache<any>(config.cacheTTL || 5 * 60 * 1000);
257
- registerCacheForCleanup(this.cache);
264
+ // Initialize performance infrastructure. The per-instance GET response
265
+ // cache is disabled when the consumer explicitly opts out
266
+ // (`enableCache: false`) or asks for a non-positive TTL (`cacheTTL <= 0`).
267
+ // When disabled, nothing is ever stored, so there is no reason to register
268
+ // the cache for the global cleanup interval. Default (config unset) keeps
269
+ // caching ON with the 5-minute TTL — unchanged for existing consumers.
270
+ this.cacheDisabled =
271
+ config.enableCache === false ||
272
+ (typeof config.cacheTTL === 'number' && config.cacheTTL <= 0);
273
+ this.cache = new TTLCache<any>(config.cacheTTL && config.cacheTTL > 0 ? config.cacheTTL : 5 * 60 * 1000);
274
+ if (!this.cacheDisabled) {
275
+ registerCacheForCleanup(this.cache);
276
+ }
258
277
  this.deduplicator = new RequestDeduplicator();
259
278
  this.requestQueue = new RequestQueue(
260
279
  config.maxConcurrentRequests || 10,
@@ -352,13 +371,18 @@ export class HttpService {
352
371
  params,
353
372
  timeout = this.config.requestTimeout || DEFAULT_REQUEST_TIMEOUT_MS,
354
373
  signal,
355
- cache = method === 'GET',
374
+ cache: cacheRequested = method === 'GET',
356
375
  cacheTTL,
357
376
  deduplicate = true,
358
377
  retry = this.config.enableRetry !== false,
359
378
  maxRetries = this.config.maxRetries || 3,
360
379
  } = config;
361
380
 
381
+ // A per-instance disabled cache (`enableCache:false` / `cacheTTL<=0`)
382
+ // overrides any per-request `cache:true`: nothing is read from nor written
383
+ // to the response cache. Request deduplication below is unaffected.
384
+ const cache = cacheRequested && !this.cacheDisabled;
385
+
362
386
  // Generate cache key (optimized for large objects)
363
387
  const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;
364
388
 
@@ -266,4 +266,72 @@ describe('HttpService identity-scoped response cache', () => {
266
266
  expect(cacheWarnings.length).toBe(0);
267
267
  });
268
268
  });
269
+
270
+ /**
271
+ * `enableCache: false` (or `cacheTTL <= 0`) must fully disable the
272
+ * per-instance GET response cache: every read hits the network, even when the
273
+ * per-request `cache: true` flag is set. This is the lever Mention's linked
274
+ * backend client uses to make React Query the single cache authority and
275
+ * avoid stale-after-write reads from the SDK's own cache. Default behavior
276
+ * (config unset) is unchanged — caching stays ON.
277
+ */
278
+ describe('per-instance cache disable', () => {
279
+ it('never serves a cached GET when enableCache is false', async () => {
280
+ const http = new HttpService({
281
+ baseURL: 'http://test.invalid',
282
+ enableRetry: false,
283
+ requestTimeout: 1000,
284
+ enableCache: false,
285
+ });
286
+ http.setTokens(makeJwt({ userId: 'user-1' }));
287
+
288
+ fetchMock.mockResolvedValueOnce(jsonResponse({ v: 1 }));
289
+ const first = await http.get<{ v: number }>('/some/resource', { cache: true });
290
+ expect(first.v).toBe(1);
291
+ expect(fetchMock).toHaveBeenCalledTimes(1);
292
+
293
+ // A second identical read with cache:true must STILL hit the network,
294
+ // because the instance cache is disabled. Nothing is ever stored.
295
+ fetchMock.mockResolvedValueOnce(jsonResponse({ v: 2 }));
296
+ const second = await http.get<{ v: number }>('/some/resource', { cache: true });
297
+ expect(second.v).toBe(2);
298
+ expect(fetchMock).toHaveBeenCalledTimes(2);
299
+ expect(http.getCacheStats().size).toBe(0);
300
+ });
301
+
302
+ it('treats cacheTTL <= 0 as a disabled cache', async () => {
303
+ const http = new HttpService({
304
+ baseURL: 'http://test.invalid',
305
+ enableRetry: false,
306
+ requestTimeout: 1000,
307
+ cacheTTL: 0,
308
+ });
309
+ http.setTokens(makeJwt({ userId: 'user-1' }));
310
+
311
+ fetchMock.mockResolvedValueOnce(jsonResponse({ v: 1 }));
312
+ await http.get('/some/resource', { cache: true });
313
+ fetchMock.mockResolvedValueOnce(jsonResponse({ v: 2 }));
314
+ await http.get('/some/resource', { cache: true });
315
+
316
+ expect(fetchMock).toHaveBeenCalledTimes(2);
317
+ expect(http.getCacheStats().size).toBe(0);
318
+ });
319
+
320
+ it('still caches by default (config unset) — no behavior change', async () => {
321
+ const http = new HttpService({
322
+ baseURL: 'http://test.invalid',
323
+ enableRetry: false,
324
+ requestTimeout: 1000,
325
+ });
326
+ http.setTokens(makeJwt({ userId: 'user-1' }));
327
+
328
+ fetchMock.mockResolvedValueOnce(jsonResponse({ v: 1 }));
329
+ const first = await http.get<{ v: number }>('/some/resource', { cache: true });
330
+ // Second read is a warm cache hit — no second network call.
331
+ const second = await http.get<{ v: number }>('/some/resource', { cache: true });
332
+
333
+ expect(first).toEqual(second);
334
+ expect(fetchMock).toHaveBeenCalledTimes(1);
335
+ });
336
+ });
269
337
  });
@@ -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
  }