@oxyhq/core 3.8.0 → 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 +23 -3
- 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 +23 -3
- 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 +28 -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 +39 -3
- 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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy / list cache-invalidation tests.
|
|
3
|
+
*
|
|
4
|
+
* The privacy reads cache their GET responses (identity-scoped):
|
|
5
|
+
* - `getBlockedUsers()` → `GET:/privacy/blocked` (~1 min TTL)
|
|
6
|
+
* - `getRestrictedUsers()` → `GET:/privacy/restricted` (~1 min TTL)
|
|
7
|
+
* - `getPrivacySettings(id)`→ `GET:/privacy/<id>/privacy` (~2 min TTL)
|
|
8
|
+
*
|
|
9
|
+
* Each corresponding write MUST invalidate the matching cached GET, otherwise a
|
|
10
|
+
* consumer that re-reads within the TTL window observes the STALE pre-write
|
|
11
|
+
* value (mirrors the follow/unfollow follow-status invalidation contract).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { OxyServices } from '../../OxyServices';
|
|
15
|
+
|
|
16
|
+
/** Build a non-verified JWT whose payload decodes to the given claims. */
|
|
17
|
+
function makeJwt(payload: Record<string, unknown>): string {
|
|
18
|
+
const b64url = (obj: Record<string, unknown>): string =>
|
|
19
|
+
Buffer.from(JSON.stringify(obj)).toString('base64url');
|
|
20
|
+
const fullPayload = { exp: Math.floor(Date.now() / 1000) + 3600, ...payload };
|
|
21
|
+
return `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url(fullPayload)}.sig`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A JSON `Response` mimicking the API's `{ data: ... }` success envelope. */
|
|
25
|
+
function jsonResponse(data: unknown): Response {
|
|
26
|
+
return new Response(JSON.stringify({ data }), {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('privacy cache invalidation', () => {
|
|
33
|
+
let originalFetch: typeof globalThis.fetch;
|
|
34
|
+
let fetchMock: jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
|
|
35
|
+
let oxy: OxyServices;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
originalFetch = globalThis.fetch;
|
|
39
|
+
fetchMock = jest.fn();
|
|
40
|
+
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
41
|
+
oxy = new OxyServices({ baseURL: 'http://test.invalid' });
|
|
42
|
+
oxy.httpService.setTokens(makeJwt({ userId: 'me' }));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
globalThis.fetch = originalFetch;
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('busts the cached blocked list after blockUser', async () => {
|
|
51
|
+
// 1) Warm the cache: empty blocked list.
|
|
52
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
53
|
+
expect(await oxy.getBlockedUsers()).toEqual([]);
|
|
54
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
55
|
+
|
|
56
|
+
// A second read within the TTL is a cache hit (no extra network call).
|
|
57
|
+
await oxy.getBlockedUsers();
|
|
58
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
59
|
+
|
|
60
|
+
// 2) Block a user — must invalidate the cached list.
|
|
61
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
62
|
+
await oxy.blockUser('target-1');
|
|
63
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
64
|
+
|
|
65
|
+
// 3) Re-read MUST re-fetch and observe the new entry.
|
|
66
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([{ blockedId: 'target-1' }]));
|
|
67
|
+
const after = await oxy.getBlockedUsers();
|
|
68
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
69
|
+
expect(after).toEqual([{ blockedId: 'target-1' }]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('busts the cached blocked list after unblockUser', async () => {
|
|
73
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([{ blockedId: 'target-1' }]));
|
|
74
|
+
await oxy.getBlockedUsers();
|
|
75
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
76
|
+
|
|
77
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
78
|
+
await oxy.unblockUser('target-1');
|
|
79
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
80
|
+
|
|
81
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
82
|
+
expect(await oxy.getBlockedUsers()).toEqual([]);
|
|
83
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('busts the cached restricted list after restrictUser / unrestrictUser', async () => {
|
|
87
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
88
|
+
await oxy.getRestrictedUsers();
|
|
89
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
90
|
+
|
|
91
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
92
|
+
await oxy.restrictUser('target-2');
|
|
93
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
94
|
+
|
|
95
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([{ restrictedId: 'target-2' }]));
|
|
96
|
+
expect(await oxy.getRestrictedUsers()).toEqual([{ restrictedId: 'target-2' }]);
|
|
97
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
98
|
+
|
|
99
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
100
|
+
await oxy.unrestrictUser('target-2');
|
|
101
|
+
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
102
|
+
|
|
103
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
104
|
+
expect(await oxy.getRestrictedUsers()).toEqual([]);
|
|
105
|
+
expect(fetchMock).toHaveBeenCalledTimes(5);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('busts the cached privacy settings (same id) after updatePrivacySettings', async () => {
|
|
109
|
+
// Warm the settings cache for an explicit id (avoids a getCurrentUser call).
|
|
110
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ isPrivateAccount: false }));
|
|
111
|
+
expect(await oxy.getPrivacySettings('me')).toEqual({ isPrivateAccount: false });
|
|
112
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
113
|
+
|
|
114
|
+
// Cache hit on the second read.
|
|
115
|
+
await oxy.getPrivacySettings('me');
|
|
116
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
117
|
+
|
|
118
|
+
// Update — must invalidate `GET:/privacy/me/privacy`.
|
|
119
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ isPrivateAccount: true }));
|
|
120
|
+
await oxy.updatePrivacySettings({ isPrivateAccount: true }, 'me');
|
|
121
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
122
|
+
|
|
123
|
+
// Re-read MUST re-fetch and observe the new value.
|
|
124
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ isPrivateAccount: true }));
|
|
125
|
+
const after = await oxy.getPrivacySettings('me');
|
|
126
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
127
|
+
expect(after).toEqual({ isPrivateAccount: true });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('invalidates the exact logical keys on block/restrict/settings writes', async () => {
|
|
131
|
+
const clearSpy = jest.spyOn(oxy, 'clearCacheEntry');
|
|
132
|
+
|
|
133
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
134
|
+
await oxy.blockUser('u1');
|
|
135
|
+
expect(clearSpy).toHaveBeenCalledWith('GET:/privacy/blocked');
|
|
136
|
+
|
|
137
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
138
|
+
await oxy.restrictUser('u2');
|
|
139
|
+
expect(clearSpy).toHaveBeenCalledWith('GET:/privacy/restricted');
|
|
140
|
+
|
|
141
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ isPrivateAccount: true }));
|
|
142
|
+
await oxy.updatePrivacySettings({ isPrivateAccount: true }, 'me');
|
|
143
|
+
expect(clearSpy).toHaveBeenCalledWith('GET:/privacy/me/privacy');
|
|
144
|
+
|
|
145
|
+
clearSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/models/interfaces.ts
CHANGED
|
@@ -29,8 +29,20 @@ export interface OxyConfig {
|
|
|
29
29
|
*/
|
|
30
30
|
clientId?: string;
|
|
31
31
|
// Performance & caching options
|
|
32
|
+
/**
|
|
33
|
+
* Enable the per-instance GET response cache. Defaults to `true` (5-minute
|
|
34
|
+
* TTL). Set to `false` to disable caching entirely for this instance — GET
|
|
35
|
+
* responses are then never stored and never served from cache, so every read
|
|
36
|
+
* hits the network. Useful for a linked backend client where another layer
|
|
37
|
+
* (e.g. React Query) is the single cache authority and the SDK's own cache
|
|
38
|
+
* would otherwise serve stale data after a write.
|
|
39
|
+
*/
|
|
32
40
|
enableCache?: boolean;
|
|
33
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Cache TTL in milliseconds (default: 5 minutes). A value `<= 0` disables the
|
|
43
|
+
* per-instance GET response cache, equivalent to `enableCache: false`.
|
|
44
|
+
*/
|
|
45
|
+
cacheTTL?: number;
|
|
34
46
|
enableRequestDeduplication?: boolean;
|
|
35
47
|
enableRetry?: boolean;
|
|
36
48
|
maxRetries?: number;
|
package/src/utils/cache.ts
CHANGED
|
@@ -91,11 +91,18 @@ export class TTLCache<T> {
|
|
|
91
91
|
* Set a value in cache
|
|
92
92
|
* @param key Cache key
|
|
93
93
|
* @param data Data to cache
|
|
94
|
-
* @param ttl Optional TTL override (uses default if not provided)
|
|
94
|
+
* @param ttl Optional TTL override (uses default if not provided). An
|
|
95
|
+
* explicit `0` or negative value is honored as "already expired" — the
|
|
96
|
+
* entry is stored with `expiresAt <= now`, so the next `get`/`has` treats
|
|
97
|
+
* it as a miss — rather than silently falling back to the default TTL.
|
|
95
98
|
*/
|
|
96
99
|
set(key: string, data: T, ttl?: number): void {
|
|
97
100
|
const now = Date.now();
|
|
98
|
-
|
|
101
|
+
// Distinguish "no override provided" (undefined → use default) from an
|
|
102
|
+
// explicit `0`/negative (do not cache). `ttl || this.defaultTTL` collapsed
|
|
103
|
+
// both into the default, making `cacheTTL:0` impossible to honor.
|
|
104
|
+
const effectiveTTL = ttl === undefined ? this.defaultTTL : ttl;
|
|
105
|
+
const expiresAt = now + effectiveTTL;
|
|
99
106
|
this.cache.set(key, { data, timestamp: now, expiresAt });
|
|
100
107
|
}
|
|
101
108
|
|