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