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