@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.
Files changed (46) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +18 -4
  3. package/dist/cjs/mixins/OxyServices.applications.js +69 -6
  4. package/dist/cjs/mixins/OxyServices.assets.js +16 -3
  5. package/dist/cjs/mixins/OxyServices.features.js +47 -10
  6. package/dist/cjs/mixins/OxyServices.managedAccounts.js +29 -3
  7. package/dist/cjs/mixins/OxyServices.privacy.js +34 -8
  8. package/dist/cjs/mixins/OxyServices.topics.js +5 -1
  9. package/dist/cjs/mixins/OxyServices.user.js +23 -3
  10. package/dist/cjs/mixins/OxyServices.workspaces.js +38 -3
  11. package/dist/cjs/utils/cache.js +9 -2
  12. package/dist/esm/.tsbuildinfo +1 -1
  13. package/dist/esm/HttpService.js +18 -4
  14. package/dist/esm/mixins/OxyServices.applications.js +69 -6
  15. package/dist/esm/mixins/OxyServices.assets.js +16 -3
  16. package/dist/esm/mixins/OxyServices.features.js +47 -10
  17. package/dist/esm/mixins/OxyServices.managedAccounts.js +29 -3
  18. package/dist/esm/mixins/OxyServices.privacy.js +34 -8
  19. package/dist/esm/mixins/OxyServices.topics.js +5 -1
  20. package/dist/esm/mixins/OxyServices.user.js +23 -3
  21. package/dist/esm/mixins/OxyServices.workspaces.js +38 -3
  22. package/dist/esm/utils/cache.js +9 -2
  23. package/dist/types/.tsbuildinfo +1 -1
  24. package/dist/types/HttpService.d.ts +9 -0
  25. package/dist/types/mixins/OxyServices.applications.d.ts +26 -0
  26. package/dist/types/mixins/OxyServices.features.d.ts +27 -6
  27. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +16 -1
  28. package/dist/types/mixins/OxyServices.privacy.d.ts +22 -4
  29. package/dist/types/mixins/OxyServices.user.d.ts +28 -1
  30. package/dist/types/mixins/OxyServices.workspaces.d.ts +12 -0
  31. package/dist/types/models/interfaces.d.ts +12 -0
  32. package/dist/types/utils/cache.d.ts +4 -1
  33. package/package.json +1 -4
  34. package/src/HttpService.ts +28 -4
  35. package/src/__tests__/httpServiceCache.test.ts +68 -0
  36. package/src/mixins/OxyServices.applications.ts +71 -6
  37. package/src/mixins/OxyServices.assets.ts +16 -3
  38. package/src/mixins/OxyServices.features.ts +47 -10
  39. package/src/mixins/OxyServices.managedAccounts.ts +29 -3
  40. package/src/mixins/OxyServices.privacy.ts +34 -8
  41. package/src/mixins/OxyServices.topics.ts +5 -1
  42. package/src/mixins/OxyServices.user.ts +39 -3
  43. package/src/mixins/OxyServices.workspaces.ts +39 -3
  44. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +147 -0
  45. package/src/models/interfaces.ts +13 -1
  46. package/src/utils/cache.ts +9 -2
@@ -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
+ });
@@ -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
- cacheTTL?: number; // Cache TTL in milliseconds (default: 5 minutes)
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;
@@ -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
- const expiresAt = now + (ttl || this.defaultTTL);
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