@ryuzaki13/react-foundation-lib 1.8.3 → 1.9.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.
@@ -1,3 +1,4 @@
1
1
  export * from "./swCachePolicy";
2
+ export * from "./useBrowserOnlineStatus";
2
3
  export * from "./useServiceWorkerUpdate";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pwa/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pwa/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC"}
package/dist/pwa/index.js CHANGED
@@ -1,133 +1,149 @@
1
1
  import { i as e } from "../chunks/isRecord-DXDTW-jA.js";
2
2
  import { logError as t } from "../utils/index.js";
3
3
  import { buildSwCachePolicyValue as n, normalizeSwCacheName as r, parseSwCachePolicy as i, resolveSwCacheCacheNameByPolicy as a } from "./sw-cache-policy.js";
4
- import { useCallback as o, useEffect as s, useState as c } from "react";
4
+ import { useCallback as o, useEffect as s, useState as c, useSyncExternalStore as l } from "react";
5
+ //#region src/pwa/useBrowserOnlineStatus.ts
6
+ function u(e) {
7
+ return window.addEventListener("online", e), window.addEventListener("offline", e), () => {
8
+ window.removeEventListener("online", e), window.removeEventListener("offline", e);
9
+ };
10
+ }
11
+ function d() {
12
+ return navigator.onLine;
13
+ }
14
+ function f() {
15
+ return !0;
16
+ }
17
+ function p() {
18
+ return l(u, d, f);
19
+ }
20
+ //#endregion
5
21
  //#region src/pwa/serviceWorker.ts
6
- var l = __SERVICE_WORKER_SCOPE__, u = __SERVICE_WORKER_URL__, d = "SW_ACTIVATED_BY_CLIENT", f = __SERVICE_WORKER_UPDATE_RELOAD_COUNT_KEY__, p = __SERVICE_WORKER_MAX_AUTO_UPDATE_RELOADS__, m = /* @__PURE__ */ new WeakSet(), h = /* @__PURE__ */ new Set(), g = null, _ = !1, v = !1, y = !1, b = !1;
7
- function x(t) {
8
- return e(t) && t.type === d ? { type: d } : null;
22
+ var m = __SERVICE_WORKER_SCOPE__, h = __SERVICE_WORKER_URL__, g = "SW_ACTIVATED_BY_CLIENT", _ = __SERVICE_WORKER_UPDATE_RELOAD_COUNT_KEY__, v = __SERVICE_WORKER_MAX_AUTO_UPDATE_RELOADS__, y = /* @__PURE__ */ new WeakSet(), b = /* @__PURE__ */ new Set(), x = null, S = !1, C = !1, w = !1, T = !1;
23
+ function E(t) {
24
+ return e(t) && t.type === g ? { type: g } : null;
9
25
  }
10
- function S() {
26
+ function D() {
11
27
  try {
12
- let e = window.sessionStorage.getItem(f), t = Number(e);
28
+ let e = window.sessionStorage.getItem(_), t = Number(e);
13
29
  return Number.isInteger(t) && t > 0 ? t : 0;
14
30
  } catch {
15
31
  return 0;
16
32
  }
17
33
  }
18
- function C() {
34
+ function O() {
19
35
  try {
20
- let e = Math.min(S() + 1, p);
21
- window.sessionStorage.setItem(f, String(e));
36
+ let e = Math.min(D() + 1, v);
37
+ window.sessionStorage.setItem(_, String(e));
22
38
  } catch {}
23
39
  }
24
- function w() {
25
- return S() < p;
40
+ function k() {
41
+ return D() < v;
26
42
  }
27
- function T() {
43
+ function A() {
28
44
  try {
29
- window.sessionStorage.removeItem(f);
45
+ window.sessionStorage.removeItem(_);
30
46
  } catch {}
31
47
  }
32
- function E() {
33
- _ || (_ = !0, window.location.reload());
48
+ function j() {
49
+ S || (S = !0, window.location.reload());
34
50
  }
35
- function D() {
36
- y || (y = !0, navigator.serviceWorker.addEventListener("controllerchange", () => {
37
- v && E();
51
+ function M() {
52
+ w || (w = !0, navigator.serviceWorker.addEventListener("controllerchange", () => {
53
+ C && j();
38
54
  }));
39
55
  }
40
- function O() {
41
- b || (b = !0, navigator.serviceWorker.addEventListener("message", (e) => {
42
- x(e.data) && (_ || (C(), E()));
56
+ function N() {
57
+ T || (T = !0, navigator.serviceWorker.addEventListener("message", (e) => {
58
+ E(e.data) && (S || (O(), j()));
43
59
  }));
44
60
  }
45
- function k(e) {
46
- for (let t of h) t(e);
61
+ function P(e) {
62
+ for (let t of b) t(e);
47
63
  }
48
- function A(e) {
49
- m.has(e) || (m.add(e), e.addEventListener("updatefound", () => {
64
+ function F(e) {
65
+ y.has(e) || (y.add(e), e.addEventListener("updatefound", () => {
50
66
  let t = e.installing;
51
67
  t && t.addEventListener("statechange", () => {
52
- t.state === "installed" && navigator.serviceWorker.controller && (g = e, k(e));
68
+ t.state === "installed" && navigator.serviceWorker.controller && (x = e, P(e));
53
69
  });
54
- }), D(), O());
70
+ }), M(), N());
55
71
  }
56
- async function j(e = {}) {
72
+ async function I(e = {}) {
57
73
  if (!("serviceWorker" in navigator)) return null;
58
- e.onUpdate && h.add(e.onUpdate);
74
+ e.onUpdate && b.add(e.onUpdate);
59
75
  try {
60
- let t = g ?? await navigator.serviceWorker.getRegistration(l) ?? await navigator.serviceWorker.register(u, {
61
- scope: l,
76
+ let t = x ?? await navigator.serviceWorker.getRegistration(m) ?? await navigator.serviceWorker.register(h, {
77
+ scope: m,
62
78
  updateViaCache: "none"
63
79
  });
64
- return g = t, A(t), e.onReady?.(t), t;
80
+ return x = t, F(t), e.onReady?.(t), t;
65
81
  } catch (e) {
66
82
  return t("[SW] registration failed:", e), null;
67
83
  }
68
84
  }
69
- function M(e) {
70
- h.delete(e);
85
+ function L(e) {
86
+ b.delete(e);
71
87
  }
72
- function N(e) {
88
+ function R(e) {
73
89
  return !!e?.waiting;
74
90
  }
75
- function P(e = g) {
91
+ function z(e = x) {
76
92
  let t = e?.waiting;
77
- return t ? (v = !0, C(), t.postMessage({ type: "SKIP_WAITING" }), !0) : !1;
93
+ return t ? (C = !0, O(), t.postMessage({ type: "SKIP_WAITING" }), !0) : !1;
78
94
  }
79
- function F() {
80
- return C(), E(), !0;
95
+ function B() {
96
+ return O(), j(), !0;
81
97
  }
82
- async function I() {
98
+ async function V() {
83
99
  if (!("serviceWorker" in navigator)) return null;
84
100
  try {
85
- let e = g ?? await navigator.serviceWorker.getRegistration(l);
86
- return e ? (g = e, A(e), await e.update(), e) : null;
101
+ let e = x ?? await navigator.serviceWorker.getRegistration(m);
102
+ return e ? (x = e, F(e), await e.update(), e) : null;
87
103
  } catch (e) {
88
104
  return t("[SW] update check failed:", e), null;
89
105
  }
90
106
  }
91
107
  //#endregion
92
108
  //#region src/pwa/useServiceWorkerUpdate.ts
93
- var L = 3e5, R = 3e4;
94
- function z() {
109
+ var H = 3e5, U = 3e4;
110
+ function W() {
95
111
  let [e] = performance.getEntriesByType("navigation");
96
112
  return e?.type === "reload" || e?.type === "navigate";
97
113
  }
98
- function B() {
99
- let [e, t] = c(null), n = e !== null, r = o(() => P(e) ? !0 : e ? F() : !1, [e]), i = o(() => {
114
+ function G() {
115
+ let [e, t] = c(null), n = e !== null, r = o(() => z(e) ? !0 : e ? B() : !1, [e]), i = o(() => {
100
116
  t(null);
101
117
  }, []);
102
118
  return s(() => {
103
119
  if (__DEV__) return;
104
- let e = !0, n = Date.now(), r = null, i = !1, a = () => z() && Date.now() - n <= R && w(), o = (e) => {
120
+ let e = !0, n = Date.now(), r = null, i = !1, a = () => W() && Date.now() - n <= U && k(), o = (e) => {
105
121
  if (i) return !0;
106
122
  if (!a()) return !1;
107
- let t = P(e);
123
+ let t = z(e);
108
124
  return i = t, t;
109
125
  }, s = () => {
110
- let t = Date.now() - n, i = Math.max(R - t, 0);
126
+ let t = Date.now() - n, i = Math.max(U - t, 0);
111
127
  r = window.setTimeout(() => {
112
- e && T();
128
+ e && A();
113
129
  }, i);
114
130
  }, c = (n) => {
115
131
  e && (o(n) || t(n));
116
132
  };
117
133
  async function l() {
118
- let n = await j({ onUpdate: c });
119
- if (!e || N(n) && o(n)) return;
120
- let r = await I();
121
- e && (i || N(r) && o(r) || (N(r) && t(r), s()));
134
+ let n = await I({ onUpdate: c });
135
+ if (!e || R(n) && o(n)) return;
136
+ let r = await V();
137
+ e && (i || R(r) && o(r) || (R(r) && t(r), s()));
122
138
  }
123
139
  l();
124
140
  let u = window.setInterval(() => {
125
- I().then((n) => {
126
- e && N(n) && t(n);
141
+ V().then((n) => {
142
+ e && R(n) && t(n);
127
143
  });
128
- }, L);
144
+ }, H);
129
145
  return () => {
130
- e = !1, r !== null && window.clearTimeout(r), window.clearInterval(u), M(c);
146
+ e = !1, r !== null && window.clearTimeout(r), window.clearInterval(u), L(c);
131
147
  };
132
148
  }, []), {
133
149
  hasUpdate: n,
@@ -136,6 +152,6 @@ function B() {
136
152
  };
137
153
  }
138
154
  //#endregion
139
- export { n as buildSwCachePolicyValue, r as normalizeSwCacheName, i as parseSwCachePolicy, a as resolveSwCacheCacheNameByPolicy, B as useServiceWorkerUpdate };
155
+ export { n as buildSwCachePolicyValue, r as normalizeSwCacheName, i as parseSwCachePolicy, a as resolveSwCacheCacheNameByPolicy, p as useBrowserOnlineStatus, G as useServiceWorkerUpdate };
140
156
 
141
157
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/pwa/serviceWorker.ts","../../src/pwa/useServiceWorkerUpdate.ts"],"sourcesContent":["import { logError } from \"../utils\";\nimport { isRecord } from \"../validators\";\n\nconst SW_SCOPE = __SERVICE_WORKER_SCOPE__;\nconst SW_URL = __SERVICE_WORKER_URL__;\nconst SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE = \"SW_ACTIVATED_BY_CLIENT\";\nconst SW_UPDATE_RELOAD_COUNT_KEY = __SERVICE_WORKER_UPDATE_RELOAD_COUNT_KEY__;\nconst MAX_SW_AUTO_UPDATE_RELOADS = __SERVICE_WORKER_MAX_AUTO_UPDATE_RELOADS__;\n\ntype SWUpdateHandler = (reg: ServiceWorkerRegistration) => void;\n\ntype ServiceWorkerClientMessage = {\n\ttype: typeof SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE;\n};\n\ntype RegisterSWOptions = {\n\t/**\n\t * Вызывается, когда новый SW установлен и ждёт активации.\n\t */\n\tonUpdate?: SWUpdateHandler;\n\n\t/**\n\t * Вызывается после успешной регистрации.\n\t */\n\tonReady?: (reg: ServiceWorkerRegistration) => void;\n};\n\nconst observedRegistrations = new WeakSet<ServiceWorkerRegistration>();\nconst updateHandlers = new Set<SWUpdateHandler>();\n\nlet latestRegistration: ServiceWorkerRegistration | null = null;\nlet refreshing = false;\nlet updateRequested = false;\nlet controllerChangeSubscribed = false;\nlet serviceWorkerMessageSubscribed = false;\n\nfunction readServiceWorkerClientMessage(value: unknown): ServiceWorkerClientMessage | null {\n\tif (!isRecord(value)) return null;\n\n\treturn value.type === SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE ? { type: SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE } : null;\n}\n\nfunction readServiceWorkerUpdateReloadCount(): number {\n\ttry {\n\t\tconst value = window.sessionStorage.getItem(SW_UPDATE_RELOAD_COUNT_KEY);\n\t\tconst count = Number(value);\n\n\t\treturn Number.isInteger(count) && count > 0 ? count : 0;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\nexport function recordServiceWorkerUpdateReload() {\n\ttry {\n\t\tconst nextCount = Math.min(readServiceWorkerUpdateReloadCount() + 1, MAX_SW_AUTO_UPDATE_RELOADS);\n\t\twindow.sessionStorage.setItem(SW_UPDATE_RELOAD_COUNT_KEY, String(nextCount));\n\t} catch {\n\t\t/**\n\t\t * sessionStorage может быть недоступен в приватном режиме или при\n\t\t * жёстких browser policy. Обновление всё равно должно продолжиться.\n\t\t */\n\t}\n}\n\nexport function canAutoApplyServiceWorkerUpdate(): boolean {\n\treturn readServiceWorkerUpdateReloadCount() < MAX_SW_AUTO_UPDATE_RELOADS;\n}\n\nexport function clearServiceWorkerUpdateReloadChain() {\n\ttry {\n\t\twindow.sessionStorage.removeItem(SW_UPDATE_RELOAD_COUNT_KEY);\n\t} catch {\n\t\t/**\n\t\t * Очистка маркера не критична: максимум пользователь увидит обычный\n\t\t * баннер вместо автоматического продолжения цепочки обновлений.\n\t\t */\n\t}\n}\n\nfunction reloadPage() {\n\tif (refreshing) return;\n\n\trefreshing = true;\n\twindow.location.reload();\n}\n\nfunction subscribeControllerChangeReload() {\n\tif (controllerChangeSubscribed) return;\n\n\tcontrollerChangeSubscribed = true;\n\n\tnavigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n\t\t/**\n\t\t * Не перезагружаем страницу от любого controllerchange.\n\t\t * Перезагрузка нужна только когда мы сами вызвали SKIP_WAITING.\n\t\t */\n\t\tif (!updateRequested) return;\n\n\t\treloadPage();\n\t});\n}\n\nfunction subscribeServiceWorkerMessages() {\n\tif (serviceWorkerMessageSubscribed) return;\n\n\tserviceWorkerMessageSubscribed = true;\n\n\tnavigator.serviceWorker.addEventListener(\"message\", (event) => {\n\t\tconst message = readServiceWorkerClientMessage(event.data);\n\t\tif (!message) return;\n\t\tif (refreshing) return;\n\n\t\trecordServiceWorkerUpdateReload();\n\t\treloadPage();\n\t});\n}\n\nfunction emitUpdate(reg: ServiceWorkerRegistration) {\n\tfor (const handler of updateHandlers) {\n\t\thandler(reg);\n\t}\n}\n\nfunction observeRegistration(reg: ServiceWorkerRegistration) {\n\tif (observedRegistrations.has(reg)) return;\n\n\tobservedRegistrations.add(reg);\n\n\treg.addEventListener(\"updatefound\", () => {\n\t\tconst newWorker = reg.installing;\n\t\tif (!newWorker) return;\n\n\t\tnewWorker.addEventListener(\"statechange\", () => {\n\t\t\t/**\n\t\t\t * installed + есть controller = новая версия установлена,\n\t\t\t * но текущая страница всё ещё под старым SW.\n\t\t\t */\n\t\t\tif (newWorker.state === \"installed\" && navigator.serviceWorker.controller) {\n\t\t\t\tlatestRegistration = reg;\n\t\t\t\temitUpdate(reg);\n\t\t\t}\n\t\t});\n\t});\n\n\tsubscribeControllerChangeReload();\n\tsubscribeServiceWorkerMessages();\n}\n\nexport async function getServiceWorkerRegistration(): Promise<ServiceWorkerRegistration | null> {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\ttry {\n\t\tconst reg = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\n\t\tif (!reg) return null;\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] get registration failed:\", err);\n\t\treturn null;\n\t}\n}\n\nexport async function registerServiceWorker(options: RegisterSWOptions = {}) {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\tif (options.onUpdate) {\n\t\tupdateHandlers.add(options.onUpdate);\n\t}\n\n\ttry {\n\t\tconst existingRegistration = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\n\t\tconst reg =\n\t\t\texistingRegistration ??\n\t\t\t(await navigator.serviceWorker.register(SW_URL, {\n\t\t\t\tscope: SW_SCOPE,\n\t\t\t\tupdateViaCache: \"none\"\n\t\t\t}));\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\toptions.onReady?.(reg);\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] registration failed:\", err);\n\t\treturn null;\n\t}\n}\n\nexport function unsubscribeServiceWorkerUpdate(handler: SWUpdateHandler) {\n\tupdateHandlers.delete(handler);\n}\n\nexport function hasWaitingServiceWorker(reg: ServiceWorkerRegistration | null | undefined): reg is ServiceWorkerRegistration {\n\treturn Boolean(reg?.waiting);\n}\n\nexport function applyServiceWorkerUpdate(reg: ServiceWorkerRegistration | null = latestRegistration) {\n\tconst waiting = reg?.waiting;\n\tif (!waiting) return false;\n\n\tupdateRequested = true;\n\trecordServiceWorkerUpdateReload();\n\n\twaiting.postMessage({ type: \"SKIP_WAITING\" });\n\n\treturn true;\n}\n\nexport function reloadPageForServiceWorkerUpdate() {\n\trecordServiceWorkerUpdateReload();\n\treloadPage();\n\n\treturn true;\n}\n\nexport async function checkServiceWorkerUpdate() {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\ttry {\n\t\tconst reg = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\t\tif (!reg) return null;\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\tawait reg.update();\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] update check failed:\", err);\n\t\treturn null;\n\t}\n}\n\n/**\n * Очищает SW-кеш OData для указанной политики.\n *\n * Примеры: `\"ttl=24h;name=ref\"`, `\"ttl=10m;max=200;name=ui\"`.\n *\n * После вызова следующий запрос с той же политикой пойдёт в сеть.\n */\nexport function invalidateSwCacheProfile(profile: string) {\n\tnavigator.serviceWorker?.controller?.postMessage({\n\t\ttype: \"invalidateProfile\",\n\t\tprofile\n\t});\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport {\n\tapplyServiceWorkerUpdate,\n\tcanAutoApplyServiceWorkerUpdate,\n\tcheckServiceWorkerUpdate,\n\tclearServiceWorkerUpdateReloadChain,\n\thasWaitingServiceWorker,\n\tregisterServiceWorker,\n\treloadPageForServiceWorkerUpdate,\n\tunsubscribeServiceWorkerUpdate\n} from \"./serviceWorker\";\n\n// TODO: Vite env\nconst SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;\n// TODO: Vite env\nconst SW_STARTUP_AUTO_UPDATE_GRACE_MS = 30 * 1000;\n\nfunction isStartupNavigationForAutoUpdate(): boolean {\n\tconst [entry] = performance.getEntriesByType(\"navigation\") as PerformanceNavigationTiming[];\n\n\t/**\n\t * reload — пользователь явно обновил страницу.\n\t * navigate — пользователь открыл страницу по ссылке/закладке.\n\t */\n\treturn entry?.type === \"reload\" || entry?.type === \"navigate\";\n}\n\nexport function useServiceWorkerUpdate() {\n\tconst [waitingRegistration, setWaitingRegistration] = useState<ServiceWorkerRegistration | null>(null);\n\n\tconst hasUpdate = waitingRegistration !== null;\n\n\tconst applyUpdate = useCallback(() => {\n\t\tif (applyServiceWorkerUpdate(waitingRegistration)) return true;\n\t\tif (!waitingRegistration) return false;\n\n\t\t/**\n\t\t * Если другой клиент уже активировал waiting worker, в текущей вкладке\n\t\t * registration.waiting станет пустым, но баннер ещё может быть видимым.\n\t\t */\n\t\treturn reloadPageForServiceWorkerUpdate();\n\t}, [waitingRegistration]);\n\n\tconst dismiss = useCallback(() => {\n\t\tsetWaitingRegistration(null);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (__DEV__) return;\n\n\t\tlet isActive = true;\n\t\tconst startupStartedAt = Date.now();\n\t\tlet startupResetTimerId: number | null = null;\n\t\tlet startupUpdateApplied = false;\n\n\t\tconst canApplyStartupUpdate = () =>\n\t\t\tisStartupNavigationForAutoUpdate() &&\n\t\t\tDate.now() - startupStartedAt <= SW_STARTUP_AUTO_UPDATE_GRACE_MS &&\n\t\t\tcanAutoApplyServiceWorkerUpdate();\n\n\t\tconst tryApplyStartupUpdate = (reg: ServiceWorkerRegistration) => {\n\t\t\tif (startupUpdateApplied) return true;\n\t\t\tif (!canApplyStartupUpdate()) return false;\n\n\t\t\tconst applied = applyServiceWorkerUpdate(reg);\n\t\t\tstartupUpdateApplied = applied;\n\n\t\t\treturn applied;\n\t\t};\n\n\t\tconst scheduleStartupChainReset = () => {\n\t\t\tconst elapsedMs = Date.now() - startupStartedAt;\n\t\t\tconst timeoutMs = Math.max(SW_STARTUP_AUTO_UPDATE_GRACE_MS - elapsedMs, 0);\n\n\t\t\tstartupResetTimerId = window.setTimeout(() => {\n\t\t\t\tif (!isActive) return;\n\n\t\t\t\tclearServiceWorkerUpdateReloadChain();\n\t\t\t}, timeoutMs);\n\t\t};\n\n\t\tconst handleUpdate = (reg: ServiceWorkerRegistration) => {\n\t\t\tif (!isActive) return;\n\n\t\t\t/**\n\t\t\t * Если update найден вскоре после reload/navigate, применяем его\n\t\t\t * автоматически. Это закрывает цепочку из нескольких деплоев,\n\t\t\t * накопленных во время простоя вкладки.\n\t\t\t */\n\t\t\tif (tryApplyStartupUpdate(reg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Если update найден уже во время активной сессии —\n\t\t\t * показываем баннер.\n\t\t\t */\n\t\t\tsetWaitingRegistration(reg);\n\t\t};\n\n\t\tasync function init() {\n\t\t\tconst reg = await registerServiceWorker({\n\t\t\t\tonUpdate: handleUpdate\n\t\t\t});\n\n\t\t\tif (!isActive) return;\n\n\t\t\t/**\n\t\t\t * Важный сценарий:\n\t\t\t * пользователь перезагрузил страницу, а новый SW уже был в waiting.\n\t\t\t * В этом случае не показываем баннер, а применяем update сразу.\n\t\t\t */\n\t\t\tif (hasWaitingServiceWorker(reg) && tryApplyStartupUpdate(reg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst checkedReg = await checkServiceWorkerUpdate();\n\n\t\t\tif (!isActive) return;\n\t\t\tif (startupUpdateApplied) return;\n\n\t\t\tif (hasWaitingServiceWorker(checkedReg) && tryApplyStartupUpdate(checkedReg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (hasWaitingServiceWorker(checkedReg)) {\n\t\t\t\tsetWaitingRegistration(checkedReg);\n\t\t\t}\n\n\t\t\tscheduleStartupChainReset();\n\t\t}\n\n\t\tvoid init();\n\n\t\tconst intervalId = window.setInterval(() => {\n\t\t\tvoid checkServiceWorkerUpdate().then((reg) => {\n\t\t\t\tif (!isActive) return;\n\n\t\t\t\tif (hasWaitingServiceWorker(reg)) {\n\t\t\t\t\tsetWaitingRegistration(reg);\n\t\t\t\t}\n\t\t\t});\n\t\t}, SW_UPDATE_CHECK_INTERVAL_MS);\n\n\t\treturn () => {\n\t\t\tisActive = false;\n\t\t\tif (startupResetTimerId !== null) {\n\t\t\t\twindow.clearTimeout(startupResetTimerId);\n\t\t\t}\n\t\t\twindow.clearInterval(intervalId);\n\t\t\tunsubscribeServiceWorkerUpdate(handleUpdate);\n\t\t};\n\t}, []);\n\n\treturn {\n\t\thasUpdate,\n\t\tapplyUpdate,\n\t\tdismiss\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/pwa/useBrowserOnlineStatus.ts","../../src/pwa/serviceWorker.ts","../../src/pwa/useServiceWorkerUpdate.ts"],"sourcesContent":["import { useSyncExternalStore } from \"react\";\n\nfunction subscribe(onStoreChange: () => void) {\n\twindow.addEventListener(\"online\", onStoreChange);\n\twindow.addEventListener(\"offline\", onStoreChange);\n\treturn () => {\n\t\twindow.removeEventListener(\"online\", onStoreChange);\n\t\twindow.removeEventListener(\"offline\", onStoreChange);\n\t};\n}\n\nfunction getSnapshot() {\n\treturn navigator.onLine;\n}\n\nfunction getServerSnapshot() {\n\treturn true;\n}\n\n/** Наблюдает browser online hint без попытки подменить им проверку доступности конкретного сервера. */\nexport function useBrowserOnlineStatus() {\n\treturn useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import { logError } from \"../utils\";\nimport { isRecord } from \"../validators\";\n\nconst SW_SCOPE = __SERVICE_WORKER_SCOPE__;\nconst SW_URL = __SERVICE_WORKER_URL__;\nconst SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE = \"SW_ACTIVATED_BY_CLIENT\";\nconst SW_UPDATE_RELOAD_COUNT_KEY = __SERVICE_WORKER_UPDATE_RELOAD_COUNT_KEY__;\nconst MAX_SW_AUTO_UPDATE_RELOADS = __SERVICE_WORKER_MAX_AUTO_UPDATE_RELOADS__;\n\ntype SWUpdateHandler = (reg: ServiceWorkerRegistration) => void;\n\ntype ServiceWorkerClientMessage = {\n\ttype: typeof SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE;\n};\n\ntype RegisterSWOptions = {\n\t/**\n\t * Вызывается, когда новый SW установлен и ждёт активации.\n\t */\n\tonUpdate?: SWUpdateHandler;\n\n\t/**\n\t * Вызывается после успешной регистрации.\n\t */\n\tonReady?: (reg: ServiceWorkerRegistration) => void;\n};\n\nconst observedRegistrations = new WeakSet<ServiceWorkerRegistration>();\nconst updateHandlers = new Set<SWUpdateHandler>();\n\nlet latestRegistration: ServiceWorkerRegistration | null = null;\nlet refreshing = false;\nlet updateRequested = false;\nlet controllerChangeSubscribed = false;\nlet serviceWorkerMessageSubscribed = false;\n\nfunction readServiceWorkerClientMessage(value: unknown): ServiceWorkerClientMessage | null {\n\tif (!isRecord(value)) return null;\n\n\treturn value.type === SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE ? { type: SW_ACTIVATED_BY_CLIENT_MESSAGE_TYPE } : null;\n}\n\nfunction readServiceWorkerUpdateReloadCount(): number {\n\ttry {\n\t\tconst value = window.sessionStorage.getItem(SW_UPDATE_RELOAD_COUNT_KEY);\n\t\tconst count = Number(value);\n\n\t\treturn Number.isInteger(count) && count > 0 ? count : 0;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\nexport function recordServiceWorkerUpdateReload() {\n\ttry {\n\t\tconst nextCount = Math.min(readServiceWorkerUpdateReloadCount() + 1, MAX_SW_AUTO_UPDATE_RELOADS);\n\t\twindow.sessionStorage.setItem(SW_UPDATE_RELOAD_COUNT_KEY, String(nextCount));\n\t} catch {\n\t\t/**\n\t\t * sessionStorage может быть недоступен в приватном режиме или при\n\t\t * жёстких browser policy. Обновление всё равно должно продолжиться.\n\t\t */\n\t}\n}\n\nexport function canAutoApplyServiceWorkerUpdate(): boolean {\n\treturn readServiceWorkerUpdateReloadCount() < MAX_SW_AUTO_UPDATE_RELOADS;\n}\n\nexport function clearServiceWorkerUpdateReloadChain() {\n\ttry {\n\t\twindow.sessionStorage.removeItem(SW_UPDATE_RELOAD_COUNT_KEY);\n\t} catch {\n\t\t/**\n\t\t * Очистка маркера не критична: максимум пользователь увидит обычный\n\t\t * баннер вместо автоматического продолжения цепочки обновлений.\n\t\t */\n\t}\n}\n\nfunction reloadPage() {\n\tif (refreshing) return;\n\n\trefreshing = true;\n\twindow.location.reload();\n}\n\nfunction subscribeControllerChangeReload() {\n\tif (controllerChangeSubscribed) return;\n\n\tcontrollerChangeSubscribed = true;\n\n\tnavigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n\t\t/**\n\t\t * Не перезагружаем страницу от любого controllerchange.\n\t\t * Перезагрузка нужна только когда мы сами вызвали SKIP_WAITING.\n\t\t */\n\t\tif (!updateRequested) return;\n\n\t\treloadPage();\n\t});\n}\n\nfunction subscribeServiceWorkerMessages() {\n\tif (serviceWorkerMessageSubscribed) return;\n\n\tserviceWorkerMessageSubscribed = true;\n\n\tnavigator.serviceWorker.addEventListener(\"message\", (event) => {\n\t\tconst message = readServiceWorkerClientMessage(event.data);\n\t\tif (!message) return;\n\t\tif (refreshing) return;\n\n\t\trecordServiceWorkerUpdateReload();\n\t\treloadPage();\n\t});\n}\n\nfunction emitUpdate(reg: ServiceWorkerRegistration) {\n\tfor (const handler of updateHandlers) {\n\t\thandler(reg);\n\t}\n}\n\nfunction observeRegistration(reg: ServiceWorkerRegistration) {\n\tif (observedRegistrations.has(reg)) return;\n\n\tobservedRegistrations.add(reg);\n\n\treg.addEventListener(\"updatefound\", () => {\n\t\tconst newWorker = reg.installing;\n\t\tif (!newWorker) return;\n\n\t\tnewWorker.addEventListener(\"statechange\", () => {\n\t\t\t/**\n\t\t\t * installed + есть controller = новая версия установлена,\n\t\t\t * но текущая страница всё ещё под старым SW.\n\t\t\t */\n\t\t\tif (newWorker.state === \"installed\" && navigator.serviceWorker.controller) {\n\t\t\t\tlatestRegistration = reg;\n\t\t\t\temitUpdate(reg);\n\t\t\t}\n\t\t});\n\t});\n\n\tsubscribeControllerChangeReload();\n\tsubscribeServiceWorkerMessages();\n}\n\nexport async function getServiceWorkerRegistration(): Promise<ServiceWorkerRegistration | null> {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\ttry {\n\t\tconst reg = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\n\t\tif (!reg) return null;\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] get registration failed:\", err);\n\t\treturn null;\n\t}\n}\n\nexport async function registerServiceWorker(options: RegisterSWOptions = {}) {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\tif (options.onUpdate) {\n\t\tupdateHandlers.add(options.onUpdate);\n\t}\n\n\ttry {\n\t\tconst existingRegistration = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\n\t\tconst reg =\n\t\t\texistingRegistration ??\n\t\t\t(await navigator.serviceWorker.register(SW_URL, {\n\t\t\t\tscope: SW_SCOPE,\n\t\t\t\tupdateViaCache: \"none\"\n\t\t\t}));\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\toptions.onReady?.(reg);\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] registration failed:\", err);\n\t\treturn null;\n\t}\n}\n\nexport function unsubscribeServiceWorkerUpdate(handler: SWUpdateHandler) {\n\tupdateHandlers.delete(handler);\n}\n\nexport function hasWaitingServiceWorker(reg: ServiceWorkerRegistration | null | undefined): reg is ServiceWorkerRegistration {\n\treturn Boolean(reg?.waiting);\n}\n\nexport function applyServiceWorkerUpdate(reg: ServiceWorkerRegistration | null = latestRegistration) {\n\tconst waiting = reg?.waiting;\n\tif (!waiting) return false;\n\n\tupdateRequested = true;\n\trecordServiceWorkerUpdateReload();\n\n\twaiting.postMessage({ type: \"SKIP_WAITING\" });\n\n\treturn true;\n}\n\nexport function reloadPageForServiceWorkerUpdate() {\n\trecordServiceWorkerUpdateReload();\n\treloadPage();\n\n\treturn true;\n}\n\nexport async function checkServiceWorkerUpdate() {\n\tif (!(\"serviceWorker\" in navigator)) return null;\n\n\ttry {\n\t\tconst reg = latestRegistration ?? (await navigator.serviceWorker.getRegistration(SW_SCOPE));\n\t\tif (!reg) return null;\n\n\t\tlatestRegistration = reg;\n\t\tobserveRegistration(reg);\n\n\t\tawait reg.update();\n\n\t\treturn reg;\n\t} catch (err) {\n\t\tlogError(\"[SW] update check failed:\", err);\n\t\treturn null;\n\t}\n}\n\n/**\n * Очищает SW-кеш OData для указанной политики.\n *\n * Примеры: `\"ttl=24h;name=ref\"`, `\"ttl=10m;max=200;name=ui\"`.\n *\n * После вызова следующий запрос с той же политикой пойдёт в сеть.\n */\nexport function invalidateSwCacheProfile(profile: string) {\n\tnavigator.serviceWorker?.controller?.postMessage({\n\t\ttype: \"invalidateProfile\",\n\t\tprofile\n\t});\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport {\n\tapplyServiceWorkerUpdate,\n\tcanAutoApplyServiceWorkerUpdate,\n\tcheckServiceWorkerUpdate,\n\tclearServiceWorkerUpdateReloadChain,\n\thasWaitingServiceWorker,\n\tregisterServiceWorker,\n\treloadPageForServiceWorkerUpdate,\n\tunsubscribeServiceWorkerUpdate\n} from \"./serviceWorker\";\n\n// TODO: Vite env\nconst SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;\n// TODO: Vite env\nconst SW_STARTUP_AUTO_UPDATE_GRACE_MS = 30 * 1000;\n\nfunction isStartupNavigationForAutoUpdate(): boolean {\n\tconst [entry] = performance.getEntriesByType(\"navigation\") as PerformanceNavigationTiming[];\n\n\t/**\n\t * reload — пользователь явно обновил страницу.\n\t * navigate — пользователь открыл страницу по ссылке/закладке.\n\t */\n\treturn entry?.type === \"reload\" || entry?.type === \"navigate\";\n}\n\nexport function useServiceWorkerUpdate() {\n\tconst [waitingRegistration, setWaitingRegistration] = useState<ServiceWorkerRegistration | null>(null);\n\n\tconst hasUpdate = waitingRegistration !== null;\n\n\tconst applyUpdate = useCallback(() => {\n\t\tif (applyServiceWorkerUpdate(waitingRegistration)) return true;\n\t\tif (!waitingRegistration) return false;\n\n\t\t/**\n\t\t * Если другой клиент уже активировал waiting worker, в текущей вкладке\n\t\t * registration.waiting станет пустым, но баннер ещё может быть видимым.\n\t\t */\n\t\treturn reloadPageForServiceWorkerUpdate();\n\t}, [waitingRegistration]);\n\n\tconst dismiss = useCallback(() => {\n\t\tsetWaitingRegistration(null);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (__DEV__) return;\n\n\t\tlet isActive = true;\n\t\tconst startupStartedAt = Date.now();\n\t\tlet startupResetTimerId: number | null = null;\n\t\tlet startupUpdateApplied = false;\n\n\t\tconst canApplyStartupUpdate = () =>\n\t\t\tisStartupNavigationForAutoUpdate() &&\n\t\t\tDate.now() - startupStartedAt <= SW_STARTUP_AUTO_UPDATE_GRACE_MS &&\n\t\t\tcanAutoApplyServiceWorkerUpdate();\n\n\t\tconst tryApplyStartupUpdate = (reg: ServiceWorkerRegistration) => {\n\t\t\tif (startupUpdateApplied) return true;\n\t\t\tif (!canApplyStartupUpdate()) return false;\n\n\t\t\tconst applied = applyServiceWorkerUpdate(reg);\n\t\t\tstartupUpdateApplied = applied;\n\n\t\t\treturn applied;\n\t\t};\n\n\t\tconst scheduleStartupChainReset = () => {\n\t\t\tconst elapsedMs = Date.now() - startupStartedAt;\n\t\t\tconst timeoutMs = Math.max(SW_STARTUP_AUTO_UPDATE_GRACE_MS - elapsedMs, 0);\n\n\t\t\tstartupResetTimerId = window.setTimeout(() => {\n\t\t\t\tif (!isActive) return;\n\n\t\t\t\tclearServiceWorkerUpdateReloadChain();\n\t\t\t}, timeoutMs);\n\t\t};\n\n\t\tconst handleUpdate = (reg: ServiceWorkerRegistration) => {\n\t\t\tif (!isActive) return;\n\n\t\t\t/**\n\t\t\t * Если update найден вскоре после reload/navigate, применяем его\n\t\t\t * автоматически. Это закрывает цепочку из нескольких деплоев,\n\t\t\t * накопленных во время простоя вкладки.\n\t\t\t */\n\t\t\tif (tryApplyStartupUpdate(reg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Если update найден уже во время активной сессии —\n\t\t\t * показываем баннер.\n\t\t\t */\n\t\t\tsetWaitingRegistration(reg);\n\t\t};\n\n\t\tasync function init() {\n\t\t\tconst reg = await registerServiceWorker({\n\t\t\t\tonUpdate: handleUpdate\n\t\t\t});\n\n\t\t\tif (!isActive) return;\n\n\t\t\t/**\n\t\t\t * Важный сценарий:\n\t\t\t * пользователь перезагрузил страницу, а новый SW уже был в waiting.\n\t\t\t * В этом случае не показываем баннер, а применяем update сразу.\n\t\t\t */\n\t\t\tif (hasWaitingServiceWorker(reg) && tryApplyStartupUpdate(reg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst checkedReg = await checkServiceWorkerUpdate();\n\n\t\t\tif (!isActive) return;\n\t\t\tif (startupUpdateApplied) return;\n\n\t\t\tif (hasWaitingServiceWorker(checkedReg) && tryApplyStartupUpdate(checkedReg)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (hasWaitingServiceWorker(checkedReg)) {\n\t\t\t\tsetWaitingRegistration(checkedReg);\n\t\t\t}\n\n\t\t\tscheduleStartupChainReset();\n\t\t}\n\n\t\tvoid init();\n\n\t\tconst intervalId = window.setInterval(() => {\n\t\t\tvoid checkServiceWorkerUpdate().then((reg) => {\n\t\t\t\tif (!isActive) return;\n\n\t\t\t\tif (hasWaitingServiceWorker(reg)) {\n\t\t\t\t\tsetWaitingRegistration(reg);\n\t\t\t\t}\n\t\t\t});\n\t\t}, SW_UPDATE_CHECK_INTERVAL_MS);\n\n\t\treturn () => {\n\t\t\tisActive = false;\n\t\t\tif (startupResetTimerId !== null) {\n\t\t\t\twindow.clearTimeout(startupResetTimerId);\n\t\t\t}\n\t\t\twindow.clearInterval(intervalId);\n\t\t\tunsubscribeServiceWorkerUpdate(handleUpdate);\n\t\t};\n\t}, []);\n\n\treturn {\n\t\thasUpdate,\n\t\tapplyUpdate,\n\t\tdismiss\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** Наблюдает browser online hint без попытки подменить им проверку доступности конкретного сервера. */
2
+ export declare function useBrowserOnlineStatus(): boolean;
3
+ //# sourceMappingURL=useBrowserOnlineStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBrowserOnlineStatus.d.ts","sourceRoot":"","sources":["../../src/pwa/useBrowserOnlineStatus.ts"],"names":[],"mappings":"AAmBA,uGAAuG;AACvG,wBAAgB,sBAAsB,YAErC"}
@@ -119,33 +119,80 @@ function _() {
119
119
  }
120
120
  //#endregion
121
121
  //#region src/query-client/persistence.ts
122
- var v = "queries", y = __REACT_QUERY_PERSISTENCE_BUSTER__, b = 7776e6;
123
- function x(e) {
122
+ var v = "queries", y = "session", b = __REACT_QUERY_PERSISTENCE_BUSTER__, x = 7776e6, S = null;
123
+ function C(e) {
124
124
  if (e) return e;
125
125
  if (globalThis.indexedDB !== void 0) return globalThis.indexedDB;
126
126
  }
127
- function S(e) {
127
+ function w(e) {
128
128
  return new Promise((t, n) => {
129
129
  e.onsuccess = () => t(e.result), e.onerror = () => n(e.error);
130
130
  });
131
131
  }
132
- function C() {
132
+ function T() {
133
133
  if (__DEV__) return `dev:${__APP_ID__}`;
134
134
  let e = _().resolveSystemIdentifier?.();
135
135
  if (e) return e;
136
136
  let t = typeof window > "u" ? "server" : window.location.hostname;
137
137
  return `${__APP_ID__}:${t}`;
138
138
  }
139
- function w() {
140
- return `${C()}`;
139
+ function E() {
140
+ return `${T()}`;
141
141
  }
142
- function T(e) {
142
+ function D(e) {
143
+ return encodeURIComponent(e);
144
+ }
145
+ function O() {
146
+ return `${E()}-${y}-`;
147
+ }
148
+ function k(e) {
149
+ return `${O()}${D(e)}-`;
150
+ }
151
+ function A(e, t) {
152
+ return `${k(e)}${D(t)}`;
153
+ }
154
+ function j(e) {
155
+ return !!e?.id.trim() && Number.isFinite(Date.parse(e?.expiresAt ?? "")) && Date.parse(e?.expiresAt ?? "") > Date.now();
156
+ }
157
+ function M() {
158
+ return j(S) ? S : null;
159
+ }
160
+ async function N(e) {
161
+ let t = R();
162
+ if (!t?.entries) return;
163
+ let n = await t.entries();
164
+ await Promise.all(n.filter(([t]) => e(t)).map(([e]) => t.removeItem(e)));
165
+ }
166
+ async function P(e) {
167
+ let t = e === void 0 ? O() : k(e);
168
+ await N((e) => e.startsWith(t));
169
+ }
170
+ async function F(e) {
171
+ let t = e && j(e) ? {
172
+ id: e.id.trim(),
173
+ expiresAt: e.expiresAt
174
+ } : null;
175
+ if (S = t, !t) {
176
+ await P();
177
+ return;
178
+ }
179
+ let n = k(t.id);
180
+ await N((e) => e.startsWith(O()) && !e.startsWith(n));
181
+ }
182
+ function I(e) {
143
183
  return e.meta?.persist === !0 && e.meta.sessionScoped !== !0;
144
184
  }
145
- function E(e = {}) {
146
- let t = x(e.indexedDB);
185
+ function L(e) {
186
+ let t = e.meta?.sessionPersistence;
187
+ return e.meta?.sessionScoped !== !0 || !t || typeof t != "object" || !("cacheName" in t) || !("maxEntries" in t) || typeof t.cacheName != "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(t.cacheName) || !Number.isSafeInteger(t.maxEntries) || t.maxEntries < 1 || t.maxEntries > 5e3 ? null : {
188
+ cacheName: t.cacheName,
189
+ maxEntries: t.maxEntries
190
+ };
191
+ }
192
+ function R(e = {}) {
193
+ let t = C(e.indexedDB);
147
194
  if (!t) return;
148
- let n = t, r = e.dbName ?? C(), i = e.storeName ?? v, a = null;
195
+ let n = t, r = e.dbName ?? T(), i = e.storeName ?? v, a = null;
149
196
  function o() {
150
197
  return a || (a = new Promise((e, t) => {
151
198
  let a = n.open(r, 1);
@@ -156,7 +203,7 @@ function E(e = {}) {
156
203
  }), a);
157
204
  }
158
205
  async function s(e, t) {
159
- return S(t((await o()).transaction(i, e).objectStore(i)));
206
+ return w(t((await o()).transaction(i, e).objectStore(i)));
160
207
  }
161
208
  async function c(e, t) {
162
209
  let n = await o();
@@ -205,26 +252,73 @@ function E(e = {}) {
205
252
  }
206
253
  };
207
254
  }
208
- function D() {
209
- let e = E();
210
- if (e) return r({
211
- storage: e,
255
+ function z() {
256
+ let e = R();
257
+ if (!e) return;
258
+ let t = e, n = r({
259
+ storage: t,
212
260
  serialize: (e) => e,
213
261
  deserialize: (e) => e,
214
- prefix: w(),
215
- buster: _().persistenceBuster ?? y,
216
- maxAge: b,
262
+ prefix: E(),
263
+ buster: _().persistenceBuster ?? b,
264
+ maxAge: x,
217
265
  refetchOnRestore: !0,
218
- filters: { predicate: T }
219
- });
266
+ filters: { predicate: I }
267
+ }), i = /* @__PURE__ */ new Map();
268
+ function a(e, n, a) {
269
+ let o = A(e.id, n), s = `${o}:${a}:${e.expiresAt}`, c = i.get(s);
270
+ if (c) return c;
271
+ let l = Promise.resolve(), u = r({
272
+ storage: {
273
+ getItem: (n) => M()?.id === e.id ? t.getItem(n) : void 0,
274
+ removeItem: (e) => t.removeItem(e),
275
+ entries: () => t.entries(),
276
+ setItem: async (n, r) => {
277
+ M()?.id === e.id && (await t.setItem(n, r), l = l.then(async () => {
278
+ let e = (await t.entries()).filter(([e]) => e.startsWith(`${o}-`));
279
+ e.sort((e, t) => (t[1].state.dataUpdatedAt ?? 0) - (e[1].state.dataUpdatedAt ?? 0)), await Promise.all(e.slice(a).map(([e]) => t.removeItem(e)));
280
+ }), await l);
281
+ }
282
+ },
283
+ serialize: (e) => e,
284
+ deserialize: (e) => e,
285
+ prefix: o,
286
+ buster: _().persistenceBuster ?? b,
287
+ maxAge: x,
288
+ refetchOnRestore: !0,
289
+ filters: { predicate: (e) => L(e)?.cacheName === n }
290
+ });
291
+ return i.set(s, u), u;
292
+ }
293
+ return {
294
+ ...n,
295
+ async persisterFn(e, t, r) {
296
+ let i = L(r);
297
+ if (!i) return n.persisterFn(e, t, r);
298
+ let o = M();
299
+ return o ? a(o, i.cacheName, i.maxEntries).persisterFn(e, t, r) : Promise.resolve(e(t));
300
+ }
301
+ };
220
302
  }
221
303
  //#endregion
222
304
  //#region src/query-client/queryMeta.ts
223
- var O = { persist: !0 }, k = { sessionScoped: !0 };
305
+ var B = { persist: !0 }, V = { sessionScoped: !0 };
306
+ function H(e) {
307
+ let t = e.cacheName.trim();
308
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(t)) throw Error("Имя session query cache должно содержать только строчные латинские буквы, цифры, _ и -");
309
+ if (!Number.isSafeInteger(e.maxEntries) || e.maxEntries < 1 || e.maxEntries > 5e3) throw Error("Лимит session query cache должен быть целым числом от 1 до 5000");
310
+ return {
311
+ sessionScoped: !0,
312
+ sessionPersistence: {
313
+ cacheName: t,
314
+ maxEntries: e.maxEntries
315
+ }
316
+ };
317
+ }
224
318
  //#endregion
225
319
  //#region src/query-client/queryClient.ts
226
- function A({ onQueryError: e, onMutationError: t }) {
227
- let n = D();
320
+ function U({ onQueryError: e, onMutationError: t }) {
321
+ let n = z();
228
322
  return new o({
229
323
  queryCache: new a({ onError: e }),
230
324
  mutationCache: new i({ onError: t }),
@@ -243,35 +337,35 @@ function A({ onQueryError: e, onMutationError: t }) {
243
337
  }
244
338
  });
245
339
  }
246
- async function j(e, t, n) {
340
+ async function W(e, t, n) {
247
341
  await e.cancelQueries({ queryKey: t });
248
342
  let r = e.getQueryData(t);
249
343
  return e.setQueryData(t, n), { previous: r };
250
344
  }
251
- function M(e, t, n) {
345
+ function G(e, t, n) {
252
346
  n?.previous !== void 0 && e.setQueryData(t, n.previous);
253
347
  }
254
- function N(e, t, n, r) {
348
+ function K(e, t, n, r) {
255
349
  e.setQueryData(t, (e) => r(e, n));
256
350
  }
257
- async function P(e, t, n) {
258
- let r = n?.refetchType ?? "none", i = F(t) ? t : [t];
351
+ async function q(e, t, n) {
352
+ let r = n?.refetchType ?? "none", i = J(t) ? t : [t];
259
353
  await Promise.all(i.map((t) => e.invalidateQueries({
260
354
  queryKey: t,
261
355
  refetchType: r
262
356
  })));
263
357
  }
264
- function F(e) {
358
+ function J(e) {
265
359
  return Array.isArray(e) ? Array.isArray(e[0]) : !1;
266
360
  }
267
361
  //#endregion
268
362
  //#region src/query-client/sessionScoped.ts
269
- var I = "session-scoped-query-cache-reset", L;
270
- function R(e) {
363
+ var Y = "session-scoped-query-cache-reset", X;
364
+ function Z(e) {
271
365
  return e.meta?.sessionScoped === !0;
272
366
  }
273
- async function z(e, t = {}) {
274
- let n = { predicate: R };
367
+ async function Q(e, t = {}) {
368
+ let n = { predicate: Z };
275
369
  await e.cancelQueries(n);
276
370
  let r = e.getQueryCache().findAll(n);
277
371
  for (let t of r) {
@@ -285,26 +379,26 @@ async function z(e, t = {}) {
285
379
  });
286
380
  }
287
381
  await e.refetchQueries({
288
- predicate: (e) => R(e) && e.getObserversCount() > 0,
382
+ predicate: (e) => Z(e) && e.getObserversCount() > 0,
289
383
  type: "active"
290
- }, { cancelRefetch: !0 }), t.broadcast !== !1 && L?.();
384
+ }, { cancelRefetch: !0 }), t.broadcast !== !1 && X?.();
291
385
  }
292
- function B(e, t) {
386
+ function $(e, t) {
293
387
  if (typeof window > "u" || typeof BroadcastChannel > "u") return { cleanup: () => void 0 };
294
388
  let n = new BroadcastChannel(t.channelName), r = () => {
295
- let e = { type: I };
389
+ let e = { type: Y };
296
390
  n.postMessage(e);
297
391
  };
298
- return L = r, n.onmessage = (t) => {
299
- V(t.data) && z(e, { broadcast: !1 });
392
+ return X = r, n.onmessage = (t) => {
393
+ ee(t.data) && Q(e, { broadcast: !1 });
300
394
  }, { cleanup: () => {
301
- n.onmessage = null, n.close(), L === r && (L = void 0);
395
+ n.onmessage = null, n.close(), X === r && (X = void 0);
302
396
  } };
303
397
  }
304
- function V(e) {
305
- return typeof e == "object" && !!e && "type" in e && e.type === I;
398
+ function ee(e) {
399
+ return typeof e == "object" && !!e && "type" in e && e.type === Y;
306
400
  }
307
401
  //#endregion
308
- export { y as REACT_QUERY_PERSISTENCE_BUSTER, b as REACT_QUERY_PERSISTENCE_MAX_AGE, l as broadcastCacheEvent, g as configureQueryPersistenceProjectAdapter, E as createIndexedDbQueryStorage, A as createQueryClient, D as createReactQueryPersister, _ as getQueryPersistenceProjectAdapter, m as installQueryInvalidationBroadcast, u as installReactQueryBroadcast, B as installSessionScopedQueryReset, R as isSessionScopedQuery, M as onErrorOptimistic, j as onMutateOptimistic, P as onSettledOptimistic, N as onSuccessOptimistic, O as persistedQueryMeta, z as resetSessionScopedQueries, k as sessionScopedQueryMeta, c as setBroadcastFn, T as shouldPersistQuery };
402
+ export { b as REACT_QUERY_PERSISTENCE_BUSTER, x as REACT_QUERY_PERSISTENCE_MAX_AGE, l as broadcastCacheEvent, P as clearSessionQueryPersistence, g as configureQueryPersistenceProjectAdapter, R as createIndexedDbQueryStorage, U as createQueryClient, z as createReactQueryPersister, H as createSessionQueryPersistenceMeta, _ as getQueryPersistenceProjectAdapter, M as getSessionQueryPersistenceScope, m as installQueryInvalidationBroadcast, u as installReactQueryBroadcast, $ as installSessionScopedQueryReset, Z as isSessionScopedQuery, G as onErrorOptimistic, W as onMutateOptimistic, q as onSettledOptimistic, K as onSuccessOptimistic, B as persistedQueryMeta, Q as resetSessionScopedQueries, V as sessionScopedQueryMeta, c as setBroadcastFn, F as setSessionQueryPersistenceScope, I as shouldPersistQuery };
309
403
 
310
404
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/query-client/broadcast.ts","../../src/query-client/invalidationBroadcast.ts","../../src/query-client/queryPersistenceAdapter.ts","../../src/query-client/persistence.ts","../../src/query-client/queryMeta.ts","../../src/query-client/queryClient.ts","../../src/query-client/sessionScoped.ts"],"sourcesContent":["import { broadcastQueryClient } from \"@tanstack/query-broadcast-client-experimental\";\n\nimport { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\ntype CacheSyncEvent =\n\t| { type: \"invalidate\"; keys: QueryKey[]; refetchType?: \"none\" | \"all\" | \"active\" | \"inactive\" }\n\t| { type: \"setQueryData\"; key: QueryKey; data: unknown };\n\ntype InstallBroadcastOptions = {\n\tqueriesChannel: string;\n\teventsChannel: string;\n};\n\ntype InstalledReactQueryBroadcast = {\n\tbroadcast: (event: CacheSyncEvent) => void;\n\tcleanup: () => void;\n};\n\ntype WireMessage = {\n\ttabId: string;\n\tevent: CacheSyncEvent;\n};\n\nlet broadcastFn: ((event: CacheSyncEvent) => void) | null = null;\n\nexport function setBroadcastFn(fn: (event: CacheSyncEvent) => void) {\n\tbroadcastFn = fn;\n}\n\nexport function broadcastCacheEvent(event: CacheSyncEvent) {\n\tbroadcastFn?.(event);\n}\n\nexport function installReactQueryBroadcast(queryClient: QueryClient, opts: InstallBroadcastOptions): InstalledReactQueryBroadcast {\n\tif (typeof window === \"undefined\") {\n\t\treturn { broadcast: () => {}, cleanup: () => {} };\n\t}\n\n\tlet isApplyingRemoteEvent = false;\n\n\t// 1) Sync query cache (data/state)\n\tconst cleanupQueryBroadcast = broadcastQueryClient({\n\t\tqueryClient,\n\t\tbroadcastChannel: opts.queriesChannel\n\t});\n\n\t// 2) Sync \"events\" (invalidate/setQueryData)\n\tconst tabId = uuidv4();\n\tconst bc = new BroadcastChannel(opts.eventsChannel);\n\tconst unsubscribeInvalidationBroadcast = queryClient.getQueryCache().subscribe((queryEvent) => {\n\t\tif (isApplyingRemoteEvent) return;\n\t\tif (queryEvent.type !== \"updated\" || queryEvent.action.type !== \"invalidate\") return;\n\n\t\tbroadcast({\n\t\t\ttype: \"invalidate\",\n\t\t\tkeys: [queryEvent.query.queryKey],\n\t\t\trefetchType: \"none\"\n\t\t});\n\t});\n\n\tbc.onmessage = (ev: MessageEvent) => {\n\t\t// MessageEvent.data: unknown -> делаем безопасное сужение без any\n\t\tconst data: unknown = ev.data;\n\t\tif (!isWireMessage(data)) return;\n\t\tif (data.tabId === tabId) return;\n\n\t\tconst e = data.event;\n\t\tisApplyingRemoteEvent = true;\n\n\t\ttry {\n\t\t\tif (e.type === \"invalidate\") {\n\t\t\t\tconst refetchType = e.refetchType ?? \"none\";\n\t\t\t\tfor (const key of e.keys) {\n\t\t\t\t\tqueryClient.invalidateQueries({ queryKey: key, refetchType });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (e.type === \"setQueryData\") {\n\t\t\t\tqueryClient.setQueryData(e.key, e.data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} finally {\n\t\t\tisApplyingRemoteEvent = false;\n\t\t}\n\t};\n\n\tfunction broadcast(event: CacheSyncEvent) {\n\t\tconst msg: WireMessage = { tabId, event };\n\t\tbc.postMessage(msg);\n\t}\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup: () => {\n\t\t\tunsubscribeInvalidationBroadcast();\n\t\t\tcleanupQueryBroadcast();\n\t\t\tbc.close();\n\t\t}\n\t};\n}\n\nfunction isWireMessage(v: unknown): v is WireMessage {\n\tif (!isRecord(v)) return false;\n\tif (typeof v.tabId !== \"string\") return false;\n\tif (!(\"event\" in v)) return false;\n\treturn isCacheSyncEvent(v.event);\n}\n\nfunction isCacheSyncEvent(v: unknown): v is CacheSyncEvent {\n\tif (!isRecord(v)) return false;\n\tif (v.type === \"invalidate\") {\n\t\t// keys: QueryKey[] — runtime мы не сможем валидировать “идеально”,\n\t\t// но проверим что это массив, а refetchType — допустимое значение\n\t\tif (!Array.isArray(v.keys)) return false;\n\t\tif (v.refetchType === undefined) return true;\n\t\treturn v.refetchType === \"none\" || v.refetchType === \"active\" || v.refetchType === \"inactive\" || v.refetchType === \"all\";\n\t}\n\tif (v.type === \"setQueryData\") {\n\t\t// key должен быть массивом или строкой — QueryKey в рантайме обычно массив\n\t\t// проверим хотя бы наличие поля\n\t\treturn \"key\" in v && \"data\" in v;\n\t}\n\treturn false;\n}\n","import { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\nexport type QueryInvalidationBroadcastOptions = {\n\t/** Уникальное в пределах origin имя канала приложения. */\n\treadonly channelName: string;\n\t/** Ограничивает как исходящие, так и входящие Query keys. */\n\treadonly matchesQuery: (queryKey: QueryKey) => boolean;\n\t/** Определяет, какие queries получающей вкладки будут перезапрошены сразу. */\n\treadonly refetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\nexport type InstalledQueryInvalidationBroadcast = {\n\t/** Явно передаёт точечную инвалидацию без изменения локального QueryClient. */\n\treadonly broadcast: (queryKey: QueryKey) => void;\n\treadonly cleanup: () => void;\n};\n\ntype QueryInvalidationWireMessage = {\n\treadonly type: \"query.invalidate\";\n\treadonly senderId: string;\n\treadonly queryKey: QueryKey;\n};\n\nfunction isQueryInvalidationWireMessage(value: unknown): value is QueryInvalidationWireMessage {\n\treturn isRecord(value) && value.type === \"query.invalidate\" && typeof value.senderId === \"string\" && Array.isArray(value.queryKey);\n}\n\n/**\n * Передаёт между вкладками только факт инвалидации выбранного Query key.\n * Server data остаётся у QueryClient каждой вкладки и заново читается её queryFn.\n */\nexport function installQueryInvalidationBroadcast(\n\tqueryClient: QueryClient,\n\toptions: QueryInvalidationBroadcastOptions\n): InstalledQueryInvalidationBroadcast {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { broadcast: () => undefined, cleanup: () => undefined };\n\t}\n\n\tconst senderId = uuidv4();\n\tconst channel = new BroadcastChannel(options.channelName);\n\tlet applyingRemoteInvalidation = false;\n\tlet closed = false;\n\n\tconst broadcast = (queryKey: QueryKey) => {\n\t\tif (closed || !options.matchesQuery(queryKey)) return;\n\t\tchannel.postMessage({ type: \"query.invalidate\", senderId, queryKey } satisfies QueryInvalidationWireMessage);\n\t};\n\n\tconst unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n\t\tif (applyingRemoteInvalidation || event.type !== \"updated\" || event.action.type !== \"invalidate\") return;\n\t\tbroadcast(event.query.queryKey);\n\t});\n\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tconst message = event.data;\n\t\tif (closed || !isQueryInvalidationWireMessage(message) || message.senderId === senderId || !options.matchesQuery(message.queryKey))\n\t\t\treturn;\n\n\t\tapplyingRemoteInvalidation = true;\n\t\ttry {\n\t\t\tvoid queryClient.invalidateQueries({\n\t\t\t\tqueryKey: message.queryKey,\n\t\t\t\texact: true,\n\t\t\t\trefetchType: options.refetchType ?? \"active\"\n\t\t\t});\n\t\t} finally {\n\t\t\tapplyingRemoteInvalidation = false;\n\t\t}\n\t};\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup() {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tunsubscribe();\n\t\t\tchannel.close();\n\t\t}\n\t};\n}\n","export type QueryPersistenceProjectAdapter = {\n\tpersistenceBuster?: string;\n\tresolveSystemIdentifier?: () => string | null | undefined;\n};\n\nlet queryPersistenceProjectAdapter: QueryPersistenceProjectAdapter = {};\n\nexport function configureQueryPersistenceProjectAdapter(adapter: QueryPersistenceProjectAdapter) {\n\tqueryPersistenceProjectAdapter = adapter;\n}\n\nexport function getQueryPersistenceProjectAdapter() {\n\treturn queryPersistenceProjectAdapter;\n}\n","import { experimental_createQueryPersister, type AsyncStorage, type PersistedQuery } from \"@tanstack/query-persist-client-core\";\nimport { type Query } from \"@tanstack/react-query\";\n\nimport { getQueryPersistenceProjectAdapter } from \"./queryPersistenceAdapter\";\n\nconst STORE_NAME = \"queries\";\n\nexport const REACT_QUERY_PERSISTENCE_BUSTER = __REACT_QUERY_PERSISTENCE_BUSTER__;\nexport const REACT_QUERY_PERSISTENCE_MAX_AGE = 90 * 24 * 60 * 60 * 1000;\n\ntype IndexedDbQueryStorageOptions = {\n\tdbName?: string;\n\tstoreName?: string;\n\tindexedDB?: IDBFactory;\n};\n\n/**\n * Описывает итог синхронного преобразования записи внутри одной IndexedDB-транзакции.\n * Отдельный result позволяет вернуть вызывающему коду вычисленный снимок только после commit.\n */\nexport type IndexedDbAtomicUpdateDecision<TStorageValue, TResult> =\n\t| { readonly action: \"keep\"; readonly result: TResult }\n\t| { readonly action: \"set\"; readonly value: TStorageValue; readonly result: TResult }\n\t| { readonly action: \"remove\"; readonly result: TResult };\n\nexport type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> & {\n\t/**\n\t * Читает и изменяет одну запись в общей readwrite-транзакции, поэтому параллельные\n\t * вкладки не могут потерять уже зафиксированное изменение между get и set.\n\t * Callback должен быть синхронным: IndexedDB закрывает неактивную транзакцию до\n\t * завершения произвольной асинхронной работы.\n\t */\n\treadonly updateItem: <TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) => Promise<TResult>;\n};\n\nfunction resolveIndexedDbFactory(factory?: IDBFactory) {\n\tif (factory) return factory;\n\tif (typeof globalThis.indexedDB === \"undefined\") return undefined;\n\treturn globalThis.indexedDB;\n}\n\nfunction requestToPromise<T>(request: IDBRequest<T>) {\n\treturn new Promise<T>((resolve, reject) => {\n\t\trequest.onsuccess = () => resolve(request.result);\n\t\trequest.onerror = () => reject(request.error);\n\t});\n}\n\nfunction getSystemIdentifier() {\n\tif (__DEV__) {\n\t\treturn `dev:${__APP_ID__}`;\n\t}\n\n\tconst projectIdentifier = getQueryPersistenceProjectAdapter().resolveSystemIdentifier?.();\n\tif (projectIdentifier) return projectIdentifier;\n\n\tconst hostname = typeof window === \"undefined\" ? \"server\" : window.location.hostname;\n\treturn `${__APP_ID__}:${hostname}`;\n}\n\nfunction getPersistencePrefix() {\n\tconst system = getSystemIdentifier();\n\treturn `${system}`;\n}\n\nexport function shouldPersistQuery(query: Pick<Query, \"meta\">) {\n\treturn query.meta?.persist === true && query.meta.sessionScoped !== true;\n}\n\nexport function createIndexedDbQueryStorage<TStorageValue = unknown>(\n\toptions: IndexedDbQueryStorageOptions = {}\n): IndexedDbQueryStorage<TStorageValue> | undefined {\n\tconst factory = resolveIndexedDbFactory(options.indexedDB);\n\tif (!factory) return undefined;\n\n\tconst idb = factory;\n\tconst dbName = options.dbName ?? getSystemIdentifier();\n\tconst storeName = options.storeName ?? STORE_NAME;\n\tlet dbPromise: Promise<IDBDatabase> | null = null;\n\n\tfunction openDb() {\n\t\tif (dbPromise) return dbPromise;\n\n\t\tdbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n\t\t\tconst request = idb.open(dbName, 1);\n\n\t\t\trequest.onupgradeneeded = () => {\n\t\t\t\tconst db = request.result;\n\t\t\t\tif (!db.objectStoreNames.contains(storeName)) {\n\t\t\t\t\tdb.createObjectStore(storeName);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trequest.onsuccess = () => resolve(request.result);\n\t\t\trequest.onerror = () => reject(request.error);\n\t\t\trequest.onblocked = () => reject(request.error ?? new Error(\"Открытие IndexedDB заблокировано\"));\n\t\t});\n\n\t\treturn dbPromise;\n\t}\n\n\tasync function runTransaction<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => IDBRequest<T>) {\n\t\tconst db = await openDb();\n\t\tconst tx = db.transaction(storeName, mode);\n\t\tconst store = tx.objectStore(storeName);\n\t\tconst request = action(store);\n\t\treturn requestToPromise(request);\n\t}\n\n\tasync function runWrite(mode: IDBTransactionMode, action: (store: IDBObjectStore) => void) {\n\t\tconst db = await openDb();\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, mode);\n\t\t\tconst store = tx.objectStore(storeName);\n\n\t\t\ttx.oncomplete = () => resolve();\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\ttry {\n\t\t\t\taction(store);\n\t\t\t} catch (error) {\n\t\t\t\ttx.abort();\n\t\t\t\treject(error);\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function updateItem<TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) {\n\t\tconst db = await openDb();\n\t\treturn new Promise<TResult>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, \"readwrite\");\n\t\t\tconst store = tx.objectStore(storeName);\n\t\t\tconst request = store.get(key);\n\t\t\tlet result: TResult;\n\t\t\tlet hasResult = false;\n\n\t\t\ttx.oncomplete = () => {\n\t\t\t\tif (hasResult) resolve(result);\n\t\t\t\telse reject(new Error(\"IndexedDB-транзакция завершилась без результата обновления\"));\n\t\t\t};\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\t\t\trequest.onerror = () => {\n\t\t\t\ttx.abort();\n\t\t\t\treject(request.error);\n\t\t\t};\n\t\t\trequest.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst decision = update(request.result as TStorageValue | undefined);\n\t\t\t\t\tresult = decision.result;\n\t\t\t\t\thasResult = true;\n\t\t\t\t\tif (decision.action === \"set\") store.put(decision.value, key);\n\t\t\t\t\telse if (decision.action === \"remove\") store.delete(key);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttx.abort();\n\t\t\t\t\treject(error);\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\t}\n\n\treturn {\n\t\tgetItem: (key) => runTransaction<TStorageValue | undefined>(\"readonly\", (store) => store.get(key)),\n\t\tsetItem: (key, value) => runWrite(\"readwrite\", (store) => store.put(value, key)),\n\t\tremoveItem: (key) => runWrite(\"readwrite\", (store) => store.delete(key)),\n\t\tupdateItem,\n\t\tentries: async () => {\n\t\t\tconst db = await openDb();\n\n\t\t\treturn new Promise<Array<[key: string, value: TStorageValue]>>((resolve, reject) => {\n\t\t\t\tconst tx = db.transaction(storeName, \"readonly\");\n\t\t\t\tconst store = tx.objectStore(storeName);\n\t\t\t\tconst request = store.openCursor();\n\t\t\t\tconst entries: Array<[key: string, value: TStorageValue]> = [];\n\n\t\t\t\ttx.oncomplete = () => resolve(entries);\n\t\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\t\trequest.onsuccess = () => {\n\t\t\t\t\tconst cursor = request.result;\n\t\t\t\t\tif (!cursor) return;\n\n\t\t\t\t\tentries.push([String(cursor.key), cursor.value as TStorageValue]);\n\t\t\t\t\tcursor.continue();\n\t\t\t\t};\n\t\t\t\trequest.onerror = () => reject(request.error);\n\t\t\t});\n\t\t}\n\t};\n}\n\nexport function createReactQueryPersister() {\n\tconst storage = createIndexedDbQueryStorage<PersistedQuery>();\n\tif (!storage) return undefined;\n\n\treturn experimental_createQueryPersister<PersistedQuery>({\n\t\tstorage,\n\t\tserialize: (query) => query,\n\t\tdeserialize: (query) => query,\n\t\tprefix: getPersistencePrefix(),\n\t\tbuster: getQueryPersistenceProjectAdapter().persistenceBuster ?? REACT_QUERY_PERSISTENCE_BUSTER,\n\t\tmaxAge: REACT_QUERY_PERSISTENCE_MAX_AGE,\n\t\trefetchOnRestore: true,\n\t\tfilters: { predicate: shouldPersistQuery }\n\t});\n}\n","import { type QueryMeta } from \"@tanstack/react-query\";\n\n/**\n * Общие признаки query, которые управляют инфраструктурой клиентского кеша.\n *\n * `persist` разрешает долговременное хранение публичных справочных данных,\n * а `sessionScoped` помечает данные, доступность или содержимое которых зависит\n * от текущей серверной сессии. Эти признаки нельзя совмещать: session-scoped\n * query никогда не должна переживать смену пользователя или роли.\n */\nexport type AppQueryMeta = {\n\treadonly persist?: boolean;\n\treadonly sessionScoped?: boolean;\n} & Record<string, unknown>;\n\ndeclare module \"@tanstack/react-query\" {\n\tinterface Register {\n\t\tqueryMeta: AppQueryMeta;\n\t}\n}\n\n/** Разрешает сохранять публичную query в долговременном хранилище. */\nexport const persistedQueryMeta = { persist: true } as const satisfies QueryMeta;\n\n/** Помечает query для сброса при изменении серверной сессии. */\nexport const sessionScopedQueryMeta = { sessionScoped: true } as const satisfies QueryMeta;\n","import {\n\tDefaultError,\n\tMutation,\n\tMutationCache,\n\tMutationFunctionContext,\n\tQuery,\n\tQueryCache,\n\tQueryClient,\n\tQueryKey\n} from \"@tanstack/react-query\";\n\nimport { createReactQueryPersister } from \"./persistence\";\n\nexport type QueryBroadcastOptions = {\n\tonQueryError?: (error: DefaultError, query: Query<unknown, unknown, unknown>) => void;\n\tonMutationError?: (\n\t\terror: DefaultError,\n\t\tvariables: unknown,\n\t\tonMutateResult: unknown,\n\t\tmutation: Mutation<unknown, unknown, unknown>,\n\t\tcontext: MutationFunctionContext\n\t) => Promise<unknown> | unknown;\n};\n\n/**\n * Создаёт настроенный QueryClient. Singleton lifecycle обеспечивает host-приложение.\n */\nexport function createQueryClient({ onQueryError, onMutationError }: QueryBroadcastOptions) {\n\tconst queryPersister = createReactQueryPersister();\n\n\tconst queryClient = new QueryClient({\n\t\tqueryCache: new QueryCache({ onError: onQueryError }),\n\t\tmutationCache: new MutationCache({ onError: onMutationError }),\n\t\tdefaultOptions: {\n\t\t\t// TODO: Vite env\n\t\t\tqueries: {\n\t\t\t\tstaleTime: 10 * 60 * 1000,\n\t\t\t\tgcTime: 30 * 60 * 1000,\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false,\n\t\t\t\t...(queryPersister ? { persister: queryPersister.persisterFn } : {})\n\t\t\t},\n\t\t\tmutations: {\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false\n\t\t\t}\n\t\t}\n\t});\n\n\treturn queryClient;\n}\n\n/**\n * Контекст, возвращаемый из `onMutateOptimistic`.\n * Содержит снапшот кеша до оптимистичного обновления.\n * Передаётся в `onError`, `onSuccess`, `onSettled` через TanStack Query.\n */\nexport type OptimisticContext<TQueryCacheData> = {\n\tprevious: TQueryCacheData | undefined;\n};\n\n/**\n * _Оптимистичное обновление — это когда мы моментально меняем локальный кэш (и UI) до ответа сервера,\n * чтобы интерфейс был отзывчивым. Если сервер вернул ошибку — откатываем изменения.\n * Если сервер подтвердил — либо принимаем ответ сервера,\n * либо оставляем оптимистичное состояние (в зависимости от ситуации)._\n *\n * Обработчик `onMutate` для оптимистичного обновления.\n *\n * Выполняет три шага:\n * 1. Отменяет активные запросы по ключу — предотвращает гонку с сервером.\n * 2. Сохраняет снапшот текущего состояния кеша.\n * 3. Применяет оптимистичный патч через `patcher`.\n *\n * Возвращает {@link OptimisticContext} — передаётся в `onError` для отката.\n *\n * @example\n * // Обновление точечных данных в списке объектов\n * useMutation({\n * mutationFn: (data: SomeType) => { ... },\n * onMutate: async (data) =>\n * onMutateOptimistic(queryClient, someQueryKey, (old) => {\n * if (!old) return old;\n * return old.map((item) =>\n * item.id === data.id ? { ...item, visible: data.visible } : item\n * );\n * }),\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n * })\n */\nexport async function onMutateOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tpatcher: (oldCache: TQueryCacheData | undefined) => TQueryCacheData | undefined\n): Promise<OptimisticContext<TQueryCacheData>> {\n\t// 1) Отменяем запросы по ключу, чтобы не было гонки с сервером\n\tawait qc.cancelQueries({ queryKey });\n\n\t// 2) Сохраняем снапшот кеша для возможного отката\n\tconst previous = qc.getQueryData<TQueryCacheData>(queryKey);\n\n\t// 3) Применяем оптимистичный патч\n\tqc.setQueryData<TQueryCacheData>(queryKey, patcher);\n\n\treturn { previous };\n}\n\n/**\n * Обработчик `onError` для оптимистичного обновления.\n *\n * Откатывает кеш к снапшоту, сохранённому в {@link OptimisticContext}.\n * Если `context` не определён (например, `onMutate` сам выбросил ошибку) — безопасно ничего не делает.\n *\n * @example\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n */\nexport function onErrorOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tcontext: OptimisticContext<TQueryCacheData> | undefined\n): void {\n\tif (context?.previous !== undefined) {\n\t\tqc.setQueryData<TQueryCacheData>(queryKey, context.previous);\n\t}\n}\n\n/**\n * Обработчик `onSuccess` для оптимистичного обновления — применяет данные сервера в кеш.\n *\n * Используется, когда сервер вернул авторитетные данные, которые нужно слить\n * с текущим состоянием кеша (или заменить его целиком).\n *\n * Если сервер вернул `void` — эту функцию вызывать не нужно:\n * оптимистичное состояние в кеше остаётся без изменений.\n *\n * @param qc - экземпляр `QueryClient`\n * @param queryKey - ключ кеша\n * @param serverData - данные, вернувшиеся от сервера\n * @param merger - функция слияния: получает текущий кеш и ответ сервера, возвращает новое состояние\n *\n * @example\n * // Полная замена кеша ответом сервера\n * onSuccess: (data) =>\n * onSuccessOptimistic(queryClient, someQueryKey, data, (_cache, serverData) => serverData),\n *\n * @example\n * // Точечное обновление одного элемента в списке\n * onSuccess: (updatedItem) =>\n * onSuccessOptimistic(queryClient, someQueryKey, updatedItem,\n * (cache, item) => cache?.map((i) => (i.id === item.id ? item : i))\n * ),\n */\nexport function onSuccessOptimistic<TQueryCacheData, TServerData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tserverData: TServerData,\n\tmerger: (cache: TQueryCacheData | undefined, serverData: TServerData) => TQueryCacheData | undefined\n): void {\n\tqc.setQueryData<TQueryCacheData>(queryKey, (old) => merger(old, serverData));\n}\n\n/**\n * Параметры для {@link onSettledOptimistic}.\n */\nexport type OnSettledOptimisticOptions = {\n\t/**\n\t * Тип рефетча при инвалидации.\n\t * - `\"none\"` (по умолчанию) — помечает кеш устаревшим, но не запускает рефетч немедленно.\n\t * - `\"active\"` — немедленно перезапрашивает активные подписчики.\n\t * - `\"inactive\"` — перезапрашивает неактивные (фоновые) запросы.\n\t * - `\"all\"` — перезапрашивает все подходящие запросы.\n\t */\n\trefetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\n/**\n * Обработчик `onSettled` для оптимистичного обновления.\n *\n * Инвалидирует один или несколько query-ключей после завершения мутации\n * (выполняется и при успехе, и при ошибке).\n *\n * По умолчанию использует `refetchType: \"none\"` — помечает кеш устаревшим,\n * но не запускает немедленный рефетч. Это безопасный вариант при оптимистике:\n * данные в кеше уже актуальны (подтверждены или откатаны), а рефетч произойдёт\n * при следующем монтировании или явном запросе.\n *\n * Broadcast в другие вкладки — ответственность вызывающего кода через `broadcastCacheEvent`.\n *\n * @example\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n *\n * @example\n * // С принудительным рефетчем активных подписчиков\n * onSettled: () =>\n * onSettledOptimistic(queryClient, [queryKeyA, queryKeyB], { refetchType: \"active\" }),\n */\nexport async function onSettledOptimistic(\n\tqc: QueryClient,\n\tqueryKeys: QueryKey | QueryKey[],\n\toptions?: OnSettledOptimisticOptions\n): Promise<void> {\n\tconst refetchType = options?.refetchType ?? \"none\";\n\t// Нормализуем одиночный ключ к массиву для единообразной обработки\n\tconst keys: QueryKey[] = isNestedQueryKey(queryKeys) ? queryKeys : [queryKeys];\n\n\tawait Promise.all(keys.map((queryKey) => qc.invalidateQueries({ queryKey, refetchType })));\n}\n\n/**\n * Определяет, является ли значение массивом query-ключей (QueryKey[]),\n * а не одиночным QueryKey.\n *\n * QueryKey в TanStack Query — это `string | readonly unknown[]`.\n * Чтобы отличить `QueryKey[]` от `QueryKey` типа `readonly unknown[]`,\n * проверяем, содержит ли массив хотя бы один вложенный массив или строку на верхнем уровне.\n */\nfunction isNestedQueryKey(value: QueryKey | QueryKey[]): value is QueryKey[] {\n\tif (!Array.isArray(value)) return false;\n\t// Если первый элемент сам является массивом — это QueryKey[]\n\treturn Array.isArray(value[0]);\n}\n","import { type Query, type QueryClient } from \"@tanstack/react-query\";\n\nconst SESSION_SCOPED_QUERY_CACHE_RESET_EVENT = \"session-scoped-query-cache-reset\";\n\ntype SessionScopedQueryCacheResetEvent = {\n\treadonly type: typeof SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n};\n\nexport type ResetSessionScopedQueriesOptions = {\n\t/**\n\t * Передавать ли команду сброса другим вкладкам. При обработке входящего\n\t * сообщения значение отключается, чтобы вкладки не создавали цикл событий.\n\t */\n\treadonly broadcast?: boolean;\n};\n\nexport type InstallSessionScopedQueryResetOptions = {\n\t/** Уникальное для приложения имя служебного BroadcastChannel. */\n\treadonly channelName: string;\n};\n\nexport type InstalledSessionScopedQueryReset = {\n\t/** Освобождает канал и прекращает межвкладочную синхронизацию. */\n\treadonly cleanup: () => void;\n};\n\nlet broadcastSessionScopedQueryReset: (() => void) | undefined;\n\n/** Проверяет, относится ли query к данным текущей серверной сессии. */\nexport function isSessionScopedQuery(query: Pick<Query, \"meta\">): boolean {\n\treturn query.meta?.sessionScoped === true;\n}\n\n/**\n * Сбрасывает все session-scoped query после входа, выхода, обновления сессии\n * или потери авторизации.\n *\n * Сначала отменяются незавершённые запросы старой сессии, затем TanStack Query\n * очищает их состояние и повторно запрашивает только активные query. Обычные\n * публичные данные при этом остаются в кеше. По умолчанию отдельная команда\n * сброса отправляется другим вкладкам без передачи содержимого кеша или токенов.\n */\nexport async function resetSessionScopedQueries(queryClient: QueryClient, options: ResetSessionScopedQueriesOptions = {}): Promise<void> {\n\tconst filters = { predicate: isSessionScopedQuery } as const;\n\n\tawait queryClient.cancelQueries(filters);\n\n\tconst matchedQueries = queryClient.getQueryCache().findAll(filters);\n\tfor (const query of matchedQueries) {\n\t\tif (query.getObserversCount() === 0) {\n\t\t\tqueryClient.getQueryCache().remove(query);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Hydrated query хранит SSR-данные как initial state, поэтому одного\n\t\t// query.reset() недостаточно: старая сессия могла бы вернуться в UI.\n\t\tquery.reset();\n\t\tquery.setState({ data: undefined, dataUpdatedAt: 0 });\n\t}\n\n\tawait queryClient.refetchQueries(\n\t\t{\n\t\t\tpredicate: (query) => isSessionScopedQuery(query) && query.getObserversCount() > 0,\n\t\t\ttype: \"active\"\n\t\t},\n\t\t{ cancelRefetch: true }\n\t);\n\n\tif (options.broadcast !== false) {\n\t\tbroadcastSessionScopedQueryReset?.();\n\t}\n}\n\n/**\n * Подключает безопасную межвкладочную синхронизацию смены сессии.\n *\n * Канал передаёт только команду сброса. В отличие от полной синхронизации\n * Query cache, он не публикует данные query и потому подходит для auth-зависимых\n * ответов. На сервере и в браузерах без BroadcastChannel возвращается no-op.\n */\nexport function installSessionScopedQueryReset(\n\tqueryClient: QueryClient,\n\toptions: InstallSessionScopedQueryResetOptions\n): InstalledSessionScopedQueryReset {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { cleanup: () => undefined };\n\t}\n\n\tconst channel = new BroadcastChannel(options.channelName);\n\tconst broadcast = () => {\n\t\tconst event: SessionScopedQueryCacheResetEvent = {\n\t\t\ttype: SESSION_SCOPED_QUERY_CACHE_RESET_EVENT\n\t\t};\n\t\tchannel.postMessage(event);\n\t};\n\n\tbroadcastSessionScopedQueryReset = broadcast;\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tif (!isSessionScopedQueryCacheResetEvent(event.data)) return;\n\t\tvoid resetSessionScopedQueries(queryClient, { broadcast: false });\n\t};\n\n\treturn {\n\t\tcleanup: () => {\n\t\t\tchannel.onmessage = null;\n\t\t\tchannel.close();\n\t\t\tif (broadcastSessionScopedQueryReset === broadcast) {\n\t\t\t\tbroadcastSessionScopedQueryReset = undefined;\n\t\t\t}\n\t\t}\n\t};\n}\n\n/** Защищает обработчик канала от посторонних или устаревших сообщений. */\nfunction isSessionScopedQueryCacheResetEvent(value: unknown): value is SessionScopedQueryCacheResetEvent {\n\treturn typeof value === \"object\" && value !== null && \"type\" in value && value.type === SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/query-client/broadcast.ts","../../src/query-client/invalidationBroadcast.ts","../../src/query-client/queryPersistenceAdapter.ts","../../src/query-client/persistence.ts","../../src/query-client/queryMeta.ts","../../src/query-client/queryClient.ts","../../src/query-client/sessionScoped.ts"],"sourcesContent":["import { broadcastQueryClient } from \"@tanstack/query-broadcast-client-experimental\";\n\nimport { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\ntype CacheSyncEvent =\n\t| { type: \"invalidate\"; keys: QueryKey[]; refetchType?: \"none\" | \"all\" | \"active\" | \"inactive\" }\n\t| { type: \"setQueryData\"; key: QueryKey; data: unknown };\n\ntype InstallBroadcastOptions = {\n\tqueriesChannel: string;\n\teventsChannel: string;\n};\n\ntype InstalledReactQueryBroadcast = {\n\tbroadcast: (event: CacheSyncEvent) => void;\n\tcleanup: () => void;\n};\n\ntype WireMessage = {\n\ttabId: string;\n\tevent: CacheSyncEvent;\n};\n\nlet broadcastFn: ((event: CacheSyncEvent) => void) | null = null;\n\nexport function setBroadcastFn(fn: (event: CacheSyncEvent) => void) {\n\tbroadcastFn = fn;\n}\n\nexport function broadcastCacheEvent(event: CacheSyncEvent) {\n\tbroadcastFn?.(event);\n}\n\nexport function installReactQueryBroadcast(queryClient: QueryClient, opts: InstallBroadcastOptions): InstalledReactQueryBroadcast {\n\tif (typeof window === \"undefined\") {\n\t\treturn { broadcast: () => {}, cleanup: () => {} };\n\t}\n\n\tlet isApplyingRemoteEvent = false;\n\n\t// 1) Sync query cache (data/state)\n\tconst cleanupQueryBroadcast = broadcastQueryClient({\n\t\tqueryClient,\n\t\tbroadcastChannel: opts.queriesChannel\n\t});\n\n\t// 2) Sync \"events\" (invalidate/setQueryData)\n\tconst tabId = uuidv4();\n\tconst bc = new BroadcastChannel(opts.eventsChannel);\n\tconst unsubscribeInvalidationBroadcast = queryClient.getQueryCache().subscribe((queryEvent) => {\n\t\tif (isApplyingRemoteEvent) return;\n\t\tif (queryEvent.type !== \"updated\" || queryEvent.action.type !== \"invalidate\") return;\n\n\t\tbroadcast({\n\t\t\ttype: \"invalidate\",\n\t\t\tkeys: [queryEvent.query.queryKey],\n\t\t\trefetchType: \"none\"\n\t\t});\n\t});\n\n\tbc.onmessage = (ev: MessageEvent) => {\n\t\t// MessageEvent.data: unknown -> делаем безопасное сужение без any\n\t\tconst data: unknown = ev.data;\n\t\tif (!isWireMessage(data)) return;\n\t\tif (data.tabId === tabId) return;\n\n\t\tconst e = data.event;\n\t\tisApplyingRemoteEvent = true;\n\n\t\ttry {\n\t\t\tif (e.type === \"invalidate\") {\n\t\t\t\tconst refetchType = e.refetchType ?? \"none\";\n\t\t\t\tfor (const key of e.keys) {\n\t\t\t\t\tqueryClient.invalidateQueries({ queryKey: key, refetchType });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (e.type === \"setQueryData\") {\n\t\t\t\tqueryClient.setQueryData(e.key, e.data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} finally {\n\t\t\tisApplyingRemoteEvent = false;\n\t\t}\n\t};\n\n\tfunction broadcast(event: CacheSyncEvent) {\n\t\tconst msg: WireMessage = { tabId, event };\n\t\tbc.postMessage(msg);\n\t}\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup: () => {\n\t\t\tunsubscribeInvalidationBroadcast();\n\t\t\tcleanupQueryBroadcast();\n\t\t\tbc.close();\n\t\t}\n\t};\n}\n\nfunction isWireMessage(v: unknown): v is WireMessage {\n\tif (!isRecord(v)) return false;\n\tif (typeof v.tabId !== \"string\") return false;\n\tif (!(\"event\" in v)) return false;\n\treturn isCacheSyncEvent(v.event);\n}\n\nfunction isCacheSyncEvent(v: unknown): v is CacheSyncEvent {\n\tif (!isRecord(v)) return false;\n\tif (v.type === \"invalidate\") {\n\t\t// keys: QueryKey[] — runtime мы не сможем валидировать “идеально”,\n\t\t// но проверим что это массив, а refetchType — допустимое значение\n\t\tif (!Array.isArray(v.keys)) return false;\n\t\tif (v.refetchType === undefined) return true;\n\t\treturn v.refetchType === \"none\" || v.refetchType === \"active\" || v.refetchType === \"inactive\" || v.refetchType === \"all\";\n\t}\n\tif (v.type === \"setQueryData\") {\n\t\t// key должен быть массивом или строкой — QueryKey в рантайме обычно массив\n\t\t// проверим хотя бы наличие поля\n\t\treturn \"key\" in v && \"data\" in v;\n\t}\n\treturn false;\n}\n","import { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\nexport type QueryInvalidationBroadcastOptions = {\n\t/** Уникальное в пределах origin имя канала приложения. */\n\treadonly channelName: string;\n\t/** Ограничивает как исходящие, так и входящие Query keys. */\n\treadonly matchesQuery: (queryKey: QueryKey) => boolean;\n\t/** Определяет, какие queries получающей вкладки будут перезапрошены сразу. */\n\treadonly refetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\nexport type InstalledQueryInvalidationBroadcast = {\n\t/** Явно передаёт точечную инвалидацию без изменения локального QueryClient. */\n\treadonly broadcast: (queryKey: QueryKey) => void;\n\treadonly cleanup: () => void;\n};\n\ntype QueryInvalidationWireMessage = {\n\treadonly type: \"query.invalidate\";\n\treadonly senderId: string;\n\treadonly queryKey: QueryKey;\n};\n\nfunction isQueryInvalidationWireMessage(value: unknown): value is QueryInvalidationWireMessage {\n\treturn isRecord(value) && value.type === \"query.invalidate\" && typeof value.senderId === \"string\" && Array.isArray(value.queryKey);\n}\n\n/**\n * Передаёт между вкладками только факт инвалидации выбранного Query key.\n * Server data остаётся у QueryClient каждой вкладки и заново читается её queryFn.\n */\nexport function installQueryInvalidationBroadcast(\n\tqueryClient: QueryClient,\n\toptions: QueryInvalidationBroadcastOptions\n): InstalledQueryInvalidationBroadcast {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { broadcast: () => undefined, cleanup: () => undefined };\n\t}\n\n\tconst senderId = uuidv4();\n\tconst channel = new BroadcastChannel(options.channelName);\n\tlet applyingRemoteInvalidation = false;\n\tlet closed = false;\n\n\tconst broadcast = (queryKey: QueryKey) => {\n\t\tif (closed || !options.matchesQuery(queryKey)) return;\n\t\tchannel.postMessage({ type: \"query.invalidate\", senderId, queryKey } satisfies QueryInvalidationWireMessage);\n\t};\n\n\tconst unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n\t\tif (applyingRemoteInvalidation || event.type !== \"updated\" || event.action.type !== \"invalidate\") return;\n\t\tbroadcast(event.query.queryKey);\n\t});\n\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tconst message = event.data;\n\t\tif (closed || !isQueryInvalidationWireMessage(message) || message.senderId === senderId || !options.matchesQuery(message.queryKey))\n\t\t\treturn;\n\n\t\tapplyingRemoteInvalidation = true;\n\t\ttry {\n\t\t\tvoid queryClient.invalidateQueries({\n\t\t\t\tqueryKey: message.queryKey,\n\t\t\t\texact: true,\n\t\t\t\trefetchType: options.refetchType ?? \"active\"\n\t\t\t});\n\t\t} finally {\n\t\t\tapplyingRemoteInvalidation = false;\n\t\t}\n\t};\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup() {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tunsubscribe();\n\t\t\tchannel.close();\n\t\t}\n\t};\n}\n","export type QueryPersistenceProjectAdapter = {\n\tpersistenceBuster?: string;\n\tresolveSystemIdentifier?: () => string | null | undefined;\n};\n\nlet queryPersistenceProjectAdapter: QueryPersistenceProjectAdapter = {};\n\nexport function configureQueryPersistenceProjectAdapter(adapter: QueryPersistenceProjectAdapter) {\n\tqueryPersistenceProjectAdapter = adapter;\n}\n\nexport function getQueryPersistenceProjectAdapter() {\n\treturn queryPersistenceProjectAdapter;\n}\n","import { experimental_createQueryPersister, type AsyncStorage, type PersistedQuery } from \"@tanstack/query-persist-client-core\";\nimport { type Query, type QueryFunction, type QueryFunctionContext, type QueryKey } from \"@tanstack/react-query\";\n\nimport { getQueryPersistenceProjectAdapter } from \"./queryPersistenceAdapter\";\n\nconst STORE_NAME = \"queries\";\nconst SESSION_PERSISTENCE_MARKER = \"session\";\n\nexport const REACT_QUERY_PERSISTENCE_BUSTER = __REACT_QUERY_PERSISTENCE_BUSTER__;\nexport const REACT_QUERY_PERSISTENCE_MAX_AGE = 90 * 24 * 60 * 60 * 1000;\n\ntype IndexedDbQueryStorageOptions = {\n\tdbName?: string;\n\tstoreName?: string;\n\tindexedDB?: IDBFactory;\n};\n\nexport type SessionQueryPersistenceScope = {\n\t/** Публичная identity серверной сессии; cookie secret или access token здесь недопустимы. */\n\treadonly id: string;\n\t/** Абсолютная граница, после которой snapshots этой сессии нельзя читать или записывать. */\n\treadonly expiresAt: string;\n};\n\nlet activeSessionPersistenceScope: SessionQueryPersistenceScope | null = null;\n\n/**\n * Описывает итог синхронного преобразования записи внутри одной IndexedDB-транзакции.\n * Отдельный result позволяет вернуть вызывающему коду вычисленный снимок только после commit.\n */\nexport type IndexedDbAtomicUpdateDecision<TStorageValue, TResult> =\n\t| { readonly action: \"keep\"; readonly result: TResult }\n\t| { readonly action: \"set\"; readonly value: TStorageValue; readonly result: TResult }\n\t| { readonly action: \"remove\"; readonly result: TResult };\n\nexport type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> & {\n\t/** Перечисляет записи object store для garbage collection и bounded persistence. */\n\treadonly entries: () => Promise<Array<[key: string, value: TStorageValue]>>;\n\t/**\n\t * Читает и изменяет одну запись в общей readwrite-транзакции, поэтому параллельные\n\t * вкладки не могут потерять уже зафиксированное изменение между get и set.\n\t * Callback должен быть синхронным: IndexedDB закрывает неактивную транзакцию до\n\t * завершения произвольной асинхронной работы.\n\t */\n\treadonly updateItem: <TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) => Promise<TResult>;\n};\n\nfunction resolveIndexedDbFactory(factory?: IDBFactory) {\n\tif (factory) return factory;\n\tif (typeof globalThis.indexedDB === \"undefined\") return undefined;\n\treturn globalThis.indexedDB;\n}\n\nfunction requestToPromise<T>(request: IDBRequest<T>) {\n\treturn new Promise<T>((resolve, reject) => {\n\t\trequest.onsuccess = () => resolve(request.result);\n\t\trequest.onerror = () => reject(request.error);\n\t});\n}\n\nfunction getSystemIdentifier() {\n\tif (__DEV__) {\n\t\treturn `dev:${__APP_ID__}`;\n\t}\n\n\tconst projectIdentifier = getQueryPersistenceProjectAdapter().resolveSystemIdentifier?.();\n\tif (projectIdentifier) return projectIdentifier;\n\n\tconst hostname = typeof window === \"undefined\" ? \"server\" : window.location.hostname;\n\treturn `${__APP_ID__}:${hostname}`;\n}\n\nfunction getPersistencePrefix() {\n\tconst system = getSystemIdentifier();\n\treturn `${system}`;\n}\n\nfunction encodePersistenceSegment(value: string) {\n\treturn encodeURIComponent(value);\n}\n\nfunction getSessionPersistenceRootPrefix() {\n\treturn `${getPersistencePrefix()}-${SESSION_PERSISTENCE_MARKER}-`;\n}\n\nfunction getSessionPersistenceScopePrefix(scopeId: string) {\n\treturn `${getSessionPersistenceRootPrefix()}${encodePersistenceSegment(scopeId)}-`;\n}\n\nfunction getSessionPersistenceCachePrefix(scopeId: string, cacheName: string) {\n\treturn `${getSessionPersistenceScopePrefix(scopeId)}${encodePersistenceSegment(cacheName)}`;\n}\n\nfunction isSessionPersistenceScopeValid(scope: SessionQueryPersistenceScope | null): scope is SessionQueryPersistenceScope {\n\treturn (\n\t\tBoolean(scope?.id.trim()) && Number.isFinite(Date.parse(scope?.expiresAt ?? \"\")) && Date.parse(scope?.expiresAt ?? \"\") > Date.now()\n\t);\n}\n\n/** Возвращает текущую browser-session identity, которой разрешено читать приватные persisted query. */\nexport function getSessionQueryPersistenceScope(): SessionQueryPersistenceScope | null {\n\treturn isSessionPersistenceScopeValid(activeSessionPersistenceScope) ? activeSessionPersistenceScope : null;\n}\n\nasync function removeSessionPersistenceEntries(predicate: (key: string) => boolean) {\n\tconst storage = createIndexedDbQueryStorage<PersistedQuery>();\n\tif (!storage?.entries) return;\n\n\tconst entries = await storage.entries();\n\tawait Promise.all(entries.filter(([key]) => predicate(key)).map(([key]) => storage.removeItem(key)));\n}\n\n/** Удаляет persisted query одной сессии или всех сессий текущего приложения. */\nexport async function clearSessionQueryPersistence(scopeId?: string): Promise<void> {\n\tconst prefix = scopeId === undefined ? getSessionPersistenceRootPrefix() : getSessionPersistenceScopePrefix(scopeId);\n\tawait removeSessionPersistenceEntries((key) => key.startsWith(prefix));\n}\n\n/**\n * Устанавливает единственную активную server-session identity и сразу исключает\n * чтение старого scope. Browser storage очищается асинхронно уже после смены\n * in-memory границы, поэтому параллельный query не может записаться старому пользователю.\n */\nexport async function setSessionQueryPersistenceScope(scope: SessionQueryPersistenceScope | null): Promise<void> {\n\tconst normalized = scope && isSessionPersistenceScopeValid(scope) ? { id: scope.id.trim(), expiresAt: scope.expiresAt } : null;\n\tactiveSessionPersistenceScope = normalized;\n\n\tif (!normalized) {\n\t\tawait clearSessionQueryPersistence();\n\t\treturn;\n\t}\n\n\tconst activePrefix = getSessionPersistenceScopePrefix(normalized.id);\n\tawait removeSessionPersistenceEntries((key) => key.startsWith(getSessionPersistenceRootPrefix()) && !key.startsWith(activePrefix));\n}\n\nexport function shouldPersistQuery(query: Pick<Query, \"meta\">) {\n\treturn query.meta?.persist === true && query.meta.sessionScoped !== true;\n}\n\nfunction readSessionQueryPersistencePolicy(query: Pick<Query, \"meta\">) {\n\tconst policy = query.meta?.sessionPersistence;\n\tif (query.meta?.sessionScoped !== true || !policy || typeof policy !== \"object\") return null;\n\tif (!(\"cacheName\" in policy) || !(\"maxEntries\" in policy)) return null;\n\tif (typeof policy.cacheName !== \"string\" || !/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(policy.cacheName)) return null;\n\tif (!Number.isSafeInteger(policy.maxEntries) || policy.maxEntries < 1 || policy.maxEntries > 5_000) return null;\n\treturn { cacheName: policy.cacheName, maxEntries: policy.maxEntries } as const;\n}\n\nexport function createIndexedDbQueryStorage<TStorageValue = unknown>(\n\toptions: IndexedDbQueryStorageOptions = {}\n): IndexedDbQueryStorage<TStorageValue> | undefined {\n\tconst factory = resolveIndexedDbFactory(options.indexedDB);\n\tif (!factory) return undefined;\n\n\tconst idb = factory;\n\tconst dbName = options.dbName ?? getSystemIdentifier();\n\tconst storeName = options.storeName ?? STORE_NAME;\n\tlet dbPromise: Promise<IDBDatabase> | null = null;\n\n\tfunction openDb() {\n\t\tif (dbPromise) return dbPromise;\n\n\t\tdbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n\t\t\tconst request = idb.open(dbName, 1);\n\n\t\t\trequest.onupgradeneeded = () => {\n\t\t\t\tconst db = request.result;\n\t\t\t\tif (!db.objectStoreNames.contains(storeName)) {\n\t\t\t\t\tdb.createObjectStore(storeName);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trequest.onsuccess = () => resolve(request.result);\n\t\t\trequest.onerror = () => reject(request.error);\n\t\t\trequest.onblocked = () => reject(request.error ?? new Error(\"Открытие IndexedDB заблокировано\"));\n\t\t});\n\n\t\treturn dbPromise;\n\t}\n\n\tasync function runTransaction<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => IDBRequest<T>) {\n\t\tconst db = await openDb();\n\t\tconst tx = db.transaction(storeName, mode);\n\t\tconst store = tx.objectStore(storeName);\n\t\tconst request = action(store);\n\t\treturn requestToPromise(request);\n\t}\n\n\tasync function runWrite(mode: IDBTransactionMode, action: (store: IDBObjectStore) => void) {\n\t\tconst db = await openDb();\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, mode);\n\t\t\tconst store = tx.objectStore(storeName);\n\n\t\t\ttx.oncomplete = () => resolve();\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\ttry {\n\t\t\t\taction(store);\n\t\t\t} catch (error) {\n\t\t\t\ttx.abort();\n\t\t\t\treject(error);\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function updateItem<TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) {\n\t\tconst db = await openDb();\n\t\treturn new Promise<TResult>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, \"readwrite\");\n\t\t\tconst store = tx.objectStore(storeName);\n\t\t\tconst request = store.get(key);\n\t\t\tlet result: TResult;\n\t\t\tlet hasResult = false;\n\n\t\t\ttx.oncomplete = () => {\n\t\t\t\tif (hasResult) resolve(result);\n\t\t\t\telse reject(new Error(\"IndexedDB-транзакция завершилась без результата обновления\"));\n\t\t\t};\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\t\t\trequest.onerror = () => {\n\t\t\t\ttx.abort();\n\t\t\t\treject(request.error);\n\t\t\t};\n\t\t\trequest.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst decision = update(request.result as TStorageValue | undefined);\n\t\t\t\t\tresult = decision.result;\n\t\t\t\t\thasResult = true;\n\t\t\t\t\tif (decision.action === \"set\") store.put(decision.value, key);\n\t\t\t\t\telse if (decision.action === \"remove\") store.delete(key);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttx.abort();\n\t\t\t\t\treject(error);\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\t}\n\n\treturn {\n\t\tgetItem: (key) => runTransaction<TStorageValue | undefined>(\"readonly\", (store) => store.get(key)),\n\t\tsetItem: (key, value) => runWrite(\"readwrite\", (store) => store.put(value, key)),\n\t\tremoveItem: (key) => runWrite(\"readwrite\", (store) => store.delete(key)),\n\t\tupdateItem,\n\t\tentries: async () => {\n\t\t\tconst db = await openDb();\n\n\t\t\treturn new Promise<Array<[key: string, value: TStorageValue]>>((resolve, reject) => {\n\t\t\t\tconst tx = db.transaction(storeName, \"readonly\");\n\t\t\t\tconst store = tx.objectStore(storeName);\n\t\t\t\tconst request = store.openCursor();\n\t\t\t\tconst entries: Array<[key: string, value: TStorageValue]> = [];\n\n\t\t\t\ttx.oncomplete = () => resolve(entries);\n\t\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\t\trequest.onsuccess = () => {\n\t\t\t\t\tconst cursor = request.result;\n\t\t\t\t\tif (!cursor) return;\n\n\t\t\t\t\tentries.push([String(cursor.key), cursor.value as TStorageValue]);\n\t\t\t\t\tcursor.continue();\n\t\t\t\t};\n\t\t\t\trequest.onerror = () => reject(request.error);\n\t\t\t});\n\t\t}\n\t};\n}\n\nexport function createReactQueryPersister() {\n\tconst storage = createIndexedDbQueryStorage<PersistedQuery>();\n\tif (!storage) return undefined;\n\tconst indexedDbStorage = storage;\n\n\tconst publicPersister = experimental_createQueryPersister<PersistedQuery>({\n\t\tstorage: indexedDbStorage,\n\t\tserialize: (query) => query,\n\t\tdeserialize: (query) => query,\n\t\tprefix: getPersistencePrefix(),\n\t\tbuster: getQueryPersistenceProjectAdapter().persistenceBuster ?? REACT_QUERY_PERSISTENCE_BUSTER,\n\t\tmaxAge: REACT_QUERY_PERSISTENCE_MAX_AGE,\n\t\trefetchOnRestore: true,\n\t\tfilters: { predicate: shouldPersistQuery }\n\t});\n\tconst sessionPersisters = new Map<string, ReturnType<typeof experimental_createQueryPersister<PersistedQuery>>>();\n\n\tfunction getSessionPersister(scope: SessionQueryPersistenceScope, cacheName: string, maxEntries: number) {\n\t\tconst prefix = getSessionPersistenceCachePrefix(scope.id, cacheName);\n\t\tconst identity = `${prefix}:${maxEntries}:${scope.expiresAt}`;\n\t\tconst existing = sessionPersisters.get(identity);\n\t\tif (existing) return existing;\n\n\t\tlet pruneQueue = Promise.resolve();\n\t\tconst boundedStorage: AsyncStorage<PersistedQuery> = {\n\t\t\tgetItem: (key) => {\n\t\t\t\tconst activeScope = getSessionQueryPersistenceScope();\n\t\t\t\treturn activeScope?.id === scope.id ? indexedDbStorage.getItem(key) : undefined;\n\t\t\t},\n\t\t\tremoveItem: (key) => indexedDbStorage.removeItem(key),\n\t\t\tentries: () => indexedDbStorage.entries(),\n\t\t\tsetItem: async (key, value) => {\n\t\t\t\tconst activeScope = getSessionQueryPersistenceScope();\n\t\t\t\tif (activeScope?.id !== scope.id) return;\n\n\t\t\t\tawait indexedDbStorage.setItem(key, value);\n\t\t\t\tpruneQueue = pruneQueue.then(async () => {\n\t\t\t\t\tconst entries = (await indexedDbStorage.entries()).filter(([entryKey]) => entryKey.startsWith(`${prefix}-`));\n\t\t\t\t\tentries.sort((left, right) => (right[1].state.dataUpdatedAt ?? 0) - (left[1].state.dataUpdatedAt ?? 0));\n\t\t\t\t\tawait Promise.all(entries.slice(maxEntries).map(([entryKey]) => indexedDbStorage.removeItem(entryKey)));\n\t\t\t\t});\n\t\t\t\tawait pruneQueue;\n\t\t\t}\n\t\t};\n\t\tconst persister = experimental_createQueryPersister<PersistedQuery>({\n\t\t\tstorage: boundedStorage,\n\t\t\tserialize: (query) => query,\n\t\t\tdeserialize: (query) => query,\n\t\t\tprefix,\n\t\t\tbuster: getQueryPersistenceProjectAdapter().persistenceBuster ?? REACT_QUERY_PERSISTENCE_BUSTER,\n\t\t\tmaxAge: REACT_QUERY_PERSISTENCE_MAX_AGE,\n\t\t\trefetchOnRestore: true,\n\t\t\tfilters: {\n\t\t\t\tpredicate: (query) => readSessionQueryPersistencePolicy(query)?.cacheName === cacheName\n\t\t\t}\n\t\t});\n\t\tsessionPersisters.set(identity, persister);\n\t\treturn persister;\n\t}\n\n\treturn {\n\t\t...publicPersister,\n\t\tasync persisterFn<T, TQueryKey extends QueryKey>(\n\t\t\tqueryFn: QueryFunction<T, TQueryKey>,\n\t\t\tcontext: QueryFunctionContext<TQueryKey>,\n\t\t\tquery: Query\n\t\t): Promise<T> {\n\t\t\tconst policy = readSessionQueryPersistencePolicy(query);\n\t\t\tif (!policy) return publicPersister.persisterFn(queryFn, context, query);\n\n\t\t\tconst scope = getSessionQueryPersistenceScope();\n\t\t\tif (!scope) return Promise.resolve(queryFn(context));\n\n\t\t\treturn getSessionPersister(scope, policy.cacheName, policy.maxEntries).persisterFn(queryFn, context, query);\n\t\t}\n\t};\n}\n","import { type QueryMeta } from \"@tanstack/react-query\";\n\nexport type SessionQueryPersistencePolicy = {\n\t/** Устойчивое предметно независимое имя bounded-кэша внутри текущей сессии. */\n\treadonly cacheName: string;\n\t/** Максимальное число последних успешно полученных query в этом кэше. */\n\treadonly maxEntries: number;\n};\n\n/**\n * Общие признаки query, которые управляют инфраструктурой клиентского кеша.\n *\n * `persist` разрешает долговременное хранение публичных справочных данных,\n * а `sessionScoped` помечает данные, доступность или содержимое которых зависит\n * от текущей серверной сессии. Эти признаки нельзя совмещать. Отдельный\n * `sessionPersistence` разрешает bounded storage только внутри активного scope.\n */\nexport type AppQueryMeta = {\n\treadonly persist?: boolean;\n\treadonly sessionScoped?: boolean;\n\treadonly sessionPersistence?: SessionQueryPersistencePolicy;\n} & Record<string, unknown>;\n\ndeclare module \"@tanstack/react-query\" {\n\tinterface Register {\n\t\tqueryMeta: AppQueryMeta;\n\t}\n}\n\n/** Разрешает сохранять публичную query в долговременном хранилище. */\nexport const persistedQueryMeta = { persist: true } as const satisfies QueryMeta;\n\n/** Помечает query для сброса при изменении серверной сессии. */\nexport const sessionScopedQueryMeta = { sessionScoped: true } as const satisfies QueryMeta;\n\n/**\n * Разрешает bounded persistence только внутри явно установленной server-session identity.\n * Scope и его срок жизни задаются отдельно через `setSessionQueryPersistenceScope`,\n * поэтому query contract не хранит идентификатор пользователя или сессии.\n */\nexport function createSessionQueryPersistenceMeta(policy: SessionQueryPersistencePolicy) {\n\tconst cacheName = policy.cacheName.trim();\n\tif (!/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(cacheName)) {\n\t\tthrow new Error(\"Имя session query cache должно содержать только строчные латинские буквы, цифры, _ и -\");\n\t}\n\tif (!Number.isSafeInteger(policy.maxEntries) || policy.maxEntries < 1 || policy.maxEntries > 5_000) {\n\t\tthrow new Error(\"Лимит session query cache должен быть целым числом от 1 до 5000\");\n\t}\n\n\treturn {\n\t\tsessionScoped: true,\n\t\tsessionPersistence: { cacheName, maxEntries: policy.maxEntries }\n\t} as const satisfies QueryMeta;\n}\n","import {\n\tDefaultError,\n\tMutation,\n\tMutationCache,\n\tMutationFunctionContext,\n\tQuery,\n\tQueryCache,\n\tQueryClient,\n\tQueryKey\n} from \"@tanstack/react-query\";\n\nimport { createReactQueryPersister } from \"./persistence\";\n\nexport type QueryBroadcastOptions = {\n\tonQueryError?: (error: DefaultError, query: Query<unknown, unknown, unknown>) => void;\n\tonMutationError?: (\n\t\terror: DefaultError,\n\t\tvariables: unknown,\n\t\tonMutateResult: unknown,\n\t\tmutation: Mutation<unknown, unknown, unknown>,\n\t\tcontext: MutationFunctionContext\n\t) => Promise<unknown> | unknown;\n};\n\n/**\n * Создаёт настроенный QueryClient. Singleton lifecycle обеспечивает host-приложение.\n */\nexport function createQueryClient({ onQueryError, onMutationError }: QueryBroadcastOptions) {\n\tconst queryPersister = createReactQueryPersister();\n\n\tconst queryClient = new QueryClient({\n\t\tqueryCache: new QueryCache({ onError: onQueryError }),\n\t\tmutationCache: new MutationCache({ onError: onMutationError }),\n\t\tdefaultOptions: {\n\t\t\t// TODO: Vite env\n\t\t\tqueries: {\n\t\t\t\tstaleTime: 10 * 60 * 1000,\n\t\t\t\tgcTime: 30 * 60 * 1000,\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false,\n\t\t\t\t...(queryPersister ? { persister: queryPersister.persisterFn } : {})\n\t\t\t},\n\t\t\tmutations: {\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false\n\t\t\t}\n\t\t}\n\t});\n\n\treturn queryClient;\n}\n\n/**\n * Контекст, возвращаемый из `onMutateOptimistic`.\n * Содержит снапшот кеша до оптимистичного обновления.\n * Передаётся в `onError`, `onSuccess`, `onSettled` через TanStack Query.\n */\nexport type OptimisticContext<TQueryCacheData> = {\n\tprevious: TQueryCacheData | undefined;\n};\n\n/**\n * _Оптимистичное обновление — это когда мы моментально меняем локальный кэш (и UI) до ответа сервера,\n * чтобы интерфейс был отзывчивым. Если сервер вернул ошибку — откатываем изменения.\n * Если сервер подтвердил — либо принимаем ответ сервера,\n * либо оставляем оптимистичное состояние (в зависимости от ситуации)._\n *\n * Обработчик `onMutate` для оптимистичного обновления.\n *\n * Выполняет три шага:\n * 1. Отменяет активные запросы по ключу — предотвращает гонку с сервером.\n * 2. Сохраняет снапшот текущего состояния кеша.\n * 3. Применяет оптимистичный патч через `patcher`.\n *\n * Возвращает {@link OptimisticContext} — передаётся в `onError` для отката.\n *\n * @example\n * // Обновление точечных данных в списке объектов\n * useMutation({\n * mutationFn: (data: SomeType) => { ... },\n * onMutate: async (data) =>\n * onMutateOptimistic(queryClient, someQueryKey, (old) => {\n * if (!old) return old;\n * return old.map((item) =>\n * item.id === data.id ? { ...item, visible: data.visible } : item\n * );\n * }),\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n * })\n */\nexport async function onMutateOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tpatcher: (oldCache: TQueryCacheData | undefined) => TQueryCacheData | undefined\n): Promise<OptimisticContext<TQueryCacheData>> {\n\t// 1) Отменяем запросы по ключу, чтобы не было гонки с сервером\n\tawait qc.cancelQueries({ queryKey });\n\n\t// 2) Сохраняем снапшот кеша для возможного отката\n\tconst previous = qc.getQueryData<TQueryCacheData>(queryKey);\n\n\t// 3) Применяем оптимистичный патч\n\tqc.setQueryData<TQueryCacheData>(queryKey, patcher);\n\n\treturn { previous };\n}\n\n/**\n * Обработчик `onError` для оптимистичного обновления.\n *\n * Откатывает кеш к снапшоту, сохранённому в {@link OptimisticContext}.\n * Если `context` не определён (например, `onMutate` сам выбросил ошибку) — безопасно ничего не делает.\n *\n * @example\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n */\nexport function onErrorOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tcontext: OptimisticContext<TQueryCacheData> | undefined\n): void {\n\tif (context?.previous !== undefined) {\n\t\tqc.setQueryData<TQueryCacheData>(queryKey, context.previous);\n\t}\n}\n\n/**\n * Обработчик `onSuccess` для оптимистичного обновления — применяет данные сервера в кеш.\n *\n * Используется, когда сервер вернул авторитетные данные, которые нужно слить\n * с текущим состоянием кеша (или заменить его целиком).\n *\n * Если сервер вернул `void` — эту функцию вызывать не нужно:\n * оптимистичное состояние в кеше остаётся без изменений.\n *\n * @param qc - экземпляр `QueryClient`\n * @param queryKey - ключ кеша\n * @param serverData - данные, вернувшиеся от сервера\n * @param merger - функция слияния: получает текущий кеш и ответ сервера, возвращает новое состояние\n *\n * @example\n * // Полная замена кеша ответом сервера\n * onSuccess: (data) =>\n * onSuccessOptimistic(queryClient, someQueryKey, data, (_cache, serverData) => serverData),\n *\n * @example\n * // Точечное обновление одного элемента в списке\n * onSuccess: (updatedItem) =>\n * onSuccessOptimistic(queryClient, someQueryKey, updatedItem,\n * (cache, item) => cache?.map((i) => (i.id === item.id ? item : i))\n * ),\n */\nexport function onSuccessOptimistic<TQueryCacheData, TServerData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tserverData: TServerData,\n\tmerger: (cache: TQueryCacheData | undefined, serverData: TServerData) => TQueryCacheData | undefined\n): void {\n\tqc.setQueryData<TQueryCacheData>(queryKey, (old) => merger(old, serverData));\n}\n\n/**\n * Параметры для {@link onSettledOptimistic}.\n */\nexport type OnSettledOptimisticOptions = {\n\t/**\n\t * Тип рефетча при инвалидации.\n\t * - `\"none\"` (по умолчанию) — помечает кеш устаревшим, но не запускает рефетч немедленно.\n\t * - `\"active\"` — немедленно перезапрашивает активные подписчики.\n\t * - `\"inactive\"` — перезапрашивает неактивные (фоновые) запросы.\n\t * - `\"all\"` — перезапрашивает все подходящие запросы.\n\t */\n\trefetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\n/**\n * Обработчик `onSettled` для оптимистичного обновления.\n *\n * Инвалидирует один или несколько query-ключей после завершения мутации\n * (выполняется и при успехе, и при ошибке).\n *\n * По умолчанию использует `refetchType: \"none\"` — помечает кеш устаревшим,\n * но не запускает немедленный рефетч. Это безопасный вариант при оптимистике:\n * данные в кеше уже актуальны (подтверждены или откатаны), а рефетч произойдёт\n * при следующем монтировании или явном запросе.\n *\n * Broadcast в другие вкладки — ответственность вызывающего кода через `broadcastCacheEvent`.\n *\n * @example\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n *\n * @example\n * // С принудительным рефетчем активных подписчиков\n * onSettled: () =>\n * onSettledOptimistic(queryClient, [queryKeyA, queryKeyB], { refetchType: \"active\" }),\n */\nexport async function onSettledOptimistic(\n\tqc: QueryClient,\n\tqueryKeys: QueryKey | QueryKey[],\n\toptions?: OnSettledOptimisticOptions\n): Promise<void> {\n\tconst refetchType = options?.refetchType ?? \"none\";\n\t// Нормализуем одиночный ключ к массиву для единообразной обработки\n\tconst keys: QueryKey[] = isNestedQueryKey(queryKeys) ? queryKeys : [queryKeys];\n\n\tawait Promise.all(keys.map((queryKey) => qc.invalidateQueries({ queryKey, refetchType })));\n}\n\n/**\n * Определяет, является ли значение массивом query-ключей (QueryKey[]),\n * а не одиночным QueryKey.\n *\n * QueryKey в TanStack Query — это `string | readonly unknown[]`.\n * Чтобы отличить `QueryKey[]` от `QueryKey` типа `readonly unknown[]`,\n * проверяем, содержит ли массив хотя бы один вложенный массив или строку на верхнем уровне.\n */\nfunction isNestedQueryKey(value: QueryKey | QueryKey[]): value is QueryKey[] {\n\tif (!Array.isArray(value)) return false;\n\t// Если первый элемент сам является массивом — это QueryKey[]\n\treturn Array.isArray(value[0]);\n}\n","import { type Query, type QueryClient } from \"@tanstack/react-query\";\n\nconst SESSION_SCOPED_QUERY_CACHE_RESET_EVENT = \"session-scoped-query-cache-reset\";\n\ntype SessionScopedQueryCacheResetEvent = {\n\treadonly type: typeof SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n};\n\nexport type ResetSessionScopedQueriesOptions = {\n\t/**\n\t * Передавать ли команду сброса другим вкладкам. При обработке входящего\n\t * сообщения значение отключается, чтобы вкладки не создавали цикл событий.\n\t */\n\treadonly broadcast?: boolean;\n};\n\nexport type InstallSessionScopedQueryResetOptions = {\n\t/** Уникальное для приложения имя служебного BroadcastChannel. */\n\treadonly channelName: string;\n};\n\nexport type InstalledSessionScopedQueryReset = {\n\t/** Освобождает канал и прекращает межвкладочную синхронизацию. */\n\treadonly cleanup: () => void;\n};\n\nlet broadcastSessionScopedQueryReset: (() => void) | undefined;\n\n/** Проверяет, относится ли query к данным текущей серверной сессии. */\nexport function isSessionScopedQuery(query: Pick<Query, \"meta\">): boolean {\n\treturn query.meta?.sessionScoped === true;\n}\n\n/**\n * Сбрасывает все session-scoped query после входа, выхода, обновления сессии\n * или потери авторизации.\n *\n * Сначала отменяются незавершённые запросы старой сессии, затем TanStack Query\n * очищает их состояние и повторно запрашивает только активные query. Обычные\n * публичные данные при этом остаются в кеше. По умолчанию отдельная команда\n * сброса отправляется другим вкладкам без передачи содержимого кеша или токенов.\n */\nexport async function resetSessionScopedQueries(queryClient: QueryClient, options: ResetSessionScopedQueriesOptions = {}): Promise<void> {\n\tconst filters = { predicate: isSessionScopedQuery } as const;\n\n\tawait queryClient.cancelQueries(filters);\n\n\tconst matchedQueries = queryClient.getQueryCache().findAll(filters);\n\tfor (const query of matchedQueries) {\n\t\tif (query.getObserversCount() === 0) {\n\t\t\tqueryClient.getQueryCache().remove(query);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Hydrated query хранит SSR-данные как initial state, поэтому одного\n\t\t// query.reset() недостаточно: старая сессия могла бы вернуться в UI.\n\t\tquery.reset();\n\t\tquery.setState({ data: undefined, dataUpdatedAt: 0 });\n\t}\n\n\tawait queryClient.refetchQueries(\n\t\t{\n\t\t\tpredicate: (query) => isSessionScopedQuery(query) && query.getObserversCount() > 0,\n\t\t\ttype: \"active\"\n\t\t},\n\t\t{ cancelRefetch: true }\n\t);\n\n\tif (options.broadcast !== false) {\n\t\tbroadcastSessionScopedQueryReset?.();\n\t}\n}\n\n/**\n * Подключает безопасную межвкладочную синхронизацию смены сессии.\n *\n * Канал передаёт только команду сброса. В отличие от полной синхронизации\n * Query cache, он не публикует данные query и потому подходит для auth-зависимых\n * ответов. На сервере и в браузерах без BroadcastChannel возвращается no-op.\n */\nexport function installSessionScopedQueryReset(\n\tqueryClient: QueryClient,\n\toptions: InstallSessionScopedQueryResetOptions\n): InstalledSessionScopedQueryReset {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { cleanup: () => undefined };\n\t}\n\n\tconst channel = new BroadcastChannel(options.channelName);\n\tconst broadcast = () => {\n\t\tconst event: SessionScopedQueryCacheResetEvent = {\n\t\t\ttype: SESSION_SCOPED_QUERY_CACHE_RESET_EVENT\n\t\t};\n\t\tchannel.postMessage(event);\n\t};\n\n\tbroadcastSessionScopedQueryReset = broadcast;\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tif (!isSessionScopedQueryCacheResetEvent(event.data)) return;\n\t\tvoid resetSessionScopedQueries(queryClient, { broadcast: false });\n\t};\n\n\treturn {\n\t\tcleanup: () => {\n\t\t\tchannel.onmessage = null;\n\t\t\tchannel.close();\n\t\t\tif (broadcastSessionScopedQueryReset === broadcast) {\n\t\t\t\tbroadcastSessionScopedQueryReset = undefined;\n\t\t\t}\n\t\t}\n\t};\n}\n\n/** Защищает обработчик канала от посторонних или устаревших сообщений. */\nfunction isSessionScopedQueryCacheResetEvent(value: unknown): value is SessionScopedQueryCacheResetEvent {\n\treturn typeof value === \"object\" && value !== null && \"type\" in value && value.type === SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { type AsyncStorage, type PersistedQuery } from "@tanstack/query-persist-client-core";
2
- import { type Query } from "@tanstack/react-query";
2
+ import { type Query, type QueryFunction, type QueryFunctionContext, type QueryKey } from "@tanstack/react-query";
3
3
  export declare const REACT_QUERY_PERSISTENCE_BUSTER: string;
4
4
  export declare const REACT_QUERY_PERSISTENCE_MAX_AGE: number;
5
5
  type IndexedDbQueryStorageOptions = {
@@ -7,6 +7,12 @@ type IndexedDbQueryStorageOptions = {
7
7
  storeName?: string;
8
8
  indexedDB?: IDBFactory;
9
9
  };
10
+ export type SessionQueryPersistenceScope = {
11
+ /** Публичная identity серверной сессии; cookie secret или access token здесь недопустимы. */
12
+ readonly id: string;
13
+ /** Абсолютная граница, после которой snapshots этой сессии нельзя читать или записывать. */
14
+ readonly expiresAt: string;
15
+ };
10
16
  /**
11
17
  * Описывает итог синхронного преобразования записи внутри одной IndexedDB-транзакции.
12
18
  * Отдельный result позволяет вернуть вызывающему коду вычисленный снимок только после commit.
@@ -23,6 +29,8 @@ export type IndexedDbAtomicUpdateDecision<TStorageValue, TResult> = {
23
29
  readonly result: TResult;
24
30
  };
25
31
  export type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> & {
32
+ /** Перечисляет записи object store для garbage collection и bounded persistence. */
33
+ readonly entries: () => Promise<Array<[key: string, value: TStorageValue]>>;
26
34
  /**
27
35
  * Читает и изменяет одну запись в общей readwrite-транзакции, поэтому параллельные
28
36
  * вкладки не могут потерять уже зафиксированное изменение между get и set.
@@ -31,12 +39,22 @@ export type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> &
31
39
  */
32
40
  readonly updateItem: <TResult>(key: string, update: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>) => Promise<TResult>;
33
41
  };
42
+ /** Возвращает текущую browser-session identity, которой разрешено читать приватные persisted query. */
43
+ export declare function getSessionQueryPersistenceScope(): SessionQueryPersistenceScope | null;
44
+ /** Удаляет persisted query одной сессии или всех сессий текущего приложения. */
45
+ export declare function clearSessionQueryPersistence(scopeId?: string): Promise<void>;
46
+ /**
47
+ * Устанавливает единственную активную server-session identity и сразу исключает
48
+ * чтение старого scope. Browser storage очищается асинхронно уже после смены
49
+ * in-memory границы, поэтому параллельный query не может записаться старому пользователю.
50
+ */
51
+ export declare function setSessionQueryPersistenceScope(scope: SessionQueryPersistenceScope | null): Promise<void>;
34
52
  export declare function shouldPersistQuery(query: Pick<Query, "meta">): boolean;
35
53
  export declare function createIndexedDbQueryStorage<TStorageValue = unknown>(options?: IndexedDbQueryStorageOptions): IndexedDbQueryStorage<TStorageValue> | undefined;
36
54
  export declare function createReactQueryPersister(): {
37
- persisterFn: <T, TQueryKey extends import("@tanstack/query-core").QueryKey>(queryFn: (context: import("@tanstack/query-core").QueryFunctionContext<TQueryKey>) => T | Promise<T>, ctx: import("@tanstack/query-core").QueryFunctionContext<TQueryKey>, query: Query) => Promise<T>;
55
+ persisterFn<T, TQueryKey extends QueryKey>(queryFn: QueryFunction<T, TQueryKey>, context: QueryFunctionContext<TQueryKey>, query: Query): Promise<T>;
38
56
  persistQuery: (query: Query) => Promise<void>;
39
- persistQueryByKey: (queryKey: import("@tanstack/query-core").QueryKey, queryClient: import("@tanstack/query-core").QueryClient) => Promise<void>;
57
+ persistQueryByKey: (queryKey: QueryKey, queryClient: import("@tanstack/query-core").QueryClient) => Promise<void>;
40
58
  retrieveQuery: <T>(queryHash: string, afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void) => Promise<T | undefined>;
41
59
  persisterGc: () => Promise<void>;
42
60
  restoreQueries: (queryClient: import("@tanstack/query-core").QueryClient, filters?: Pick<import("@tanstack/query-core").QueryFilters, "queryKey" | "exact">) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/query-client/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChI,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAMnD,eAAO,MAAM,8BAA8B,QAAqC,CAAC;AACjF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,KAAK,4BAA4B,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,6BAA6B,CAAC,aAAa,EAAE,OAAO,IAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qBAAqB,CAAC,aAAa,IAAI,YAAY,CAAC,aAAa,CAAC,GAAG;IAChF;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,6BAA6B,CAAC,aAAa,EAAE,OAAO,CAAC,KAC/F,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAgCF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,WAE5D;AAED,wBAAgB,2BAA2B,CAAC,aAAa,GAAG,OAAO,EAClE,OAAO,GAAE,4BAAiC,GACxC,qBAAqB,CAAC,aAAa,CAAC,GAAG,SAAS,CA4HlD;AAED,wBAAgB,yBAAyB;;;;+DA1JrC,CAAC;;qFAOI,CAAC;2BAGuD,CAAC;cA8JjE"}
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/query-client/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChI,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAOjH,eAAO,MAAM,8BAA8B,QAAqC,CAAC;AACjF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,KAAK,4BAA4B,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B,CAAC;AAIF;;;GAGG;AACH,MAAM,MAAM,6BAA6B,CAAC,aAAa,EAAE,OAAO,IAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qBAAqB,CAAC,aAAa,IAAI,YAAY,CAAC,aAAa,CAAC,GAAG;IAChF,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,6BAA6B,CAAC,aAAa,EAAE,OAAO,CAAC,KAC/F,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAsDF,uGAAuG;AACvG,wBAAgB,+BAA+B,IAAI,4BAA4B,GAAG,IAAI,CAErF;AAUD,gFAAgF;AAChF,wBAAsB,4BAA4B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF;AAED;;;;GAIG;AACH,wBAAsB,+BAA+B,CAAC,KAAK,EAAE,4BAA4B,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/G;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,WAE5D;AAWD,wBAAgB,2BAA2B,CAAC,aAAa,GAAG,OAAO,EAClE,OAAO,GAAE,4BAAiC,GACxC,qBAAqB,CAAC,aAAa,CAAC,GAAG,SAAS,CA4HlD;AAED,wBAAgB,yBAAyB;gBA8DrB,CAAC,EAAE,SAAS,SAAS,QAAQ,WACrC,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC,WAC3B,oBAAoB,CAAC,SAAS,CAAC,SACjC,KAAK,GACV,OAAO,CAAC,CAAC,CAAC;;;+DAlT2B,CAAC;;qFAGzC,CAAF;2BAGoC,CAAC;cAsTpC"}
@@ -1,14 +1,21 @@
1
+ export type SessionQueryPersistencePolicy = {
2
+ /** Устойчивое предметно независимое имя bounded-кэша внутри текущей сессии. */
3
+ readonly cacheName: string;
4
+ /** Максимальное число последних успешно полученных query в этом кэше. */
5
+ readonly maxEntries: number;
6
+ };
1
7
  /**
2
8
  * Общие признаки query, которые управляют инфраструктурой клиентского кеша.
3
9
  *
4
10
  * `persist` разрешает долговременное хранение публичных справочных данных,
5
11
  * а `sessionScoped` помечает данные, доступность или содержимое которых зависит
6
- * от текущей серверной сессии. Эти признаки нельзя совмещать: session-scoped
7
- * query никогда не должна переживать смену пользователя или роли.
12
+ * от текущей серверной сессии. Эти признаки нельзя совмещать. Отдельный
13
+ * `sessionPersistence` разрешает bounded storage только внутри активного scope.
8
14
  */
9
15
  export type AppQueryMeta = {
10
16
  readonly persist?: boolean;
11
17
  readonly sessionScoped?: boolean;
18
+ readonly sessionPersistence?: SessionQueryPersistencePolicy;
12
19
  } & Record<string, unknown>;
13
20
  declare module "@tanstack/react-query" {
14
21
  interface Register {
@@ -23,4 +30,16 @@ export declare const persistedQueryMeta: {
23
30
  export declare const sessionScopedQueryMeta: {
24
31
  readonly sessionScoped: true;
25
32
  };
33
+ /**
34
+ * Разрешает bounded persistence только внутри явно установленной server-session identity.
35
+ * Scope и его срок жизни задаются отдельно через `setSessionQueryPersistenceScope`,
36
+ * поэтому query contract не хранит идентификатор пользователя или сессии.
37
+ */
38
+ export declare function createSessionQueryPersistenceMeta(policy: SessionQueryPersistencePolicy): {
39
+ readonly sessionScoped: true;
40
+ readonly sessionPersistence: {
41
+ readonly cacheName: string;
42
+ readonly maxEntries: number;
43
+ };
44
+ };
26
45
  //# sourceMappingURL=queryMeta.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryMeta.d.ts","sourceRoot":"","sources":["../../src/query-client/queryMeta.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CACjC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,OAAO,QAAQ,uBAAuB,CAAC;IACtC,UAAU,QAAQ;QACjB,SAAS,EAAE,YAAY,CAAC;KACxB;CACD;AAED,sEAAsE;AACtE,eAAO,MAAM,kBAAkB;;CAAiD,CAAC;AAEjF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB;;CAAuD,CAAC"}
1
+ {"version":3,"file":"queryMeta.d.ts","sourceRoot":"","sources":["../../src/query-client/queryMeta.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,6BAA6B,GAAG;IAC3C,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;CAC5D,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,OAAO,QAAQ,uBAAuB,CAAC;IACtC,UAAU,QAAQ;QACjB,SAAS,EAAE,YAAY,CAAC;KACxB;CACD;AAED,sEAAsE;AACtE,eAAO,MAAM,kBAAkB;;CAAiD,CAAC;AAEjF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB;;CAAuD,CAAC;AAE3F;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,6BAA6B;;;;;;EAatF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "1.8.3",
3
+ "version": "1.9.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -13,6 +13,9 @@ import { Meta } from "@storybook/addon-docs/blocks";
13
13
 
14
14
  Используйте точный subpath. Внутренний файл `serviceWorker` не является самостоятельным публичным export, даже если отдельные функции используются самим hook.
15
15
 
16
+ `useBrowserOnlineStatus()` возвращает browser hint `navigator.onLine` через `useSyncExternalStore` и подписывается на события
17
+ `online`/`offline`. Это сигнал для UX и запуска revalidation, а не доказательство доступности конкретного backend.
18
+
16
19
  ## Часть 1. Обновление Service Worker
17
20
 
18
21
  ### Задача hook
@@ -378,6 +381,7 @@ Waiting worker остаётся установленным. Следующая
378
381
  | Экспорт | Публичный path | Назначение |
379
382
  | ----------------------------------- | -------------- | -------------------------------------------------- |
380
383
  | `useServiceWorkerUpdate()` | `/pwa` | Регистрация, проверка и UI-состояние обновления SW |
384
+ | `useBrowserOnlineStatus()` | `/pwa` | Реактивный browser online/offline hint |
381
385
  | `parseSwCachePolicy()` | оба | Разобрать текстовую политику |
382
386
  | `buildSwCachePolicyValue()` | оба | Собрать каноническую строку из draft |
383
387
  | `normalizeSwCacheName()` | оба | Подготовить безопасную часть cache name |
@@ -274,12 +274,14 @@ Stale не означает «удалена». Data может продолжа
274
274
  | Нужны данные только пока открыта вкладка? | Обычная query без `persistedQueryMeta`. |
275
275
  | Данные публичные, тяжёлые и могут пережить reload? | Рассмотреть `persistedQueryMeta`. |
276
276
  | Данные зависят от пользователя, роли или server session? | `sessionScopedQueryMeta`. |
277
+ | Session snapshot явно разрешено читать offline? | `createSessionQueryPersistenceMeta` + active scope. |
277
278
  | Нужно передать обновление другой открытой вкладке? | Broadcast installation и подходящее cache event. |
278
279
  | UI должен измениться до ответа mutation? | Optimistic update с обязательным rollback policy. |
279
280
  | Server response авторитетен и уже содержит новое состояние? | `setQueryData`/`onSuccessOptimistic`. |
280
281
  | После mutation нужен свежий server snapshot? | Scoped `invalidateQueries` с выбранным `refetchType`. |
281
282
 
282
- `persistedQueryMeta` и `sessionScopedQueryMeta` взаимоисключающие. Session data не должна переживать смену пользователя.
283
+ `persistedQueryMeta` и `sessionScopedQueryMeta` взаимоисключающие. Для узкого offline allowlist session data используется
284
+ отдельный `sessionPersistence` contract: он требует активную server-session identity и абсолютный `expiresAt`.
283
285
 
284
286
  ## Быстрый старт
285
287
 
@@ -489,7 +491,8 @@ queryOptions({
489
491
  });
490
492
  ```
491
493
 
492
- `Record<string, unknown>` разрешает consumer-проекту добавлять собственную metadata. Infrastructure этого модуля читает только `persist` и `sessionScoped`.
494
+ `Record<string, unknown>` разрешает consumer-проекту добавлять собственную metadata. Infrastructure этого модуля читает
495
+ `persist`, `sessionScoped` и явный `sessionPersistence` contract.
493
496
 
494
497
  ### `persistedQueryMeta`
495
498
 
@@ -533,6 +536,36 @@ queryOptions({
533
536
 
534
537
  При смене сессии host обязан вызвать `resetSessionScopedQueries`.
535
538
 
539
+ ### Session-scoped offline persistence
540
+
541
+ Только для явно согласованного offline allowlist создайте bounded metadata:
542
+
543
+ ```ts
544
+ import {
545
+ createSessionQueryPersistenceMeta,
546
+ setSessionQueryPersistenceScope
547
+ } from "@ryuzaki13/react-foundation-lib/query-client";
548
+
549
+ await setSessionQueryPersistenceScope({
550
+ id: session.id,
551
+ expiresAt: session.expiresAt
552
+ });
553
+
554
+ queryOptions({
555
+ queryKey: ["opened-week", weekStart],
556
+ queryFn: fetchWeek,
557
+ meta: createSessionQueryPersistenceMeta({
558
+ cacheName: "opened-weeks",
559
+ maxEntries: 24
560
+ }),
561
+ networkMode: "offlineFirst"
562
+ });
563
+ ```
564
+
565
+ Scope устанавливается отдельно от query contract, не содержит cookie secret/access token и меняет in-memory границу до очистки IndexedDB.
566
+ При `null`, истёкшем `expiresAt` или смене `id` старые session snapshots удаляются. `networkMode: "offlineFirst"` нужен, чтобы
567
+ TanStack вызвал persister до network pause. Cached data остаётся неавторитетным: write после reconnect обязан повторно получить серверный state.
568
+
536
569
  ### Meta нельзя совмещать
537
570
 
538
571
  Неверно:
@@ -550,13 +583,14 @@ meta: {
550
583
 
551
584
  - публичный/безопасный долгоживущий справочник → `persistedQueryMeta`;
552
585
  - auth/session dependent data → `sessionScopedQueryMeta`;
586
+ - разрешённый bounded offline snapshot → `createSessionQueryPersistenceMeta`;
553
587
  - обычные transient data → meta не нужна.
554
588
 
555
589
  ## Настройка project adapter
556
590
 
557
591
  ### Зачем нужен adapter
558
592
 
559
- Foundation-пакет не знает имя SAP system, client, tenant или окружение конкретного приложения. `QueryPersistenceProjectAdapter` позволяет host передать устойчивый namespace и собственный buster:
593
+ Foundation-пакет не знает имя SAP system, client, tenant или окружение конкретного приложения. `QueryPersistenceProjectAdapter` позволяет host передать устойчивый namespace и собственный buster. Текущая session identity задаётся отдельным runtime API и не входит в adapter:
560
594
 
561
595
  ```ts
562
596
  type QueryPersistenceProjectAdapter = {
@@ -778,7 +812,8 @@ Storage не скрывает ошибки. Promise отклоняется, ес
778
812
  const persister = createReactQueryPersister();
779
813
  ```
780
814
 
781
- Возвращает результат `experimental_createQueryPersister` либо `undefined`, если IndexedDB недоступен.
815
+ Возвращает routing-обёртку над `experimental_createQueryPersister` либо `undefined`, если IndexedDB недоступен. Обычные public query
816
+ идут в прежний persister, а query с `sessionPersistence` — в отдельный cache активной сессии с независимым `maxEntries`.
782
817
 
783
818
  Настройки foundation wrapper:
784
819
 
@@ -2356,9 +2391,11 @@ Profile tests подтверждают:
2356
2391
 
2357
2392
  | Symbol | Вид | Назначение |
2358
2393
  | ------------------------ | ----- | ----------------------------------------------- |
2359
- | `AppQueryMeta` | type | `persist`, `sessionScoped` и consumer metadata. |
2360
- | `persistedQueryMeta` | const | Opt-in public persistence marker. |
2361
- | `sessionScopedQueryMeta` | const | Session reset marker. |
2394
+ | `AppQueryMeta` | type | Public/session persistence и consumer metadata. |
2395
+ | `SessionQueryPersistencePolicy` | type | Имя и bounded limit session cache. |
2396
+ | `persistedQueryMeta` | const | Opt-in public persistence marker. |
2397
+ | `sessionScopedQueryMeta` | const | Session reset marker без browser persistence. |
2398
+ | `createSessionQueryPersistenceMeta` | function | Явный opt-in bounded persistence текущей server session. |
2362
2399
 
2363
2400
  ### Persistence project adapter
2364
2401
 
@@ -2377,6 +2414,10 @@ Profile tests подтверждают:
2377
2414
  | `shouldPersistQuery` | function | Opt-in predicate с session safety. |
2378
2415
  | `createIndexedDbQueryStorage` | function | AsyncStorage поверх IndexedDB. |
2379
2416
  | `createReactQueryPersister` | function | Настроенный per-query TanStack persister. |
2417
+ | `SessionQueryPersistenceScope` | type | Public session id и абсолютный expiresAt. |
2418
+ | `getSessionQueryPersistenceScope` | function | Читает валидный активный browser scope. |
2419
+ | `setSessionQueryPersistenceScope` | function | Меняет scope и очищает чужие snapshots. |
2420
+ | `clearSessionQueryPersistence` | function | Удаляет один или все session snapshots. |
2380
2421
 
2381
2422
  Options type `createIndexedDbQueryStorage` не экспортируется отдельным symbol; используйте структурный object или `Parameters<typeof createIndexedDbQueryStorage>[0]`.
2382
2423