@itd-api/cache 0.0.1 → 0.1.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/dist/index.cjs CHANGED
@@ -1,683 +1,822 @@
1
- 'use strict';
2
-
3
- var lruCache = require('lru-cache');
4
-
5
- // src/errors.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let lru_cache = require("lru-cache");
3
+ //#region src/errors.ts
4
+ /** Ошибка настройки или использования плагина кэша. */
6
5
  var CacheError = class extends Error {
7
- name = "CacheError";
6
+ name = "CacheError";
8
7
  };
9
-
10
- // src/key.ts
11
- var OMITTED_FIELDS = /* @__PURE__ */ new Set([
12
- "method",
13
- "path",
14
- "service",
15
- "baseUrl",
16
- "query",
17
- "body",
18
- "headers",
19
- "raw",
20
- "skipAuth",
21
- "signal",
22
- "timeout",
23
- "retry",
24
- "skipQueue",
25
- "skipAuthRefresh",
26
- "cache"
8
+ //#endregion
9
+ //#region src/key.ts
10
+ const OMITTED_FIELDS = /* @__PURE__ */ new Set([
11
+ "method",
12
+ "operationId",
13
+ "path",
14
+ "service",
15
+ "baseUrl",
16
+ "query",
17
+ "body",
18
+ "headers",
19
+ "raw",
20
+ "skipAuth",
21
+ "signal",
22
+ "timeout",
23
+ "retry",
24
+ "retrySafety",
25
+ "skipQueue",
26
+ "skipAuthRefresh",
27
+ "extensions"
27
28
  ]);
29
+ /** Строка query в том же порядке и с теми же правилами, что использует itd-api. */
28
30
  function queryKey(query) {
29
- if (!query) return "";
30
- const search = new URLSearchParams();
31
- for (const [key, value] of Object.entries(query)) {
32
- if (value === void 0 || value === null) continue;
33
- if (Array.isArray(value)) {
34
- for (const item of value) search.append(key, String(item));
35
- } else {
36
- search.append(key, String(value));
37
- }
38
- }
39
- return search.toString();
31
+ if (!query) return "";
32
+ const search = new URLSearchParams();
33
+ for (const [key, value] of Object.entries(query)) {
34
+ if (value === void 0 || value === null) continue;
35
+ if (Array.isArray(value)) for (const item of value) search.append(key, String(item));
36
+ else search.append(key, String(value));
37
+ }
38
+ return search.toString();
40
39
  }
41
- function buildCacheKey(route, request) {
42
- const extras = {};
43
- for (const [key, value] of Object.entries(request)) {
44
- if (!OMITTED_FIELDS.has(key) && value !== void 0) extras[key] = value;
45
- }
46
- try {
47
- return JSON.stringify({
48
- route,
49
- method: request.method.toUpperCase(),
50
- service: request.service ?? null,
51
- baseUrl: request.baseUrl ?? null,
52
- path: request.path,
53
- query: queryKey(request.query),
54
- body: request.body ?? null,
55
- raw: request.raw ?? false,
56
- skipAuth: request.skipAuth ?? null,
57
- extras
58
- });
59
- } catch {
60
- return void 0;
61
- }
40
+ /**
41
+ * Собирает ключ из значений, влияющих на адрес, тело или разобранный ответ.
42
+ *
43
+ * Заголовки и транспортные опции намеренно не входят. Если тело либо опция другого
44
+ * плагина не сериализуются как JSON, запрос выполняется без кэша.
45
+ */
46
+ function buildCacheKey(operation, request) {
47
+ const extras = {};
48
+ for (const [key, value] of Object.entries(request)) if (!OMITTED_FIELDS.has(key) && value !== void 0) extras[key] = value;
49
+ const { cache: _cacheMode, ...extensions } = request.extensions ?? {};
50
+ if (Object.keys(extensions).length > 0) extras.extensions = extensions;
51
+ try {
52
+ return JSON.stringify({
53
+ operation,
54
+ method: request.method.toUpperCase(),
55
+ service: request.service ?? null,
56
+ baseUrl: request.baseUrl ?? null,
57
+ path: request.path,
58
+ query: queryKey(request.query),
59
+ body: request.body ?? null,
60
+ raw: request.raw ?? false,
61
+ skipAuth: request.skipAuth ?? null,
62
+ extras
63
+ });
64
+ } catch {
65
+ return;
66
+ }
62
67
  }
63
-
64
- // src/mutations.ts
65
- var POST_CONTENT = [
66
- "posts.list",
67
- "posts.get",
68
- "posts.byUser",
69
- "posts.likedByUser",
70
- "posts.comments",
71
- "posts.stats",
72
- "comments.replies",
73
- "hashtags.search",
74
- "hashtags.trending",
75
- "hashtags.posts",
76
- "search.all",
77
- "users.me",
78
- "users.get",
79
- "users.pins"
80
- ];
81
- var POST_REACTIONS = [
82
- "posts.list",
83
- "posts.get",
84
- "posts.byUser",
85
- "posts.likedByUser",
86
- "posts.stats",
87
- "hashtags.posts",
88
- "search.all"
89
- ];
90
- var COMMENTS = [
91
- "posts.list",
92
- "posts.get",
93
- "posts.comments",
94
- "posts.stats",
95
- "comments.replies",
96
- "hashtags.posts",
97
- "search.all"
68
+ //#endregion
69
+ //#region src/operations.ts
70
+ function freezeOperations(operations) {
71
+ for (const operation of operations) Object.freeze(operation);
72
+ return Object.freeze(operations);
73
+ }
74
+ /** Читающие операции, которые можно кэшировать. */
75
+ const CACHE_OPERATIONS = freezeOperations([
76
+ {
77
+ id: "auth.sessions",
78
+ category: "auth"
79
+ },
80
+ {
81
+ id: "users.me",
82
+ category: "users"
83
+ },
84
+ {
85
+ id: "users.checkUsername",
86
+ category: "users"
87
+ },
88
+ {
89
+ id: "users.search",
90
+ category: "users"
91
+ },
92
+ {
93
+ id: "users.whoToFollow",
94
+ category: "users"
95
+ },
96
+ {
97
+ id: "users.topClans",
98
+ category: "users"
99
+ },
100
+ {
101
+ id: "users.followers",
102
+ category: "users"
103
+ },
104
+ {
105
+ id: "users.following",
106
+ category: "users"
107
+ },
108
+ {
109
+ id: "users.blocked",
110
+ category: "users"
111
+ },
112
+ {
113
+ id: "users.getPrivacy",
114
+ category: "users"
115
+ },
116
+ {
117
+ id: "users.pins",
118
+ category: "users"
119
+ },
120
+ {
121
+ id: "users.followStatus",
122
+ category: "users"
123
+ },
124
+ {
125
+ id: "users.get",
126
+ category: "users"
127
+ },
128
+ {
129
+ id: "posts.list",
130
+ category: "posts"
131
+ },
132
+ {
133
+ id: "posts.likedByUser",
134
+ category: "posts"
135
+ },
136
+ {
137
+ id: "posts.byUser",
138
+ category: "posts"
139
+ },
140
+ {
141
+ id: "posts.comments",
142
+ category: "posts"
143
+ },
144
+ {
145
+ id: "posts.stats",
146
+ category: "posts"
147
+ },
148
+ {
149
+ id: "posts.get",
150
+ category: "posts"
151
+ },
152
+ {
153
+ id: "comments.replies",
154
+ category: "comments"
155
+ },
156
+ {
157
+ id: "notifications.list",
158
+ category: "notifications"
159
+ },
160
+ {
161
+ id: "notifications.count",
162
+ category: "notifications"
163
+ },
164
+ {
165
+ id: "notifications.getSettings",
166
+ category: "notifications"
167
+ },
168
+ {
169
+ id: "hashtags.search",
170
+ category: "hashtags"
171
+ },
172
+ {
173
+ id: "hashtags.trending",
174
+ category: "hashtags"
175
+ },
176
+ {
177
+ id: "hashtags.posts",
178
+ category: "hashtags"
179
+ },
180
+ {
181
+ id: "search.all",
182
+ category: "search"
183
+ },
184
+ {
185
+ id: "files.get",
186
+ category: "files"
187
+ },
188
+ {
189
+ id: "subscription.status",
190
+ category: "subscription"
191
+ },
192
+ {
193
+ id: "subscription.methods",
194
+ category: "subscription"
195
+ },
196
+ {
197
+ id: "verification.status",
198
+ category: "verification"
199
+ },
200
+ {
201
+ id: "platform.changelog",
202
+ category: "platform"
203
+ },
204
+ {
205
+ id: "platform.announcements",
206
+ category: "platform"
207
+ },
208
+ {
209
+ id: "platform.portal",
210
+ category: "platform"
211
+ },
212
+ {
213
+ id: "platform.status",
214
+ category: "platform"
215
+ }
216
+ ]);
217
+ const OPERATIONS = new Map(CACHE_OPERATIONS.map((operation) => [operation.id, operation]));
218
+ /** Проверяет публичное имя кэшируемой операции. */
219
+ function isCacheOperationId(value) {
220
+ return OPERATIONS.has(value);
221
+ }
222
+ /** Находит читающую операцию по стабильному семантическому ID. */
223
+ function cacheOperation(operationId) {
224
+ return OPERATIONS.get(operationId);
225
+ }
226
+ //#endregion
227
+ //#region src/mutations.ts
228
+ const POST_CONTENT = [
229
+ "posts.list",
230
+ "posts.get",
231
+ "posts.byUser",
232
+ "posts.likedByUser",
233
+ "posts.comments",
234
+ "posts.stats",
235
+ "comments.replies",
236
+ "hashtags.search",
237
+ "hashtags.trending",
238
+ "hashtags.posts",
239
+ "search.all",
240
+ "users.me",
241
+ "users.get",
242
+ "users.pins"
98
243
  ];
99
- var PROFILE = [
100
- "users.me",
101
- "users.get",
102
- "users.checkUsername",
103
- "users.search",
104
- "users.whoToFollow",
105
- "users.topClans",
106
- "users.followers",
107
- "users.following",
108
- "users.blocked",
109
- "users.getPrivacy",
110
- "users.pins",
111
- "users.followStatus",
112
- "posts.list",
113
- "posts.get",
114
- "posts.byUser",
115
- "posts.likedByUser",
116
- "posts.comments",
117
- "comments.replies",
118
- "hashtags.posts",
119
- "search.all"
244
+ const POST_REACTIONS = [
245
+ "posts.list",
246
+ "posts.get",
247
+ "posts.byUser",
248
+ "posts.likedByUser",
249
+ "posts.stats",
250
+ "hashtags.posts",
251
+ "search.all"
120
252
  ];
121
- var FOLLOWING = [
122
- "users.me",
123
- "users.get",
124
- "users.search",
125
- "users.whoToFollow",
126
- "users.followers",
127
- "users.following",
128
- "users.followStatus",
129
- "posts.list",
130
- "posts.byUser",
131
- "search.all"
253
+ const COMMENTS = [
254
+ "posts.list",
255
+ "posts.get",
256
+ "posts.comments",
257
+ "posts.stats",
258
+ "comments.replies",
259
+ "hashtags.posts",
260
+ "search.all"
132
261
  ];
133
- var BLOCKS = [
134
- "users.me",
135
- "users.get",
136
- "users.search",
137
- "users.whoToFollow",
138
- "users.followers",
139
- "users.following",
140
- "users.blocked",
141
- "users.followStatus",
142
- "posts.list",
143
- "search.all"
262
+ const PROFILE = [
263
+ "users.me",
264
+ "users.get",
265
+ "users.checkUsername",
266
+ "users.search",
267
+ "users.whoToFollow",
268
+ "users.topClans",
269
+ "users.followers",
270
+ "users.following",
271
+ "users.blocked",
272
+ "users.getPrivacy",
273
+ "users.pins",
274
+ "users.followStatus",
275
+ "posts.list",
276
+ "posts.get",
277
+ "posts.byUser",
278
+ "posts.likedByUser",
279
+ "posts.comments",
280
+ "comments.replies",
281
+ "hashtags.posts",
282
+ "search.all"
144
283
  ];
145
- var PINS = [
146
- "users.me",
147
- "users.get",
148
- "users.pins",
149
- "posts.list",
150
- "posts.get",
151
- "posts.byUser"
284
+ const FOLLOWING = [
285
+ "users.me",
286
+ "users.get",
287
+ "users.search",
288
+ "users.whoToFollow",
289
+ "users.followers",
290
+ "users.following",
291
+ "users.followStatus",
292
+ "posts.list",
293
+ "posts.byUser",
294
+ "search.all"
152
295
  ];
153
- var NOTIFICATIONS = [
154
- "notifications.list",
155
- "notifications.count"
296
+ const BLOCKS = [
297
+ "users.me",
298
+ "users.get",
299
+ "users.search",
300
+ "users.whoToFollow",
301
+ "users.followers",
302
+ "users.following",
303
+ "users.blocked",
304
+ "users.followStatus",
305
+ "posts.list",
306
+ "search.all"
156
307
  ];
157
- var SUBSCRIPTION = [
158
- "subscription.status",
159
- "subscription.methods"
308
+ const PINS = [
309
+ "users.me",
310
+ "users.get",
311
+ "users.pins",
312
+ "posts.list",
313
+ "posts.get",
314
+ "posts.byUser"
160
315
  ];
161
- var NOTHING = [];
162
- var CACHE_MUTATIONS = Object.freeze([
163
- // Авторизация и сессии.
164
- { method: "POST", path: /^\/api\/v1\/auth\/refresh$/, invalidates: NOTHING },
165
- { method: "POST", path: /^\/api\/v1\/auth\/resend-otp$/, invalidates: NOTHING },
166
- { method: "POST", path: /^\/api\/v1\/auth\/forgot-password$/, invalidates: NOTHING },
167
- { method: "POST", path: /^\/api\/v1\/auth\/sign-up$/, invalidates: "all" },
168
- { method: "POST", path: /^\/api\/v1\/auth\/sign-in$/, invalidates: "all" },
169
- { method: "POST", path: /^\/api\/v1\/auth\/verify-otp$/, invalidates: "all" },
170
- { method: "POST", path: /^\/api\/v1\/auth\/logout$/, invalidates: "all" },
171
- { method: "POST", path: /^\/api\/v1\/auth\/reset-password$/, invalidates: "all" },
172
- { method: "POST", path: /^\/api\/v1\/auth\/change-password$/, invalidates: "all" },
173
- {
174
- method: "DELETE",
175
- path: /^\/api\/v1\/auth\/sessions(?:\/[^/]+)?$/,
176
- invalidates: ["auth.sessions"],
177
- scope: "installation"
178
- },
179
- // Посты и комментарии.
180
- { method: "POST", path: /^\/api\/posts$/, invalidates: POST_CONTENT },
181
- { method: "PUT", path: /^\/api\/posts\/[^/]+$/, invalidates: POST_CONTENT },
182
- { method: "DELETE", path: /^\/api\/posts\/[^/]+$/, invalidates: POST_CONTENT },
183
- { method: "POST", path: /^\/api\/posts\/[^/]+\/restore$/, invalidates: POST_CONTENT },
184
- { method: "POST", path: /^\/api\/posts\/[^/]+\/like$/, invalidates: POST_REACTIONS },
185
- { method: "DELETE", path: /^\/api\/posts\/[^/]+\/like$/, invalidates: POST_REACTIONS },
186
- { method: "POST", path: /^\/api\/posts\/[^/]+\/repost$/, invalidates: POST_CONTENT },
187
- { method: "DELETE", path: /^\/api\/posts\/[^/]+\/repost$/, invalidates: POST_CONTENT },
188
- { method: "POST", path: /^\/api\/posts\/[^/]+\/pin$/, invalidates: PINS },
189
- { method: "DELETE", path: /^\/api\/posts\/[^/]+\/pin$/, invalidates: PINS },
190
- { method: "POST", path: /^\/api\/posts\/[^/]+\/poll\/vote$/, invalidates: POST_REACTIONS },
191
- { method: "POST", path: /^\/api\/posts\/[^/]+\/comments$/, invalidates: COMMENTS },
192
- { method: "POST", path: /^\/api\/comments\/[^/]+\/replies$/, invalidates: COMMENTS },
193
- { method: "PATCH", path: /^\/api\/comments\/[^/]+$/, invalidates: COMMENTS },
194
- { method: "DELETE", path: /^\/api\/comments\/[^/]+$/, invalidates: COMMENTS },
195
- { method: "POST", path: /^\/api\/comments\/[^/]+\/restore$/, invalidates: COMMENTS },
196
- { method: "POST", path: /^\/api\/comments\/[^/]+\/like$/, invalidates: COMMENTS },
197
- { method: "DELETE", path: /^\/api\/comments\/[^/]+\/like$/, invalidates: COMMENTS },
198
- // Профиль и связи между пользователями.
199
- { method: "PUT", path: /^\/api\/users\/me$/, invalidates: PROFILE },
200
- { method: "DELETE", path: /^\/api\/users\/me$/, invalidates: PROFILE },
201
- { method: "POST", path: /^\/api\/users\/me\/restore$/, invalidates: PROFILE },
202
- { method: "POST", path: /^\/api\/users\/profile$/, invalidates: PROFILE },
203
- { method: "POST", path: /^\/api\/users\/[^/]+\/follow$/, invalidates: FOLLOWING },
204
- { method: "DELETE", path: /^\/api\/users\/[^/]+\/follow$/, invalidates: FOLLOWING },
205
- { method: "POST", path: /^\/api\/users\/[^/]+\/block$/, invalidates: BLOCKS },
206
- { method: "DELETE", path: /^\/api\/users\/[^/]+\/block$/, invalidates: BLOCKS },
207
- {
208
- method: "PUT",
209
- path: /^\/api\/users\/me\/privacy$/,
210
- invalidates: ["users.me", "users.get", "users.getPrivacy"]
211
- },
212
- { method: "PUT", path: /^\/api\/users\/me\/pin$/, invalidates: PINS },
213
- { method: "DELETE", path: /^\/api\/users\/me\/pin$/, invalidates: PINS },
214
- // Уведомления.
215
- {
216
- method: "POST",
217
- path: /^\/api\/notifications\/(?:[^/]+\/read|read-batch|read-all)$/,
218
- invalidates: NOTIFICATIONS,
219
- scope: "installation"
220
- },
221
- {
222
- method: "PUT",
223
- path: /^\/api\/notifications\/settings$/,
224
- invalidates: ["notifications.getSettings"],
225
- scope: "installation"
226
- },
227
- // Файлы и настройки аккаунта.
228
- { method: "POST", path: /^\/api\/files\/upload$/, invalidates: NOTHING },
229
- {
230
- method: "DELETE",
231
- path: /^\/api\/files\/[^/]+$/,
232
- invalidates: ["files.get", ...POST_CONTENT]
233
- },
234
- {
235
- method: "POST",
236
- path: /^\/api\/verification\/submit$/,
237
- invalidates: ["verification.status"],
238
- scope: "installation"
239
- },
240
- {
241
- method: "POST",
242
- path: /^\/api\/v1\/subscription\/pay$/,
243
- invalidates: SUBSCRIPTION,
244
- scope: "installation"
245
- },
246
- {
247
- method: "POST",
248
- path: /^\/api\/v1\/subscription\/auto-renewal$/,
249
- invalidates: SUBSCRIPTION,
250
- scope: "installation"
251
- },
252
- {
253
- method: "POST",
254
- path: /^\/api\/v1\/subscription\/bind-card$/,
255
- invalidates: SUBSCRIPTION,
256
- scope: "installation"
257
- },
258
- {
259
- method: "POST",
260
- path: /^\/api\/v1\/subscription\/methods\/[^/]+\/default$/,
261
- invalidates: SUBSCRIPTION,
262
- scope: "installation"
263
- },
264
- {
265
- method: "DELETE",
266
- path: /^\/api\/v1\/subscription\/methods\/[^/]+$/,
267
- invalidates: SUBSCRIPTION,
268
- scope: "installation"
269
- },
270
- // Эти запросы не меняют ни один доступный для кэширования ответ.
271
- { method: "POST", path: /^\/api\/reports$/, invalidates: NOTHING },
272
- { method: "POST", path: /^\/api\/v1\/[ix]$/, invalidates: NOTHING }
273
- ]);
274
- function cacheMutation(method, path) {
275
- const normalized = method.toUpperCase();
276
- return CACHE_MUTATIONS.find(
277
- (mutation) => mutation.method === normalized && mutation.path.test(path)
278
- );
279
- }
280
-
281
- // src/routes.ts
282
- var CACHE_ROUTES = Object.freeze([
283
- { id: "auth.sessions", category: "auth", method: "GET", path: /^\/api\/v1\/auth\/sessions$/ },
284
- { id: "users.me", category: "users", method: "GET", path: /^\/api\/users\/me$/ },
285
- {
286
- id: "users.checkUsername",
287
- category: "users",
288
- method: "GET",
289
- path: /^\/api\/users\/check-username$/
290
- },
291
- { id: "users.search", category: "users", method: "GET", path: /^\/api\/users\/search$/ },
292
- {
293
- id: "users.whoToFollow",
294
- category: "users",
295
- method: "GET",
296
- path: /^\/api\/users\/suggestions\/who-to-follow$/
297
- },
298
- {
299
- id: "users.topClans",
300
- category: "users",
301
- method: "GET",
302
- path: /^\/api\/users\/stats\/top-clans$/
303
- },
304
- {
305
- id: "users.followers",
306
- category: "users",
307
- method: "GET",
308
- path: /^\/api\/users\/[^/]+\/followers$/
309
- },
310
- {
311
- id: "users.following",
312
- category: "users",
313
- method: "GET",
314
- path: /^\/api\/users\/[^/]+\/following$/
315
- },
316
- {
317
- id: "users.blocked",
318
- category: "users",
319
- method: "GET",
320
- path: /^\/api\/users\/me\/blocked$/
321
- },
322
- {
323
- id: "users.getPrivacy",
324
- category: "users",
325
- method: "GET",
326
- path: /^\/api\/users\/me\/privacy$/
327
- },
328
- { id: "users.pins", category: "users", method: "GET", path: /^\/api\/users\/me\/pins$/ },
329
- {
330
- id: "users.followStatus",
331
- category: "users",
332
- method: "POST",
333
- path: /^\/api\/users\/follow-status$/
334
- },
335
- { id: "users.get", category: "users", method: "GET", path: /^\/api\/users\/[^/]+$/ },
336
- { id: "posts.list", category: "posts", method: "GET", path: /^\/api\/posts$/ },
337
- {
338
- id: "posts.likedByUser",
339
- category: "posts",
340
- method: "GET",
341
- path: /^\/api\/posts\/user\/[^/]+\/liked$/
342
- },
343
- {
344
- id: "posts.byUser",
345
- category: "posts",
346
- method: "GET",
347
- path: /^\/api\/posts\/user\/[^/]+$/
348
- },
349
- {
350
- id: "posts.comments",
351
- category: "posts",
352
- method: "GET",
353
- path: /^\/api\/posts\/[^/]+\/comments$/
354
- },
355
- { id: "posts.stats", category: "posts", method: "POST", path: /^\/api\/posts\/stats$/ },
356
- { id: "posts.get", category: "posts", method: "GET", path: /^\/api\/posts\/[^/]+$/ },
357
- {
358
- id: "comments.replies",
359
- category: "comments",
360
- method: "GET",
361
- path: /^\/api\/comments\/[^/]+\/replies$/
362
- },
363
- {
364
- id: "notifications.list",
365
- category: "notifications",
366
- method: "GET",
367
- path: /^\/api\/notifications\/$/
368
- },
369
- {
370
- id: "notifications.count",
371
- category: "notifications",
372
- method: "GET",
373
- path: /^\/api\/notifications\/count$/
374
- },
375
- {
376
- id: "notifications.getSettings",
377
- category: "notifications",
378
- method: "GET",
379
- path: /^\/api\/notifications\/settings$/
380
- },
381
- {
382
- id: "hashtags.search",
383
- category: "hashtags",
384
- method: "GET",
385
- path: /^\/api\/hashtags$/
386
- },
387
- {
388
- id: "hashtags.trending",
389
- category: "hashtags",
390
- method: "GET",
391
- path: /^\/api\/hashtags\/trending$/
392
- },
393
- {
394
- id: "hashtags.posts",
395
- category: "hashtags",
396
- method: "GET",
397
- path: /^\/api\/hashtags\/[^/]+\/posts$/
398
- },
399
- { id: "search.all", category: "search", method: "GET", path: /^\/api\/search$/ },
400
- { id: "files.get", category: "files", method: "GET", path: /^\/api\/files\/[^/]+$/ },
401
- {
402
- id: "subscription.status",
403
- category: "subscription",
404
- method: "GET",
405
- path: /^\/api\/v1\/subscription\/$/
406
- },
407
- {
408
- id: "subscription.methods",
409
- category: "subscription",
410
- method: "GET",
411
- path: /^\/api\/v1\/subscription\/methods$/
412
- },
413
- {
414
- id: "verification.status",
415
- category: "verification",
416
- method: "GET",
417
- path: /^\/api\/verification\/status$/
418
- },
419
- {
420
- id: "platform.changelog",
421
- category: "platform",
422
- method: "GET",
423
- path: /^\/api\/platform\/changelog$/
424
- },
425
- {
426
- id: "platform.announcements",
427
- category: "platform",
428
- method: "GET",
429
- path: /^\/api\/platform\/announcements$/
430
- },
431
- { id: "platform.portal", category: "platform", method: "GET", path: /^\/api\/v1\/portal$/ },
432
- { id: "platform.status", category: "platform", method: "GET", path: /^\/api\/status$/ }
316
+ const NOTIFICATIONS = ["notifications.list", "notifications.count"];
317
+ const SUBSCRIPTION = ["subscription.status", "subscription.methods"];
318
+ const NOTHING = [];
319
+ /**
320
+ * Известные изменяющие запросы и читающие операции, чьи ответы они могут изменить.
321
+ *
322
+ * Каталог намеренно консервативен: лучше удалить несколько связанных списков, чем оставить
323
+ * персонализированное поле или вложенный объект устаревшим.
324
+ */
325
+ const CACHE_MUTATIONS = Object.freeze([
326
+ {
327
+ operationId: "auth.refresh",
328
+ invalidates: NOTHING
329
+ },
330
+ {
331
+ operationId: "auth.resendOtp",
332
+ invalidates: NOTHING
333
+ },
334
+ {
335
+ operationId: "auth.forgotPassword",
336
+ invalidates: NOTHING
337
+ },
338
+ {
339
+ operationId: "auth.signUp",
340
+ invalidates: "all"
341
+ },
342
+ {
343
+ operationId: "auth.signIn",
344
+ invalidates: "all"
345
+ },
346
+ {
347
+ operationId: "auth.verifyOtp",
348
+ invalidates: "all"
349
+ },
350
+ {
351
+ operationId: "auth.logout",
352
+ invalidates: "all"
353
+ },
354
+ {
355
+ operationId: "auth.resetPassword",
356
+ invalidates: "all"
357
+ },
358
+ {
359
+ operationId: "auth.changePassword",
360
+ invalidates: "all"
361
+ },
362
+ {
363
+ operationId: "auth.revokeSession",
364
+ invalidates: ["auth.sessions"],
365
+ scope: "account"
366
+ },
367
+ {
368
+ operationId: "auth.revokeOtherSessions",
369
+ invalidates: ["auth.sessions"],
370
+ scope: "account"
371
+ },
372
+ {
373
+ operationId: "posts.create",
374
+ invalidates: POST_CONTENT
375
+ },
376
+ {
377
+ operationId: "posts.update",
378
+ invalidates: POST_CONTENT
379
+ },
380
+ {
381
+ operationId: "posts.remove",
382
+ invalidates: POST_CONTENT
383
+ },
384
+ {
385
+ operationId: "posts.restore",
386
+ invalidates: POST_CONTENT
387
+ },
388
+ {
389
+ operationId: "posts.like",
390
+ invalidates: POST_REACTIONS
391
+ },
392
+ {
393
+ operationId: "posts.unlike",
394
+ invalidates: POST_REACTIONS
395
+ },
396
+ {
397
+ operationId: "posts.repost",
398
+ invalidates: POST_CONTENT
399
+ },
400
+ {
401
+ operationId: "posts.unrepost",
402
+ invalidates: POST_CONTENT
403
+ },
404
+ {
405
+ operationId: "posts.pin",
406
+ invalidates: PINS
407
+ },
408
+ {
409
+ operationId: "posts.unpin",
410
+ invalidates: PINS
411
+ },
412
+ {
413
+ operationId: "posts.vote",
414
+ invalidates: POST_REACTIONS
415
+ },
416
+ {
417
+ operationId: "posts.comment",
418
+ invalidates: COMMENTS
419
+ },
420
+ {
421
+ operationId: "comments.reply",
422
+ invalidates: COMMENTS
423
+ },
424
+ {
425
+ operationId: "comments.update",
426
+ invalidates: COMMENTS
427
+ },
428
+ {
429
+ operationId: "comments.remove",
430
+ invalidates: COMMENTS
431
+ },
432
+ {
433
+ operationId: "comments.restore",
434
+ invalidates: COMMENTS
435
+ },
436
+ {
437
+ operationId: "comments.like",
438
+ invalidates: COMMENTS
439
+ },
440
+ {
441
+ operationId: "comments.unlike",
442
+ invalidates: COMMENTS
443
+ },
444
+ {
445
+ operationId: "users.updateMe",
446
+ invalidates: PROFILE
447
+ },
448
+ {
449
+ operationId: "users.deactivate",
450
+ invalidates: PROFILE
451
+ },
452
+ {
453
+ operationId: "users.restore",
454
+ invalidates: PROFILE
455
+ },
456
+ {
457
+ operationId: "users.createProfile",
458
+ invalidates: PROFILE
459
+ },
460
+ {
461
+ operationId: "users.follow",
462
+ invalidates: FOLLOWING
463
+ },
464
+ {
465
+ operationId: "users.unfollow",
466
+ invalidates: FOLLOWING
467
+ },
468
+ {
469
+ operationId: "users.block",
470
+ invalidates: BLOCKS
471
+ },
472
+ {
473
+ operationId: "users.unblock",
474
+ invalidates: BLOCKS
475
+ },
476
+ {
477
+ operationId: "users.updatePrivacy",
478
+ invalidates: [
479
+ "users.me",
480
+ "users.get",
481
+ "users.getPrivacy"
482
+ ]
483
+ },
484
+ {
485
+ operationId: "users.setPin",
486
+ invalidates: PINS
487
+ },
488
+ {
489
+ operationId: "users.removePin",
490
+ invalidates: PINS
491
+ },
492
+ {
493
+ operationId: "notifications.markRead",
494
+ invalidates: NOTIFICATIONS,
495
+ scope: "account"
496
+ },
497
+ {
498
+ operationId: "notifications.markReadBatch",
499
+ invalidates: NOTIFICATIONS,
500
+ scope: "account"
501
+ },
502
+ {
503
+ operationId: "notifications.markAllRead",
504
+ invalidates: NOTIFICATIONS,
505
+ scope: "account"
506
+ },
507
+ {
508
+ operationId: "notifications.updateSettings",
509
+ invalidates: ["notifications.getSettings"],
510
+ scope: "account"
511
+ },
512
+ {
513
+ operationId: "files.upload",
514
+ invalidates: NOTHING
515
+ },
516
+ {
517
+ operationId: "files.remove",
518
+ invalidates: ["files.get", ...POST_CONTENT]
519
+ },
520
+ {
521
+ operationId: "verification.submit",
522
+ invalidates: ["verification.status"],
523
+ scope: "account"
524
+ },
525
+ {
526
+ operationId: "subscription.pay",
527
+ invalidates: SUBSCRIPTION,
528
+ scope: "account"
529
+ },
530
+ {
531
+ operationId: "subscription.setAutoRenewal",
532
+ invalidates: SUBSCRIPTION,
533
+ scope: "account"
534
+ },
535
+ {
536
+ operationId: "subscription.bindCard",
537
+ invalidates: SUBSCRIPTION,
538
+ scope: "account"
539
+ },
540
+ {
541
+ operationId: "subscription.setDefaultMethod",
542
+ invalidates: SUBSCRIPTION,
543
+ scope: "account"
544
+ },
545
+ {
546
+ operationId: "subscription.removeMethod",
547
+ invalidates: SUBSCRIPTION,
548
+ scope: "account"
549
+ },
550
+ {
551
+ operationId: "reports.create",
552
+ invalidates: NOTHING
553
+ },
554
+ {
555
+ operationId: "telemetry.dwell",
556
+ invalidates: NOTHING
557
+ },
558
+ {
559
+ operationId: "telemetry.interaction",
560
+ invalidates: NOTHING
561
+ }
433
562
  ]);
434
- var ROUTE_IDS = new Set(CACHE_ROUTES.map((route) => route.id));
435
- function isCacheRouteId(value) {
436
- return ROUTE_IDS.has(value);
563
+ for (const mutation of CACHE_MUTATIONS) {
564
+ Object.freeze(mutation.invalidates);
565
+ Object.freeze(mutation);
437
566
  }
438
- function cacheRoute(method, path) {
439
- const normalized = method.toUpperCase();
440
- return CACHE_ROUTES.find((route) => route.method === normalized && route.path.test(path));
567
+ const MUTATIONS = new Map(CACHE_MUTATIONS.map((mutation) => [mutation.operationId, mutation]));
568
+ /** Находит известную мутацию по стабильному семантическому ID. */
569
+ function cacheMutation(operationId) {
570
+ return MUTATIONS.get(operationId);
441
571
  }
442
-
443
- // src/plugin.ts
444
- var DEFAULT_MAX_ENTRIES = 500;
445
- var CACHE_MODES = /* @__PURE__ */ new Set(["default", "reload", "no-store"]);
572
+ //#endregion
573
+ //#region src/plugin.ts
574
+ /** Режимы кэширования отдельного запроса. */
575
+ const CacheModes = Object.freeze({
576
+ /** Отдать свежий кэш, иначе выполнить запрос и сохранить ответ. */
577
+ Default: "default",
578
+ /** Пропустить сохранённое значение, выполнить запрос и перезаписать кэш. */
579
+ Reload: "reload",
580
+ /** Не читать и не писать кэш для этого запроса. */
581
+ NoStore: "no-store"
582
+ });
583
+ const DEFAULT_MAX_ENTRIES = 500;
584
+ const CACHE_MODES = new Set(Object.values(CacheModes));
446
585
  function assertPositive(value, name, integer = false) {
447
- if (!Number.isFinite(value) || value <= 0 || integer && !Number.isInteger(value)) {
448
- throw new CacheError(
449
- `${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C ${integer ? "\u0446\u0435\u043B\u044B\u043C " : ""}\u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`
450
- );
451
- }
586
+ if (!Number.isFinite(value) || value <= 0 || integer && !Number.isInteger(value)) throw new CacheError(`${name} должен быть ${integer ? "целым " : ""}положительным числом, получено: ${value}`);
452
587
  }
453
588
  function resolveOptions(options) {
454
- if (!options || typeof options !== "object") {
455
- throw new CacheError("cache() \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043E\u0431\u044A\u0435\u043A\u0442 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A");
456
- }
457
- assertPositive(options.ttl, "cache.ttl");
458
- if (!Array.isArray(options.routes) || options.routes.length === 0) {
459
- throw new CacheError("cache.routes \u0434\u043E\u043B\u0436\u0435\u043D \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u043C\u0430\u0440\u0448\u0440\u0443\u0442");
460
- }
461
- const routes = /* @__PURE__ */ new Set();
462
- for (const route of options.routes) {
463
- if (typeof route !== "string" || !isCacheRouteId(route)) {
464
- throw new CacheError(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u043C\u0430\u0440\u0448\u0440\u0443\u0442 \u043A\u044D\u0448\u0430: ${JSON.stringify(route)}`);
465
- }
466
- routes.add(route);
467
- }
468
- const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
469
- assertPositive(maxEntries, "cache.maxEntries", true);
470
- const deduplicate = options.deduplicate ?? true;
471
- if (typeof deduplicate !== "boolean") {
472
- throw new CacheError(`cache.deduplicate \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C boolean, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${deduplicate}`);
473
- }
474
- return { ttl: options.ttl, routes, maxEntries, deduplicate };
589
+ if (!options || typeof options !== "object") throw new CacheError("cache() принимает объект настроек");
590
+ assertPositive(options.ttl, "cache.ttl");
591
+ if (!Array.isArray(options.operations) || options.operations.length === 0) throw new CacheError("cache.operations должен содержать хотя бы одну операцию");
592
+ const operations = /* @__PURE__ */ new Set();
593
+ for (const operation of options.operations) {
594
+ if (typeof operation !== "string" || !isCacheOperationId(operation)) throw new CacheError(`Неизвестная операция кэша: ${JSON.stringify(operation)}`);
595
+ operations.add(operation);
596
+ }
597
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
598
+ assertPositive(maxEntries, "cache.maxEntries", true);
599
+ const deduplicate = options.deduplicate ?? true;
600
+ if (typeof deduplicate !== "boolean") throw new CacheError(`cache.deduplicate должен быть boolean, получено: ${deduplicate}`);
601
+ return {
602
+ ttl: options.ttl,
603
+ operations,
604
+ maxEntries,
605
+ deduplicate
606
+ };
475
607
  }
476
608
  function cloneValue(value) {
477
- try {
478
- return { cacheable: true, value: structuredClone(value) };
479
- } catch {
480
- return { cacheable: false, value };
481
- }
609
+ try {
610
+ return {
611
+ cacheable: true,
612
+ value: structuredClone(value)
613
+ };
614
+ } catch {
615
+ return {
616
+ cacheable: false,
617
+ value
618
+ };
619
+ }
482
620
  }
483
621
  function cacheMode(request) {
484
- const mode = request.cache ?? "default";
485
- if (!CACHE_MODES.has(mode)) {
486
- throw new CacheError(
487
- `cache \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C 'default', 'reload' \u0438\u043B\u0438 'no-store', \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(mode)}`
488
- );
489
- }
490
- return mode;
622
+ const mode = request.extensions?.cache ?? CacheModes.Default;
623
+ if (!CACHE_MODES.has(mode)) throw new CacheError(`cache должен быть '${CacheModes.Default}', '${CacheModes.Reload}' или '${CacheModes.NoStore}', получено: ${String(mode)}`);
624
+ return mode;
491
625
  }
626
+ /** Создаёт TTL/LRU-кэш разобранных ответов itd-api. */
492
627
  function cache(options) {
493
- const config = resolveOptions(options);
494
- const values = new lruCache.LRUCache({
495
- max: config.maxEntries,
496
- ttl: config.ttl,
497
- updateAgeOnGet: false,
498
- allowStale: false
499
- });
500
- const pending = /* @__PURE__ */ new Map();
501
- const routeGenerations = /* @__PURE__ */ new Map();
502
- const installationGenerations = /* @__PURE__ */ new Map();
503
- const installationRouteGenerations = /* @__PURE__ */ new Map();
504
- const keyStates = /* @__PURE__ */ new Map();
505
- let generation = 0;
506
- let installationSequence = 0;
507
- const installationRouteKey = (installation, route) => `${installation}:${route}`;
508
- const clear = () => {
509
- generation += 1;
510
- values.clear();
511
- pending.clear();
512
- };
513
- const invalidate = (...routes) => {
514
- if (routes.length === 0) return;
515
- const selected = /* @__PURE__ */ new Set();
516
- for (const route of routes) {
517
- if (!isCacheRouteId(route)) {
518
- throw new CacheError(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u043C\u0430\u0440\u0448\u0440\u0443\u0442 \u043A\u044D\u0448\u0430: ${JSON.stringify(route)}`);
519
- }
520
- selected.add(route);
521
- routeGenerations.set(route, (routeGenerations.get(route) ?? 0) + 1);
522
- }
523
- for (const [key, entry] of values.entries()) {
524
- if (selected.has(entry.route)) values.delete(key);
525
- }
526
- for (const [key, entry] of pending) {
527
- if (selected.has(entry.route)) pending.delete(key);
528
- }
529
- };
530
- const clearInstallation = (installation) => {
531
- installationGenerations.set(installation, (installationGenerations.get(installation) ?? 0) + 1);
532
- for (const [key, entry] of values.entries()) {
533
- if (entry.installation === installation) values.delete(key);
534
- }
535
- for (const [key, entry] of pending) {
536
- if (entry.installation === installation) pending.delete(key);
537
- }
538
- };
539
- const invalidateInstallation = (installation, routes) => {
540
- if (routes.length === 0) return;
541
- const selected = new Set(routes);
542
- for (const route of selected) {
543
- const key = installationRouteKey(installation, route);
544
- installationRouteGenerations.set(key, (installationRouteGenerations.get(key) ?? 0) + 1);
545
- }
546
- for (const [key, entry] of values.entries()) {
547
- if (entry.installation === installation && selected.has(entry.route)) values.delete(key);
548
- }
549
- for (const [key, entry] of pending) {
550
- if (entry.installation === installation && selected.has(entry.route)) pending.delete(key);
551
- }
552
- };
553
- const applyMutation = (installation, mutation) => {
554
- if (mutation.invalidates === "all") {
555
- clearInstallation(installation);
556
- } else if (mutation.scope === "installation") {
557
- invalidateInstallation(installation, mutation.invalidates);
558
- } else {
559
- invalidate(...mutation.invalidates);
560
- }
561
- };
562
- const createTransformer = (installation, getAuthScope) => {
563
- let observedAuthScope;
564
- const resolveScope = () => {
565
- const authScope = getAuthScope ? getAuthScope() : "default";
566
- if (observedAuthScope !== void 0 && observedAuthScope !== authScope) {
567
- clearInstallation(installation);
568
- }
569
- observedAuthScope = authScope;
570
- return JSON.stringify([installation, authScope]);
571
- };
572
- return async (rawRequest, next) => {
573
- const request = rawRequest;
574
- const route = cacheRoute(request.method, request.path);
575
- const method = request.method.toUpperCase();
576
- const isRead = route !== void 0 || method === "GET" || method === "HEAD";
577
- if (!isRead) {
578
- const result = await next(request);
579
- const mutation = cacheMutation(method, request.path);
580
- if (mutation) applyMutation(installation, mutation);
581
- else clear();
582
- return result;
583
- }
584
- if (!route || !config.routes.has(route.id)) return next(request);
585
- const mode = cacheMode(request);
586
- if (mode === "no-store") return next(request);
587
- const unscopedKey = buildCacheKey(route.id, request);
588
- if (unscopedKey === void 0) return next(request);
589
- const scope = resolveScope();
590
- const key = JSON.stringify([scope, unscopedKey]);
591
- if (mode === "reload") {
592
- const state = keyStates.get(key) ?? { active: 0, generation: 0 };
593
- state.generation += 1;
594
- keyStates.set(key, state);
595
- values.delete(key);
596
- pending.delete(key);
597
- }
598
- if (mode === "default") {
599
- const hit = values.get(key);
600
- if (hit) {
601
- const cloned = cloneValue(hit.value);
602
- if (cloned.cacheable) return cloned.value;
603
- values.delete(key);
604
- }
605
- const existing = pending.get(key);
606
- if (config.deduplicate && request.signal === void 0 && request.timeout === void 0 && existing) {
607
- const loaded = await existing.promise;
608
- return cloneValue(loaded.value).value;
609
- }
610
- }
611
- const startedGeneration = generation;
612
- const startedInstallationGeneration = installationGenerations.get(installation) ?? 0;
613
- const startedRouteGeneration = routeGenerations.get(route.id) ?? 0;
614
- const scopedRouteKey = installationRouteKey(installation, route.id);
615
- const startedInstallationRouteGeneration = installationRouteGenerations.get(scopedRouteKey) ?? 0;
616
- const keyState = keyStates.get(key) ?? { active: 0, generation: 0 };
617
- keyState.active += 1;
618
- keyStates.set(key, keyState);
619
- const startedKeyGeneration = keyState.generation;
620
- const load = (async () => {
621
- try {
622
- const result = await next(request);
623
- const stored = cloneValue(result);
624
- const currentScope = resolveScope();
625
- if (stored.cacheable && currentScope === scope && generation === startedGeneration && (installationGenerations.get(installation) ?? 0) === startedInstallationGeneration && (routeGenerations.get(route.id) ?? 0) === startedRouteGeneration && (installationRouteGenerations.get(scopedRouteKey) ?? 0) === startedInstallationRouteGeneration && keyState.generation === startedKeyGeneration) {
626
- values.set(key, { installation, route: route.id, value: stored.value });
627
- }
628
- return { cacheable: stored.cacheable, value: result };
629
- } finally {
630
- keyState.active -= 1;
631
- if (keyState.active === 0 && keyStates.get(key) === keyState) keyStates.delete(key);
632
- }
633
- })();
634
- const mayDeduplicate = mode === "default" && config.deduplicate && request.signal === void 0 && request.timeout === void 0;
635
- const entry = { installation, route: route.id, promise: load };
636
- if (mayDeduplicate) pending.set(key, entry);
637
- try {
638
- const loaded = await load;
639
- return loaded.value;
640
- } finally {
641
- if (pending.get(key) === entry) pending.delete(key);
642
- }
643
- };
644
- };
645
- return {
646
- name: "cache",
647
- optionKeys: ["cache"],
648
- get size() {
649
- values.purgeStale();
650
- return values.size;
651
- },
652
- clear,
653
- invalidate,
654
- attachRealtime(stream) {
655
- if (!stream || typeof stream.on !== "function") {
656
- throw new CacheError("attachRealtime() \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043F\u043E\u0442\u043E\u043A \u0438\u0437 itd.realtime()");
657
- }
658
- invalidate("notifications.list", "notifications.count");
659
- const offNotification = stream.on(
660
- "notification",
661
- () => invalidate("notifications.list", "notifications.count")
662
- );
663
- const offUnreadCount = stream.on("unreadCount", () => invalidate("notifications.count"));
664
- return () => {
665
- offNotification();
666
- offUnreadCount();
667
- };
668
- },
669
- install({ use, getAuthScope }) {
670
- installationSequence += 1;
671
- use(createTransformer(installationSequence, getAuthScope));
672
- }
673
- };
628
+ const config = resolveOptions(options);
629
+ const values = new lru_cache.LRUCache({
630
+ max: config.maxEntries,
631
+ ttl: config.ttl,
632
+ updateAgeOnGet: false,
633
+ allowStale: false
634
+ });
635
+ const pending = /* @__PURE__ */ new Map();
636
+ const operationGenerations = /* @__PURE__ */ new Map();
637
+ const scopeGenerations = /* @__PURE__ */ new Map();
638
+ const scopeOperationGenerations = /* @__PURE__ */ new Map();
639
+ const keyStates = /* @__PURE__ */ new Map();
640
+ let generation = 0;
641
+ let installationSequence = 0;
642
+ const scopeOperationKey = (accountScope, operation) => JSON.stringify([accountScope, operation]);
643
+ const clear = () => {
644
+ generation += 1;
645
+ values.clear();
646
+ pending.clear();
647
+ };
648
+ const invalidate = (...operations) => {
649
+ if (operations.length === 0) return;
650
+ const selected = /* @__PURE__ */ new Set();
651
+ for (const operation of operations) {
652
+ if (!isCacheOperationId(operation)) throw new CacheError(`Неизвестная операция кэша: ${JSON.stringify(operation)}`);
653
+ selected.add(operation);
654
+ operationGenerations.set(operation, (operationGenerations.get(operation) ?? 0) + 1);
655
+ }
656
+ for (const [key, entry] of values.entries()) if (selected.has(entry.operation)) values.delete(key);
657
+ for (const [key, entry] of pending) if (selected.has(entry.operation)) pending.delete(key);
658
+ };
659
+ const clearScope = (accountScope) => {
660
+ scopeGenerations.set(accountScope, (scopeGenerations.get(accountScope) ?? 0) + 1);
661
+ for (const [key, entry] of values.entries()) if (entry.accountScope === accountScope) values.delete(key);
662
+ for (const [key, entry] of pending) if (entry.accountScope === accountScope) pending.delete(key);
663
+ };
664
+ const invalidateScope = (accountScope, operations) => {
665
+ if (operations.length === 0) return;
666
+ const selected = new Set(operations);
667
+ for (const operation of selected) {
668
+ const key = scopeOperationKey(accountScope, operation);
669
+ scopeOperationGenerations.set(key, (scopeOperationGenerations.get(key) ?? 0) + 1);
670
+ }
671
+ for (const [key, entry] of values.entries()) if (entry.accountScope === accountScope && selected.has(entry.operation)) values.delete(key);
672
+ for (const [key, entry] of pending) if (entry.accountScope === accountScope && selected.has(entry.operation)) pending.delete(key);
673
+ };
674
+ const applyMutation = (accountScope, mutation) => {
675
+ if (mutation.invalidates === "all") clearScope(accountScope);
676
+ else if (mutation.scope === "account") invalidateScope(accountScope, mutation.invalidates);
677
+ else invalidate(...mutation.invalidates);
678
+ };
679
+ const createTransformer = (installation, baseUrl, getAuthIdentity, getAuthScope) => {
680
+ const fallbackAuthScope = JSON.stringify([baseUrl, `installation:${installation}`]);
681
+ const resolveIdentity = async () => {
682
+ const identity = await getAuthIdentity?.();
683
+ const accountScope = identity?.userId ? JSON.stringify([baseUrl, identity.userId]) : getAuthScope ? JSON.stringify([baseUrl, getAuthScope()]) : fallbackAuthScope;
684
+ return {
685
+ accountScope,
686
+ sessionScope: identity?.userId && identity.sessionId ? JSON.stringify([
687
+ baseUrl,
688
+ identity.userId,
689
+ identity.sessionId
690
+ ]) : JSON.stringify([accountScope, getAuthScope ? getAuthScope() : `installation:${installation}`])
691
+ };
692
+ };
693
+ return async (request, next) => {
694
+ const operation = cacheOperation(request.operationId);
695
+ const method = request.method.toUpperCase();
696
+ if (!(operation !== void 0 || method === "GET" || method === "HEAD")) {
697
+ const mutation = cacheMutation(request.operationId);
698
+ const startedIdentity = mutation ? await resolveIdentity() : void 0;
699
+ const result = await next(request);
700
+ if (mutation && startedIdentity) {
701
+ applyMutation(startedIdentity.accountScope, mutation);
702
+ const currentIdentity = await resolveIdentity();
703
+ if (currentIdentity.accountScope !== startedIdentity.accountScope && (mutation.invalidates === "all" || mutation.scope === "account")) applyMutation(currentIdentity.accountScope, mutation);
704
+ } else clear();
705
+ return result;
706
+ }
707
+ if (!operation || !config.operations.has(operation.id)) return next(request);
708
+ const mode = cacheMode(request);
709
+ if (mode === CacheModes.NoStore) return next(request);
710
+ const unscopedKey = buildCacheKey(operation.id, request);
711
+ if (unscopedKey === void 0) return next(request);
712
+ const identity = await resolveIdentity();
713
+ const scope = operation.id === "auth.sessions" ? identity.sessionScope : identity.accountScope;
714
+ const key = JSON.stringify([scope, unscopedKey]);
715
+ if (mode === CacheModes.Reload) {
716
+ const state = keyStates.get(key) ?? {
717
+ active: 0,
718
+ generation: 0
719
+ };
720
+ state.generation += 1;
721
+ keyStates.set(key, state);
722
+ values.delete(key);
723
+ pending.delete(key);
724
+ }
725
+ if (mode === CacheModes.Default) {
726
+ const hit = values.get(key);
727
+ if (hit) {
728
+ const cloned = cloneValue(hit.value);
729
+ if (cloned.cacheable) return cloned.value;
730
+ values.delete(key);
731
+ }
732
+ const existing = pending.get(key);
733
+ if (config.deduplicate && request.signal === void 0 && request.timeout === void 0 && existing) return cloneValue((await existing.promise).value).value;
734
+ }
735
+ const startedGeneration = generation;
736
+ const startedScopeGeneration = scopeGenerations.get(identity.accountScope) ?? 0;
737
+ const startedOperationGeneration = operationGenerations.get(operation.id) ?? 0;
738
+ const scopedOperationKey = scopeOperationKey(identity.accountScope, operation.id);
739
+ const startedScopeOperationGeneration = scopeOperationGenerations.get(scopedOperationKey) ?? 0;
740
+ const keyState = keyStates.get(key) ?? {
741
+ active: 0,
742
+ generation: 0
743
+ };
744
+ keyState.active += 1;
745
+ keyStates.set(key, keyState);
746
+ const startedKeyGeneration = keyState.generation;
747
+ const load = (async () => {
748
+ try {
749
+ const result = await next(request);
750
+ const stored = cloneValue(result);
751
+ const currentIdentity = await resolveIdentity();
752
+ const currentScope = operation.id === "auth.sessions" ? currentIdentity.sessionScope : currentIdentity.accountScope;
753
+ if (stored.cacheable && currentScope === scope && generation === startedGeneration && (scopeGenerations.get(identity.accountScope) ?? 0) === startedScopeGeneration && (operationGenerations.get(operation.id) ?? 0) === startedOperationGeneration && (scopeOperationGenerations.get(scopedOperationKey) ?? 0) === startedScopeOperationGeneration && keyState.generation === startedKeyGeneration) values.set(key, {
754
+ accountScope: identity.accountScope,
755
+ operation: operation.id,
756
+ value: stored.value
757
+ });
758
+ return {
759
+ cacheable: stored.cacheable,
760
+ value: result
761
+ };
762
+ } finally {
763
+ keyState.active -= 1;
764
+ if (keyState.active === 0 && keyStates.get(key) === keyState) keyStates.delete(key);
765
+ }
766
+ })();
767
+ const mayDeduplicate = mode === CacheModes.Default && config.deduplicate && request.signal === void 0 && request.timeout === void 0;
768
+ const entry = {
769
+ accountScope: identity.accountScope,
770
+ operation: operation.id,
771
+ promise: load
772
+ };
773
+ if (mayDeduplicate) pending.set(key, entry);
774
+ try {
775
+ return (await load).value;
776
+ } finally {
777
+ if (pending.get(key) === entry) pending.delete(key);
778
+ }
779
+ };
780
+ };
781
+ return {
782
+ name: "cache",
783
+ get size() {
784
+ values.purgeStale();
785
+ return values.size;
786
+ },
787
+ clear,
788
+ invalidate,
789
+ attachRealtime(stream) {
790
+ if (!stream || typeof stream.on !== "function") throw new CacheError("attachRealtime() принимает поток из itd.realtime()");
791
+ const invalidateStream = (...operations) => {
792
+ const identity = typeof stream.getAuthIdentity === "function" ? stream.getAuthIdentity() : void 0;
793
+ const streamBaseUrl = typeof stream.baseUrl === "string" && stream.baseUrl.length > 0 ? stream.baseUrl : void 0;
794
+ const legacyScope = typeof stream.getAuthScope === "function" ? stream.getAuthScope() : void 0;
795
+ const accountScope = identity?.userId && streamBaseUrl ? JSON.stringify([streamBaseUrl, identity.userId]) : legacyScope !== void 0 && streamBaseUrl ? JSON.stringify([streamBaseUrl, legacyScope]) : void 0;
796
+ if (accountScope === void 0) invalidate(...operations);
797
+ else invalidateScope(accountScope, operations);
798
+ };
799
+ invalidateStream("notifications.list", "notifications.count");
800
+ const offNotification = stream.on("notification", () => invalidateStream("notifications.list", "notifications.count"));
801
+ const offUnreadCount = stream.on("unreadCount", () => invalidateStream("notifications.count"));
802
+ return () => {
803
+ offNotification();
804
+ offUnreadCount();
805
+ };
806
+ },
807
+ install({ operations, baseUrl, getAuthIdentity, getAuthScope }) {
808
+ installationSequence += 1;
809
+ operations.use(createTransformer(installationSequence, baseUrl, getAuthIdentity, getAuthScope));
810
+ }
811
+ };
674
812
  }
675
-
676
- exports.CACHE_ROUTES = CACHE_ROUTES;
813
+ //#endregion
814
+ exports.CACHE_OPERATIONS = CACHE_OPERATIONS;
677
815
  exports.CacheError = CacheError;
816
+ exports.CacheModes = CacheModes;
678
817
  exports.buildCacheKey = buildCacheKey;
679
818
  exports.cache = cache;
680
- exports.cacheRoute = cacheRoute;
681
- exports.isCacheRouteId = isCacheRouteId;
682
- //# sourceMappingURL=index.cjs.map
819
+ exports.cacheOperation = cacheOperation;
820
+ exports.isCacheOperationId = isCacheOperationId;
821
+
683
822
  //# sourceMappingURL=index.cjs.map