@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +18 -4
- package/dist/cjs/mixins/OxyServices.applications.js +69 -6
- package/dist/cjs/mixins/OxyServices.assets.js +16 -3
- package/dist/cjs/mixins/OxyServices.features.js +47 -10
- package/dist/cjs/mixins/OxyServices.managedAccounts.js +29 -3
- package/dist/cjs/mixins/OxyServices.privacy.js +34 -8
- package/dist/cjs/mixins/OxyServices.topics.js +5 -1
- package/dist/cjs/mixins/OxyServices.user.js +11 -2
- package/dist/cjs/mixins/OxyServices.workspaces.js +38 -3
- package/dist/cjs/utils/cache.js +9 -2
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +18 -4
- package/dist/esm/mixins/OxyServices.applications.js +69 -6
- package/dist/esm/mixins/OxyServices.assets.js +16 -3
- package/dist/esm/mixins/OxyServices.features.js +47 -10
- package/dist/esm/mixins/OxyServices.managedAccounts.js +29 -3
- package/dist/esm/mixins/OxyServices.privacy.js +34 -8
- package/dist/esm/mixins/OxyServices.topics.js +5 -1
- package/dist/esm/mixins/OxyServices.user.js +11 -2
- package/dist/esm/mixins/OxyServices.workspaces.js +38 -3
- package/dist/esm/utils/cache.js +9 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +9 -0
- package/dist/types/mixins/OxyServices.applications.d.ts +26 -0
- package/dist/types/mixins/OxyServices.features.d.ts +27 -6
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +16 -1
- package/dist/types/mixins/OxyServices.privacy.d.ts +22 -4
- package/dist/types/mixins/OxyServices.user.d.ts +8 -1
- package/dist/types/mixins/OxyServices.workspaces.d.ts +12 -0
- package/dist/types/models/interfaces.d.ts +12 -0
- package/dist/types/utils/cache.d.ts +4 -1
- package/package.json +1 -4
- package/src/HttpService.ts +28 -4
- package/src/__tests__/httpServiceCache.test.ts +68 -0
- package/src/mixins/OxyServices.applications.ts +71 -6
- package/src/mixins/OxyServices.assets.ts +16 -3
- package/src/mixins/OxyServices.features.ts +47 -10
- package/src/mixins/OxyServices.managedAccounts.ts +29 -3
- package/src/mixins/OxyServices.privacy.ts +34 -8
- package/src/mixins/OxyServices.topics.ts +5 -1
- package/src/mixins/OxyServices.user.ts +11 -2
- package/src/mixins/OxyServices.workspaces.ts +39 -3
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +147 -0
- package/src/models/interfaces.ts +13 -1
- 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.
|
|
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": {
|
package/src/HttpService.ts
CHANGED
|
@@ -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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|