synapse-storage 3.0.5 → 3.0.8

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/api.js CHANGED
@@ -1 +1,859 @@
1
- export{e as ApiClient,d as ResponseFormat,a as apiLogger,b as createUniqueId,c as headersToObject}from'./chunk-4USKKL5R.js';import'./chunk-ZE2EJX2Y.js';
1
+ // src/api/utils/api-helpers.ts
2
+ var apiLogger = {
3
+ debug: (message, ...args) => {
4
+ if (process.env.NODE_ENV !== "production") {
5
+ console.debug(`[API] ${message}`, ...args);
6
+ }
7
+ },
8
+ log: (message, ...args) => {
9
+ if (process.env.NODE_ENV !== "production") {
10
+ console.log(`[API] ${message}`, ...args);
11
+ }
12
+ },
13
+ info: (message, ...args) => {
14
+ console.info(`[API] ${message}`, ...args);
15
+ },
16
+ warn: (message, ...args) => {
17
+ console.warn(`[API] ${message}`, ...args);
18
+ },
19
+ error: (message, ...args) => {
20
+ console.error(`[API] ${message}`, ...args);
21
+ }
22
+ };
23
+ function createUniqueId(name) {
24
+ return `${name ? `${name}|` : ""}${Math.random().toString(36).substring(2, 9) + Date.now().toString(36)}`;
25
+ }
26
+ function headersToObject(headers) {
27
+ const result = {};
28
+ headers.forEach((value, key) => {
29
+ result[key.toLowerCase()] = value;
30
+ });
31
+ return result;
32
+ }
33
+
34
+ // src/api/utils/create-header-context.ts
35
+ function createHeaderContext(context = {}, optionContext = {}) {
36
+ return {
37
+ ...context,
38
+ ...optionContext,
39
+ getFromStorage: context.getFromStorage || ((key) => {
40
+ try {
41
+ const item = localStorage.getItem(key);
42
+ return item ? JSON.parse(item) : void 0;
43
+ } catch (error) {
44
+ console.warn(`[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0438\u0437 localStorage: ${error}`);
45
+ return void 0;
46
+ }
47
+ }),
48
+ getCookie: context.getCookie || ((name) => {
49
+ try {
50
+ const matches = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, "\\$1")}=([^;]*)`));
51
+ return matches ? decodeURIComponent(matches[1]) : void 0;
52
+ } catch (error) {
53
+ console.warn(`[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F cookie: ${error}`);
54
+ return void 0;
55
+ }
56
+ })
57
+ };
58
+ }
59
+
60
+ // src/api/utils/endpoint-headers.ts
61
+ async function prepareRequestHeaders(prepareHeadersFn, context) {
62
+ let headers = new Headers();
63
+ const headerContext = context || createHeaderContext({}, {});
64
+ if (prepareHeadersFn) {
65
+ try {
66
+ headers = await Promise.resolve(prepareHeadersFn(headers, headerContext));
67
+ } catch (error) {
68
+ console.warn("[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432", error);
69
+ }
70
+ }
71
+ return headers;
72
+ }
73
+ function createPrepareHeaders(globalPrepareHeaders, endpointPrepareHeaders) {
74
+ return async (headers, context) => {
75
+ let processedHeaders = new Headers(headers);
76
+ if (globalPrepareHeaders) {
77
+ try {
78
+ processedHeaders = await Promise.resolve(globalPrepareHeaders(processedHeaders, context));
79
+ } catch (error) {
80
+ console.warn("[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0445 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432", error);
81
+ }
82
+ }
83
+ if (endpointPrepareHeaders) {
84
+ try {
85
+ processedHeaders = await Promise.resolve(endpointPrepareHeaders(processedHeaders, context));
86
+ } catch (error) {
87
+ console.warn("[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432 \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u0430", error);
88
+ }
89
+ }
90
+ return processedHeaders;
91
+ };
92
+ }
93
+
94
+ // src/api/types/api.interface.ts
95
+ var ResponseFormat = /* @__PURE__ */ ((ResponseFormat2) => {
96
+ ResponseFormat2["Json"] = "json";
97
+ ResponseFormat2["Blob"] = "blob";
98
+ ResponseFormat2["ArrayBuffer"] = "arrayBuffer";
99
+ ResponseFormat2["Text"] = "text";
100
+ ResponseFormat2["FormData"] = "formData";
101
+ ResponseFormat2["Raw"] = "raw";
102
+ return ResponseFormat2;
103
+ })(ResponseFormat || {});
104
+
105
+ // src/api/utils/file-utils.ts
106
+ function getResponseFormatForMimeType(contentType) {
107
+ const type = contentType.toLowerCase().split(";")[0].trim();
108
+ if (type.includes("application/json")) {
109
+ return "json" /* Json */;
110
+ }
111
+ if (type.includes("text/")) {
112
+ return "text" /* Text */;
113
+ }
114
+ if (type.includes("multipart/form-data")) {
115
+ return "formData" /* FormData */;
116
+ }
117
+ if (type.includes("application/octet-stream") || type.includes("application/pdf") || type.includes("image/") || type.includes("audio/") || type.includes("video/")) {
118
+ return "blob" /* Blob */;
119
+ }
120
+ return void 0;
121
+ }
122
+ function isFileResponse(headers) {
123
+ const contentType = headers.get("content-type") || "";
124
+ const contentDisposition = headers.get("content-disposition") || "";
125
+ const isFileContentType = contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("image/") || contentType.includes("audio/") || contentType.includes("video/");
126
+ const isAttachment = contentDisposition.includes("attachment") || contentDisposition.includes("filename=");
127
+ return isFileContentType || isAttachment;
128
+ }
129
+ function extractFilenameFromHeaders(headers) {
130
+ const contentDisposition = headers.get("content-disposition");
131
+ if (!contentDisposition) return void 0;
132
+ const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
133
+ if (filenameMatch && filenameMatch[1]) {
134
+ return filenameMatch[1].replace(/['"]/g, "").trim();
135
+ }
136
+ return void 0;
137
+ }
138
+ function getFileMetadataFromHeaders(headers) {
139
+ const contentType = headers.get("content-type") || "";
140
+ const contentDisposition = headers.get("content-disposition") || "";
141
+ const contentLength = headers.get("content-length");
142
+ if (!isFileResponse(headers)) {
143
+ return void 0;
144
+ }
145
+ const filename = extractFilenameFromHeaders(headers);
146
+ return {
147
+ filename,
148
+ contentType,
149
+ contentDisposition,
150
+ size: contentLength ? parseInt(contentLength, 10) : void 0
151
+ };
152
+ }
153
+
154
+ // src/api/utils/fetch-base-query.ts
155
+ async function getResponseData(response, format) {
156
+ let responseFormat = format;
157
+ const contentType = response.headers.get("content-type") || "";
158
+ if (!responseFormat && contentType) {
159
+ if (isFileResponse(response.headers)) {
160
+ responseFormat = "blob" /* Blob */;
161
+ } else {
162
+ responseFormat = getResponseFormatForMimeType(contentType);
163
+ }
164
+ }
165
+ if (!responseFormat) {
166
+ responseFormat = "json" /* Json */;
167
+ }
168
+ try {
169
+ let fileMetadata;
170
+ if (responseFormat === "blob" /* Blob */ || responseFormat === "arrayBuffer" /* ArrayBuffer */) {
171
+ fileMetadata = getFileMetadataFromHeaders(response.headers);
172
+ }
173
+ switch (responseFormat) {
174
+ case "json" /* Json */: {
175
+ try {
176
+ const data = await response.json();
177
+ return response.ok ? { data, fileMetadata } : { error: data, fileMetadata };
178
+ } catch (error) {
179
+ const text = await response.text();
180
+ return response.ok ? { data: text, fileMetadata } : { error: text, fileMetadata };
181
+ }
182
+ }
183
+ case "text" /* Text */: {
184
+ const text = await response.text();
185
+ return response.ok ? { data: text, fileMetadata } : { error: text, fileMetadata };
186
+ }
187
+ case "blob" /* Blob */: {
188
+ const blob2 = await response.blob();
189
+ return response.ok ? { data: blob2, fileMetadata } : { error: blob2, fileMetadata };
190
+ }
191
+ case "arrayBuffer" /* ArrayBuffer */: {
192
+ const buffer = await response.arrayBuffer();
193
+ return response.ok ? { data: buffer, fileMetadata } : { error: buffer, fileMetadata };
194
+ }
195
+ case "formData" /* FormData */: {
196
+ const formData = await response.formData();
197
+ return response.ok ? { data: formData, fileMetadata } : { error: formData, fileMetadata };
198
+ }
199
+ case "raw" /* Raw */: {
200
+ return response.ok ? { data: response, fileMetadata } : { error: response, fileMetadata };
201
+ }
202
+ default:
203
+ const blob = await response.blob();
204
+ return response.ok ? { data: blob, fileMetadata } : { error: blob, fileMetadata };
205
+ }
206
+ } catch (err) {
207
+ console.error(`[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u0438\u0437\u0432\u043B\u0435\u0447\u0435\u043D\u0438\u044F \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 (\u0444\u043E\u0440\u043C\u0430\u0442: ${responseFormat})`, err);
208
+ return response.ok ? { data: void 0 } : { error: err };
209
+ }
210
+ }
211
+ function fetchBaseQuery(options) {
212
+ const { baseUrl, timeout = 3e4, fetchFn = fetch, credentials = "same-origin" } = options;
213
+ return async (args, queryOptions = {}, headers) => {
214
+ const { path, method, body, query, responseFormat: reqResponseFormat } = args;
215
+ const { signal, timeout: requestTimeout = timeout, responseFormat: optResponseFormat } = queryOptions;
216
+ const responseFormat = optResponseFormat || reqResponseFormat;
217
+ const url = new URL(path.startsWith("http") ? path : `${baseUrl}${path}`);
218
+ if (query) {
219
+ Object.entries(query).forEach(([key, value]) => {
220
+ if (value !== void 0 && value !== null) {
221
+ if (Array.isArray(value)) {
222
+ value.forEach((item) => url.searchParams.append(key, String(item)));
223
+ } else {
224
+ url.searchParams.append(key, String(value));
225
+ }
226
+ }
227
+ });
228
+ }
229
+ let serializedBody;
230
+ if (body !== void 0) {
231
+ if (body instanceof FormData || body instanceof Blob) {
232
+ serializedBody = body;
233
+ } else if (typeof body === "object" && body !== null) {
234
+ try {
235
+ serializedBody = JSON.stringify(body);
236
+ if (!headers.has("Content-Type")) {
237
+ headers.set("Content-Type", "application/json");
238
+ }
239
+ } catch (error) {
240
+ console.error("[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 \u0442\u0435\u043B\u0430 \u0437\u0430\u043F\u0440\u043E\u0441\u0430", error);
241
+ serializedBody = String(body);
242
+ }
243
+ } else {
244
+ serializedBody = String(body);
245
+ }
246
+ }
247
+ let timeoutId;
248
+ const timeoutPromise = new Promise((_, reject) => {
249
+ if (requestTimeout) {
250
+ timeoutId = window.setTimeout(() => {
251
+ reject(new Error(`\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430 (${requestTimeout}\u043C\u0441)`));
252
+ }, requestTimeout);
253
+ }
254
+ });
255
+ try {
256
+ const fetchPromise = fetchFn(url.toString(), {
257
+ method,
258
+ headers,
259
+ body: serializedBody,
260
+ signal,
261
+ credentials
262
+ });
263
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
264
+ const { data, error, fileMetadata } = await getResponseData(response, responseFormat);
265
+ const result = {
266
+ data,
267
+ error,
268
+ ok: response.ok,
269
+ status: response.status,
270
+ statusText: response.statusText,
271
+ headers: response.headers,
272
+ fileDownloadResult: fileMetadata
273
+ };
274
+ return result;
275
+ } catch (err) {
276
+ const error = err;
277
+ console.error("[API] \u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430", error);
278
+ return {
279
+ error,
280
+ ok: false,
281
+ status: 0,
282
+ statusText: error.message,
283
+ headers: new Headers()
284
+ };
285
+ } finally {
286
+ if (timeoutId) {
287
+ window.clearTimeout(timeoutId);
288
+ }
289
+ }
290
+ };
291
+ }
292
+
293
+ // src/api/utils/get-cacheable-headers.ts
294
+ function getCacheableHeaders(headers, cacheableHeaders = []) {
295
+ const result = {};
296
+ if (!headers || cacheableHeaders.length === 0) {
297
+ return result;
298
+ }
299
+ cacheableHeaders.forEach((key) => {
300
+ if (headers.has(key)) {
301
+ result[key] = headers.get(key) || "";
302
+ }
303
+ });
304
+ return result;
305
+ }
306
+
307
+ // src/api/components/endpoint.ts
308
+ var EndpointClass = class {
309
+ constructor(name, queryStorage, configCurrentEndpoint, cacheableHeaderKeys, globalCacheConfig, baseQueryConfig) {
310
+ this.name = name;
311
+ this.queryStorage = queryStorage;
312
+ this.configCurrentEndpoint = configCurrentEndpoint;
313
+ this.cacheableHeaderKeys = cacheableHeaderKeys;
314
+ this.globalCacheConfig = globalCacheConfig;
315
+ this.baseQueryConfig = baseQueryConfig;
316
+ this.prepareHeaders = createPrepareHeaders(baseQueryConfig.prepareHeaders, configCurrentEndpoint.prepareHeaders);
317
+ this.queryFunction = fetchBaseQuery({
318
+ baseUrl: baseQueryConfig.baseUrl,
319
+ fetchFn: baseQueryConfig.fetchFn,
320
+ timeout: baseQueryConfig.timeout,
321
+ credentials: baseQueryConfig.credentials
322
+ });
323
+ this.cacheableHeaders = [...cacheableHeaderKeys || [], ...configCurrentEndpoint.includeCacheableHeaderKeys || []].filter(
324
+ (key) => !configCurrentEndpoint.excludeCacheableHeaderKeys?.includes(key)
325
+ );
326
+ this.meta.name = name;
327
+ this.meta.tags = configCurrentEndpoint.tags ?? this.meta.tags;
328
+ this.meta.invalidatesTags = configCurrentEndpoint.invalidatesTags ?? this.meta.invalidatesTags;
329
+ this.meta.cache = this.queryStorage.createCacheConfig(this.configCurrentEndpoint) ?? this.meta.cache;
330
+ }
331
+ endpointSubscribers = /* @__PURE__ */ new Set();
332
+ /** Сколько раз был вызван метод request */
333
+ fetchCounts = 0;
334
+ meta = {
335
+ cache: false,
336
+ invalidatesTags: [],
337
+ name: "",
338
+ tags: []
339
+ };
340
+ queryFunction;
341
+ /** Массив заголовков, которые нужно включить в ключ кэширования */
342
+ cacheableHeaders;
343
+ prepareHeaders;
344
+ request(params, options) {
345
+ this.fetchCounts++;
346
+ const requestId = createUniqueId(this.name);
347
+ const controller = new AbortController();
348
+ const requestSubscribers = /* @__PURE__ */ new Set();
349
+ const currentState = {
350
+ status: "idle",
351
+ requestParams: params,
352
+ headers: {},
353
+ error: void 0,
354
+ data: void 0,
355
+ fromCache: false
356
+ };
357
+ const headerContext = createHeaderContext({ requestParams: params }, options?.context || {});
358
+ const notifyRequestSubscribers = (newState) => {
359
+ Object.assign(currentState, newState);
360
+ requestSubscribers.forEach((cb) => {
361
+ cb({ ...currentState });
362
+ });
363
+ };
364
+ const waitPromise = new Promise(async (resolve, reject) => {
365
+ try {
366
+ const headers = await prepareRequestHeaders(this.prepareHeaders, headerContext);
367
+ const headersForCache = getCacheableHeaders(headers, options?.cacheableHeaderKeys ? options.cacheableHeaderKeys : this.cacheableHeaders);
368
+ const shouldCache = this.queryStorage.shouldCache(this.configCurrentEndpoint, options);
369
+ const [cacheKey, cacheParams] = this.queryStorage.createCacheKey(this.name, { ...params, ...headersForCache });
370
+ let cachedResult;
371
+ if (shouldCache) {
372
+ cachedResult = await this.queryStorage.getCachedResult(cacheKey);
373
+ }
374
+ if (cachedResult) {
375
+ notifyRequestSubscribers({
376
+ fromCache: true,
377
+ status: "success",
378
+ data: cachedResult.data,
379
+ error: void 0,
380
+ headers: cachedResult.headers,
381
+ requestParams: params
382
+ });
383
+ resolve({
384
+ ...cachedResult,
385
+ fromCache: true
386
+ });
387
+ } else {
388
+ notifyRequestSubscribers({
389
+ fromCache: false,
390
+ status: "loading"
391
+ });
392
+ const requestDefinition = this.configCurrentEndpoint.request(params, options?.context);
393
+ const mergedOptions = { ...options, signal: controller.signal };
394
+ const response = await this.queryFunction(requestDefinition, mergedOptions, headers);
395
+ if (response.ok) {
396
+ const { headers: headers2, ...restResponse } = response;
397
+ if (shouldCache) {
398
+ const currentCacheConfig = this.queryStorage.createCacheConfig(this.configCurrentEndpoint);
399
+ await this.queryStorage.setCachedResult(
400
+ cacheKey,
401
+ { ...restResponse, headers: headersToObject(headers2) },
402
+ currentCacheConfig,
403
+ cacheParams ?? {},
404
+ this.configCurrentEndpoint.tags ?? [],
405
+ this.configCurrentEndpoint.invalidatesTags ?? []
406
+ );
407
+ }
408
+ notifyRequestSubscribers({
409
+ fromCache: false,
410
+ status: "success",
411
+ data: response.data,
412
+ error: void 0,
413
+ headers: response.headers,
414
+ requestParams: params
415
+ });
416
+ this.endpointSubscribers.forEach((cb) => {
417
+ const endpointState = {
418
+ status: "success",
419
+ fetchCounts: this.fetchCounts,
420
+ meta: this.meta,
421
+ cacheableHeaders: this.cacheableHeaders,
422
+ error: void 0
423
+ };
424
+ cb(endpointState);
425
+ });
426
+ resolve({
427
+ ...response,
428
+ fromCache: false
429
+ });
430
+ } else {
431
+ notifyRequestSubscribers({
432
+ fromCache: false,
433
+ status: "error",
434
+ data: void 0,
435
+ error: response.error,
436
+ headers: response.headers,
437
+ requestParams: params
438
+ });
439
+ this.endpointSubscribers.forEach((cb) => {
440
+ const endpointState = {
441
+ status: "error",
442
+ fetchCounts: this.fetchCounts,
443
+ meta: this.meta,
444
+ cacheableHeaders: this.cacheableHeaders,
445
+ error: response.error
446
+ };
447
+ cb(endpointState);
448
+ });
449
+ reject(response.error);
450
+ }
451
+ }
452
+ } catch (error) {
453
+ notifyRequestSubscribers({
454
+ fromCache: false,
455
+ status: "error",
456
+ data: void 0,
457
+ error,
458
+ headers: void 0,
459
+ requestParams: params
460
+ });
461
+ reject(error);
462
+ }
463
+ });
464
+ return {
465
+ id: requestId,
466
+ subscribe(listener, options2 = {}) {
467
+ const { autoUnsubscribe = true } = options2;
468
+ requestSubscribers.add(listener);
469
+ listener(currentState);
470
+ const unsubscribe = () => requestSubscribers.delete(listener);
471
+ if (autoUnsubscribe) {
472
+ waitPromise.finally(() => {
473
+ unsubscribe();
474
+ });
475
+ }
476
+ return unsubscribe;
477
+ },
478
+ wait: () => waitPromise,
479
+ waitWithCallbacks(handlers = {}) {
480
+ const { idle, loading, success, error } = handlers;
481
+ this.subscribe(
482
+ (state) => {
483
+ switch (state.status) {
484
+ case "idle":
485
+ idle?.(state);
486
+ break;
487
+ case "loading":
488
+ loading?.(state);
489
+ break;
490
+ case "success":
491
+ success?.(state.data, state);
492
+ break;
493
+ case "error":
494
+ error?.(state.error, state);
495
+ break;
496
+ }
497
+ },
498
+ { autoUnsubscribe: true }
499
+ );
500
+ return waitPromise;
501
+ },
502
+ abort: () => {
503
+ if (controller && !controller.signal.aborted) {
504
+ controller.abort();
505
+ }
506
+ },
507
+ then: (onfulfilled, onrejected) => waitPromise.then(onfulfilled, onrejected),
508
+ catch: (onrejected) => waitPromise.catch(onrejected),
509
+ finally: (onfinally) => waitPromise.finally(onfinally)
510
+ };
511
+ }
512
+ subscribe(cb) {
513
+ this.endpointSubscribers.add(cb);
514
+ const currentState = {
515
+ status: "idle",
516
+ fetchCounts: this.fetchCounts,
517
+ meta: this.meta,
518
+ cacheableHeaders: this.cacheableHeaders,
519
+ error: void 0
520
+ };
521
+ cb(currentState);
522
+ return () => this.endpointSubscribers.delete(cb);
523
+ }
524
+ reset() {
525
+ this.fetchCounts = 0;
526
+ return Promise.resolve();
527
+ }
528
+ destroy() {
529
+ this.endpointSubscribers.clear();
530
+ }
531
+ };
532
+
533
+ // src/core/storage/utils/storage-key.ts
534
+ var StorageKey = class {
535
+ constructor(value, isRawKey = false) {
536
+ this.value = value;
537
+ this.isRawKey = isRawKey;
538
+ }
539
+ toString() {
540
+ return this.value;
541
+ }
542
+ toJSON() {
543
+ return this.value;
544
+ }
545
+ valueOf() {
546
+ return this.value;
547
+ }
548
+ isUnparseable() {
549
+ return this.isRawKey;
550
+ }
551
+ };
552
+
553
+ // src/core/storage/utils/cache.util.ts
554
+ var CacheUtils = class {
555
+ static createMetadata(ttl = 0, tags = []) {
556
+ const now = Date.now();
557
+ const expiresAt = ttl > 0 ? now + ttl : Infinity;
558
+ return {
559
+ createdAt: now,
560
+ updatedAt: now,
561
+ expiresAt,
562
+ tags,
563
+ createdAtDateTime: this.formatDateTime(now),
564
+ updatedAtDateTime: this.formatDateTime(now),
565
+ expiresAtDateTime: expiresAt === Infinity ? "never" : this.formatDateTime(expiresAt)
566
+ };
567
+ }
568
+ static formatDateTime(timestamp) {
569
+ return new Date(timestamp).toISOString();
570
+ }
571
+ static isExpired(metadata) {
572
+ return Date.now() > metadata.expiresAt;
573
+ }
574
+ static updateMetadata(metadata) {
575
+ return {
576
+ ...metadata,
577
+ updatedAt: Date.now()
578
+ };
579
+ }
580
+ static createKey(...parts) {
581
+ return new StorageKey(parts.join("_"));
582
+ }
583
+ static createApiKey(endpoint, params) {
584
+ if (!params) return [new StorageKey(endpoint, true), params];
585
+ const sortedParams = Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("&");
586
+ return [new StorageKey(`${endpoint}_${sortedParams}`, true), params];
587
+ }
588
+ // Функция для проверки, есть ли у записи определенные теги
589
+ static hasAnyTag(metadata, tags = []) {
590
+ if (!metadata.tags || !tags.length) return false;
591
+ return tags.some((tag) => metadata.tags?.includes(tag));
592
+ }
593
+ };
594
+
595
+ // src/api/components/query-storage.ts
596
+ var QueryStorage = class {
597
+ constructor(storageExternal, globalCacheConfig) {
598
+ this.storageExternal = storageExternal;
599
+ this.globalCacheConfig = globalCacheConfig;
600
+ }
601
+ /** Экземпляр хранилища */
602
+ storage = null;
603
+ cleanupInterval = null;
604
+ /** Настройки кэша по умолчанию */
605
+ defaultCacheOptions = {
606
+ ttl: 5 * 60 * 1e3,
607
+ // 5 минут по умолчанию
608
+ cleanup: {
609
+ enabled: true,
610
+ interval: 10 * 60 * 1e3
611
+ // 10 минут
612
+ },
613
+ invalidateOnError: true
614
+ };
615
+ async initialize() {
616
+ await this.createStorage();
617
+ this.startCleanupInterval();
618
+ return this;
619
+ }
620
+ async createStorage() {
621
+ try {
622
+ const s = this.storageExternal;
623
+ await s.initialize();
624
+ this.storage = s;
625
+ } catch (error) {
626
+ console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430", error);
627
+ throw error;
628
+ }
629
+ }
630
+ startCleanupInterval() {
631
+ if (this.cleanupInterval) {
632
+ clearInterval(this.cleanupInterval);
633
+ this.cleanupInterval = null;
634
+ }
635
+ const cleanupConfig = typeof this.globalCacheConfig === "object" ? this.globalCacheConfig.cleanup : this.defaultCacheOptions.cleanup;
636
+ if (cleanupConfig?.enabled && cleanupConfig.interval) {
637
+ this.cleanupInterval = setInterval(() => {
638
+ this.cleanup().catch((err) => console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0447\u0438\u0441\u0442\u043A\u0435 \u043A\u044D\u0448\u0430:", err));
639
+ }, cleanupConfig.interval);
640
+ }
641
+ }
642
+ /**
643
+ * Получает экземпляр хранилища
644
+ */
645
+ getStorage() {
646
+ return this.storage;
647
+ }
648
+ /**
649
+ * Создает ключ кэша для запроса с учетом заголовков
650
+ * @param endpoint Имя эндпоинта
651
+ * @param params Параметры запроса (все что посчитаем нужным)
652
+ */
653
+ createCacheKey(endpoint, params) {
654
+ return CacheUtils.createApiKey(endpoint, params);
655
+ }
656
+ /**
657
+ * Получает результат запроса из кэша
658
+ */
659
+ async getCachedResult(cacheKey) {
660
+ if (!this.storage) throw new Error("\u0425\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E");
661
+ const cachedEntry = await this.storage.get(cacheKey);
662
+ if (!cachedEntry) return void 0;
663
+ if (CacheUtils.isExpired(cachedEntry.metadata)) {
664
+ await this.storage.delete(cacheKey);
665
+ return void 0;
666
+ }
667
+ const updatedEntry = {
668
+ ...cachedEntry,
669
+ metadata: CacheUtils.updateMetadata(cachedEntry.metadata)
670
+ };
671
+ await this.storage.set(cacheKey, updatedEntry);
672
+ return cachedEntry.data;
673
+ }
674
+ /**
675
+ * Сохраняет результат запроса в кэш
676
+ * @param cacheKey Ключ кэша
677
+ * @param data Данные для кэширования
678
+ * @param cacheOptions Метаданные
679
+ * @param cacheParams Параметры которые влияли на созадние ключа
680
+ * @param tags Тэги эндпоинта
681
+ * @param invalidatesTags Тэги которые нужно инвалидировать
682
+ */
683
+ async setCachedResult(cacheKey, data, cacheOptions, cacheParams, tags, invalidatesTags) {
684
+ if (!this.storage) throw new Error("\u0425\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E");
685
+ if (invalidatesTags?.length) {
686
+ await this.invalidateCacheByTags(invalidatesTags);
687
+ }
688
+ const cacheMetadata = CacheUtils.createMetadata(cacheOptions.ttl, tags);
689
+ const cacheEntry = {
690
+ data,
691
+ metadata: cacheMetadata,
692
+ params: cacheParams
693
+ };
694
+ await this.storage.set(cacheKey, cacheEntry);
695
+ }
696
+ /**
697
+ * Проверяет, должен ли запрос быть кэширован
698
+ * @param endpointConfig Конфигурация эндпоинта
699
+ * @param options Опции запроса
700
+ * @returns true если запрос должен кэшироваться
701
+ */
702
+ shouldCache(endpointConfig, options) {
703
+ if (this.globalCacheConfig === false) return false;
704
+ if (endpointConfig?.cache === false) return false;
705
+ if (typeof endpointConfig?.cache === "object" && endpointConfig?.cache.ttl === 0) return false;
706
+ if (options?.disableCache === true) return false;
707
+ if (this.globalCacheConfig === void 0 && endpointConfig?.cache === void 0) return false;
708
+ return true;
709
+ }
710
+ /**
711
+ * Создает итоговую конфигурацию кэширования для конкретного эндпоинта
712
+ * Объединяет глабальный конфиг с текущим
713
+ * @param endpointConfig Конфигурация эндпоинта
714
+ */
715
+ createCacheConfig(endpointConfig) {
716
+ let resultConfig = this.defaultCacheOptions;
717
+ if (typeof this.globalCacheConfig === "object") {
718
+ resultConfig = this.globalCacheConfig;
719
+ }
720
+ if (typeof endpointConfig?.cache === "object") {
721
+ const endpointCache = endpointConfig.cache;
722
+ resultConfig = {
723
+ // @ts-ignore
724
+ ...resultConfig,
725
+ ...endpointCache
726
+ };
727
+ }
728
+ return resultConfig;
729
+ }
730
+ /**
731
+ * Инвалидирует кэш по тегам
732
+ * @param tags Теги для инвалидации
733
+ */
734
+ async invalidateCacheByTags(tags) {
735
+ if (!this.storage) throw new Error("\u0425\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E");
736
+ const keys = await this.storage.keys();
737
+ for (const key of keys) {
738
+ const cachedEntry = await this.storage.get(key);
739
+ if (cachedEntry && CacheUtils.hasAnyTag(cachedEntry.metadata, tags)) {
740
+ await this.storage.delete(key);
741
+ }
742
+ }
743
+ }
744
+ /**
745
+ * Инвалидирует кэш по ключу
746
+ * @param cacheKey Ключ кэша
747
+ */
748
+ async invalidateCache(cacheKey) {
749
+ if (!this.storage) throw new Error("\u0425\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E");
750
+ await this.storage.delete(cacheKey);
751
+ }
752
+ /**
753
+ * Выполняет очистку всех просроченных записей кэша
754
+ */
755
+ async cleanup() {
756
+ if (!this.storage) {
757
+ throw new Error("\u0425\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E");
758
+ }
759
+ const keys = await this.storage.keys();
760
+ for (const key of keys) {
761
+ const value = await this.storage.get(key);
762
+ if (value && CacheUtils.isExpired(value.metadata)) {
763
+ await this.storage.delete(key);
764
+ }
765
+ }
766
+ }
767
+ /**
768
+ * Уничтожает хранилище и освобождает ресурсы
769
+ */
770
+ async destroy() {
771
+ if (this.cleanupInterval) {
772
+ window.clearInterval(this.cleanupInterval);
773
+ this.cleanupInterval = null;
774
+ }
775
+ if (this.storage) {
776
+ await this.storage.destroy();
777
+ this.storage = null;
778
+ }
779
+ }
780
+ };
781
+
782
+ // src/api/api.module.ts
783
+ var ApiClient = class {
784
+ /** Хранилище запросов */
785
+ // @ts-ignore
786
+ queryStorage;
787
+ cacheableHeaderKeys;
788
+ globalCacheConfig;
789
+ baseQueryConfig;
790
+ storageExternal;
791
+ createEndpoints;
792
+ /** Реестр эндпоинтов */
793
+ endpoints = {};
794
+ constructor(options) {
795
+ this.cacheableHeaderKeys = options.cacheableHeaderKeys;
796
+ this.globalCacheConfig = options.cache;
797
+ this.baseQueryConfig = options.baseQuery;
798
+ this.storageExternal = options.storage;
799
+ this.createEndpoints = options.endpoints;
800
+ }
801
+ async init() {
802
+ this.queryStorage = await new QueryStorage(this.storageExternal, this.globalCacheConfig).initialize();
803
+ await this.initializeEndpoints();
804
+ return this;
805
+ }
806
+ async initializeEndpoints() {
807
+ const create = (config) => config;
808
+ const endpointsConfig = await this.createEndpoints(create) || {};
809
+ for (const [endpointKey, endpointConfig] of Object.entries(endpointsConfig)) {
810
+ const key = endpointKey;
811
+ this.endpoints[key] = new EndpointClass(endpointKey, this.queryStorage, endpointConfig, this.cacheableHeaderKeys, this.globalCacheConfig, this.baseQueryConfig);
812
+ }
813
+ }
814
+ /**
815
+ * Получает все эндпоинты с улучшенной типизацией
816
+ * @returns Типизированный объект эндпоинтов
817
+ */
818
+ getEndpoints() {
819
+ return this.endpoints;
820
+ }
821
+ /**
822
+ * Выполняет запрос к API с типизацией и обработкой ошибок
823
+ * @param endpointName Имя эндпоинта (с подсказками TypeScript)
824
+ * @param params Параметры запроса (с типизацией)
825
+ * @param options Опции запроса
826
+ * @returns Promise с типизированным результатом запроса
827
+ */
828
+ async request(endpointName, params, options) {
829
+ const endpoints = this.getEndpoints();
830
+ const endpoint = endpoints[endpointName];
831
+ if (!endpoint) {
832
+ throw new Error(`\u042D\u043D\u0434\u043F\u043E\u0438\u043D\u0442 ${String(endpointName)} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D`);
833
+ }
834
+ try {
835
+ const stateRequest = endpoint.request(params, options);
836
+ return await stateRequest.wait();
837
+ } catch (error) {
838
+ apiLogger.error(`\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u043A ${String(endpointName)}`, { error, params });
839
+ throw error;
840
+ }
841
+ }
842
+ async destroy() {
843
+ await Promise.all(
844
+ Object.values(this.endpoints).map(async (endpoint) => {
845
+ endpoint.destroy();
846
+ return Promise.resolve();
847
+ })
848
+ );
849
+ this.endpoints = {};
850
+ await this.queryStorage.destroy();
851
+ }
852
+ };
853
+ export {
854
+ ApiClient,
855
+ ResponseFormat,
856
+ apiLogger,
857
+ createUniqueId,
858
+ headersToObject
859
+ };