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