@ryuzaki13/react-foundation-lib 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/array/index.js +1 -1
- package/dist/chunks/{array-BBSjoTRy.js → array-D-_yMMUK.js} +2 -2
- package/dist/chunks/{array-BBSjoTRy.js.map → array-D-_yMMUK.js.map} +1 -1
- package/dist/chunks/{formatters--mhLir82.js → formatters-CVj-3Vn0.js} +280 -269
- package/dist/chunks/formatters-CVj-3Vn0.js.map +1 -0
- package/dist/chunks/{odata-service-DnPlNGpU.js → odata-service-B5w4YBao.js} +13 -17
- package/dist/chunks/{odata-service-DnPlNGpU.js.map → odata-service-B5w4YBao.js.map} +1 -1
- package/dist/chunks/{validate-nWY_Mla5.js → validate-BzjCBUqb.js} +14 -18
- package/dist/chunks/{validate-nWY_Mla5.js.map → validate-BzjCBUqb.js.map} +1 -1
- package/dist/error-report/index.js +13 -13
- package/dist/excel/index.js +3 -3
- package/dist/excel/index.js.map +1 -1
- package/dist/formatters/index.js +6 -6
- package/dist/formatters/strings/formatRussianPlural.d.ts +25 -0
- package/dist/formatters/strings/formatRussianPlural.d.ts.map +1 -0
- package/dist/formatters/strings/index.d.ts +1 -0
- package/dist/formatters/strings/index.d.ts.map +1 -1
- package/dist/hooks/index.js +82 -81
- package/dist/hooks/index.js.map +1 -1
- package/dist/media/index.js +2 -1
- package/dist/media/index.js.map +1 -1
- package/dist/number-scale/index.js +1 -1
- package/dist/number-scale/index.js.map +1 -1
- package/dist/odata-service/index.js +1 -1
- package/dist/pwa/index.js +1 -1
- package/dist/pwa/index.js.map +1 -1
- package/dist/pwa/sw-cache-policy.js +3 -3
- package/dist/pwa/sw-cache-policy.js.map +1 -1
- package/dist/query-client/index.js +5 -5
- package/dist/query-client/index.js.map +1 -1
- package/dist/range-output/index.js +1 -1
- package/dist/range-output/index.js.map +1 -1
- package/dist/table/index.js +3 -3
- package/dist/table/index.js.map +1 -1
- package/dist/tree-table/index.js +1 -1
- package/package.json +33 -33
- package/src/docs/formatters.mdx +17 -0
- package/dist/chunks/formatters--mhLir82.js.map +0 -1
package/dist/media/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/getCurrentFontSize.ts","../../src/media/pxToEm.ts","../../src/media/responsive.ts","../../src/media/useMediaQuery.ts","../../src/media/useMatchMedia.ts"],"sourcesContent":["export const BREAKPOINTS_EM = {\n\tmobileMax: 767.98 / 16,\n\ttabletMin: 768 / 16,\n\ttabletMax: 1151.98 / 16,\n\tlaptopMin: 1152 / 16\n} as const;\n\nexport type Breakpoint = \"mobile\" | \"tablet\" | \"laptop\";\nexport type MediaMatches = Record<Breakpoint, boolean>;\n","const DEFAULT_FONT_SIZE = 16;\nconst DEFAULT_LINE_HEIGHT = 1.2;\n\nexport function getCurrentFontSize(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst rootFontSize = window.getComputedStyle(target).fontSize;\n\tconst fontSize = Number.parseFloat(rootFontSize);\n\n\tif (!Number.isFinite(fontSize) || fontSize <= 0) {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\treturn fontSize;\n}\n\nexport function getCurrentLineHeight(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst computedStyles = window.getComputedStyle(target);\n\tconst rootLineHeight = computedStyles.lineHeight;\n\tconst lineHeight = Number.parseFloat(rootLineHeight);\n\n\tif (!Number.isFinite(lineHeight) || lineHeight <= 0) {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\treturn lineHeight / DEFAULT_FONT_SIZE;\n}\n\nexport function getControlHeight() {\n\tconst fontSize = getCurrentFontSize();\n\tconst lineHeight = getCurrentLineHeight();\n\n\t// Правильно расчета --control-height\n\treturn fontSize + lineHeight * 10;\n}\n","import { getCurrentFontSize } from \"./getCurrentFontSize\";\n\nexport function pxToEm(px: number, baseEm?: number): string {\n\tconst emSize = baseEm ?? getCurrentFontSize(); // динамическая привязка\n\treturn (px / emSize).toString();\n}\n","import { Breakpoint } from \"./breakpoints\";\n\nexport type ResponsiveMap<T> = {\n\tmobile?: T;\n\ttablet?: T;\n\tlaptop?: T;\n};\nexport type ResponsiveValue<T> = T | ResponsiveMap<T>;\nexport type UnwrapResponsive<T> = T extends ResponsiveValue<infer U> ? U : T;\n\nexport function isResponsiveMap<T>(v: ResponsiveValue<T>): v is ResponsiveMap<T> {\n\tif (typeof v !== \"object\" || v === null) return false;\n\n\t// ключевой момент: отличаем “responsive-map” от произвольного объекта T\n\treturn \"mobile\" in v || \"tablet\" in v || \"laptop\" in v || \"desktop\" in v;\n}\n\n/**\n * Превращает объект пропсов, где некоторые поля ResponsiveValue<...>,\n * в объект, где эти поля уже \"плоские\" (U).\n */\nexport type ResolvedProps<T extends object> = {\n\t[K in keyof T]: UnwrapResponsive<T[K]>;\n};\n\n/**\n * Резолвит responsive значение под текущий брейкпоинт.\n * Приоритеты:\n * - exact match (mobile/tablet/laptop)\n * - fallback на \"первое определённое\" в предсказуемом порядке\n */\nexport function resolveResponsiveValue<T>(value: ResponsiveValue<T> | undefined, bp: Breakpoint): T | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isResponsiveMap(value)) {\n\t\t// здесь value точно T (в т.ч. может быть объектом — и это ок)\n\t\treturn value;\n\t}\n\n\tif (bp === \"mobile\") return value.mobile ?? value.tablet ?? value.laptop;\n\tif (bp === \"tablet\") return value.tablet ?? value.mobile ?? value.laptop;\n\n\treturn value.laptop ?? value.tablet ?? value.mobile;\n}\n\n/**\n * Типобезопасно резолвит все поля объекта по списку ключей, которые responsive.\n * (TS-магия: на выходе будет ResolvedProps<T>.)\n */\nexport function resolveProps<T extends object>(props: T, bp: Breakpoint, responsiveKeys: readonly (keyof T)[]): ResolvedProps<T> {\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst out: any = { ...props };\n\n\tfor (const key of responsiveKeys) {\n\t\tout[key] = resolveResponsiveValue(out[key], bp);\n\t}\n\n\treturn out as ResolvedProps<T>;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\n\ntype UseMediaQueryOptions = {\n\treadonly ssrFallback?: boolean;\n};\n\nconst noop = () => {};\n\nfunction readMediaQuery(query: string, fallback: boolean): boolean {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return fallback;\n\n\treturn window.matchMedia(query).matches;\n}\n\nfunction subscribeMediaQuery(query: string, listener: () => void): () => void {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return noop;\n\n\tconst mql = window.matchMedia(query);\n\n\tif (typeof mql.addEventListener === \"function\") {\n\t\tmql.addEventListener(\"change\", listener);\n\t\treturn () => mql.removeEventListener(\"change\", listener);\n\t}\n\n\tmql.addListener(listener);\n\treturn () => mql.removeListener(listener);\n}\n\n/**\n * SSR-безопасная подписка на произвольный media query.\n * Используется для клиентских предпочтений, где значение должно совпадать с SSR fallback на гидрации.\n */\nexport function useMediaQuery(query: string, { ssrFallback = false }: UseMediaQueryOptions = {}) {\n\tconst subscribe = useCallback((listener: () => void) => subscribeMediaQuery(query, listener), [query]);\n\tconst getSnapshot = useCallback(() => readMediaQuery(query, ssrFallback), [query, ssrFallback]);\n\tconst getServerSnapshot = useCallback(() => ssrFallback, [ssrFallback]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import { useSyncExternalStore } from \"react\";\n\nimport { BREAKPOINTS_EM, type Breakpoint, type MediaMatches } from \"./breakpoints\";\nimport { getCurrentFontSize } from \"./getCurrentFontSize\";\n\n/**\n * Публичный снимок состояния, который возвращает хук/store.\n * Содержит сырые совпадения media query и вычисленный активный брейкпоинт.\n */\ntype MatchMediaSnapshot = {\n\tmatches: MediaMatches;\n\tactiveBreakpoint: Breakpoint;\n};\n\nfunction subscribeMql(mql: MediaQueryList, cb: () => void) {\n\tif (\"addEventListener\" in mql) {\n\t\tmql.addEventListener(\"change\", cb);\n\t\treturn () => mql.removeEventListener(\"change\", cb);\n\t}\n\t// Safari legacy\n\t// @ts-expect-error legacy api\n\tmql.addListener(cb);\n\t// @ts-expect-error legacy api\n\treturn () => mql.removeListener(cb);\n}\n\nfunction buildQueriesPx(emSize: number) {\n\tconst toPx = (em: number) => em * emSize;\n\n\tconst mobileMax = toPx(BREAKPOINTS_EM.mobileMax);\n\tconst tabletMin = toPx(BREAKPOINTS_EM.tabletMin);\n\tconst tabletMax = toPx(BREAKPOINTS_EM.tabletMax);\n\tconst laptopMin = toPx(BREAKPOINTS_EM.laptopMin);\n\n\treturn {\n\t\tmobile: `(width <= ${mobileMax}px)`,\n\t\ttablet: `(width >= ${tabletMin}px) and (width <= ${tabletMax}px)`,\n\t\tlaptop: `(width >= ${laptopMin}px)`\n\t} satisfies Record<Breakpoint, string>;\n}\n\nconst DEFAULT_MATCHES: MediaMatches = {\n\tmobile: false,\n\ttablet: false,\n\tlaptop: false\n};\n\nconst DEFAULT_SNAPSHOT: MatchMediaSnapshot = {\n\tmatches: DEFAULT_MATCHES,\n\tactiveBreakpoint: \"laptop\"\n};\n\n/**\n * Игнорируем микроколебания `em`, чтобы не перестраивать media query без необходимости.\n */\nconst EM_CHANGE_THRESHOLD = 0.25;\n/**\n * Склеиваем частые события observer-ов в одно перестроение.\n */\nconst EM_REBUILD_DEBOUNCE_MS = 80;\n\nlet snapshot: MatchMediaSnapshot = DEFAULT_SNAPSHOT;\nlet currentMql: Record<Breakpoint, MediaQueryList> | null = null;\nlet mqlUnsubs: Array<() => void> = [];\nlet emSizeRef = 0;\nlet initialized = false;\nlet mutationObserver: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet emProbeElement: HTMLDivElement | null = null;\nlet rebuildTimer: ReturnType<typeof setTimeout> | null = null;\n\nconst listeners = new Set<() => void>();\n\n/**\n * Выбирает один активный брейкпоинт по приоритету: mobile -> tablet -> laptop.\n */\nfunction getActiveBreakpoint(matches: MediaMatches): Breakpoint {\n\tif (matches.mobile) return \"mobile\";\n\tif (matches.tablet) return \"tablet\";\n\treturn \"laptop\";\n}\n\n/**\n * Поверхностное сравнение media-состояния, чтобы пропускать пустые уведомления.\n */\nfunction matchesEqual(a: MediaMatches, b: MediaMatches) {\n\treturn a.mobile === b.mobile && a.tablet === b.tablet && a.laptop === b.laptop;\n}\n\n/**\n * Обновляет глобальный snapshot и уведомляет подписчиков только при реальных изменениях.\n */\nfunction emitIfChanged(nextMatches: MediaMatches) {\n\tif (matchesEqual(snapshot.matches, nextMatches)) return;\n\tsnapshot = {\n\t\tmatches: nextMatches,\n\t\tactiveBreakpoint: getActiveBreakpoint(nextMatches)\n\t};\n\tlisteners.forEach((listener) => listener());\n}\n\n/**\n * Читает текущие значения `MediaQueryList` и публикует новый snapshot.\n */\nfunction recomputeFromMql() {\n\tif (!currentMql) return;\n\n\temitIfChanged({\n\t\tmobile: currentMql.mobile.matches,\n\t\ttablet: currentMql.tablet.matches,\n\t\tlaptop: currentMql.laptop.matches\n\t});\n}\n\n/**\n * Снимает активные mql-подписки перед перестроением/очисткой.\n */\nfunction clearMqlSubscriptions() {\n\tmqlUnsubs.forEach((unsubscribe) => unsubscribe());\n\tmqlUnsubs = [];\n}\n\n/**\n * Перестраивает media query, когда корневой `em` изменился достаточно сильно (или принудительно).\n * Это дорогой путь, поэтому он специально ограничен проверками.\n */\nfunction rebuildIfEmChanged(force = false) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tconst em = getCurrentFontSize();\n\tif (!force && currentMql && Math.abs(em - emSizeRef) < EM_CHANGE_THRESHOLD) return;\n\n\temSizeRef = em;\n\tclearMqlSubscriptions();\n\n\tconst queries = buildQueriesPx(em);\n\tcurrentMql = {\n\t\tmobile: window.matchMedia(queries.mobile),\n\t\ttablet: window.matchMedia(queries.tablet),\n\t\tlaptop: window.matchMedia(queries.laptop)\n\t};\n\n\tmqlUnsubs = (Object.keys(currentMql) as Breakpoint[]).map((key) => subscribeMql(currentMql![key], recomputeFromMql));\n\trecomputeFromMql();\n}\n\n/**\n * Дебаунс-обертка для перестроения query по событиям observer-ов.\n */\nfunction scheduleRebuild() {\n\tif (rebuildTimer) clearTimeout(rebuildTimer);\n\trebuildTimer = setTimeout(() => {\n\t\trebuildTimer = null;\n\t\trebuildIfEmChanged();\n\t}, EM_REBUILD_DEBOUNCE_MS);\n}\n\n/**\n * Наблюдает за изменением style/class у root и размером probe-элемента `1em`.\n * Это позволяет ловить изменения font-size/zoom без слушателя window resize.\n */\nfunction setupEmObservers() {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tmutationObserver = new MutationObserver(scheduleRebuild);\n\tmutationObserver.observe(document.documentElement, {\n\t\tattributes: true,\n\t\tattributeFilter: [\"style\", \"class\", \"data-font-size\"]\n\t});\n\n\tif (typeof ResizeObserver === \"undefined\") return;\n\n\temProbeElement = document.createElement(\"div\");\n\temProbeElement.style.position = \"absolute\";\n\temProbeElement.style.visibility = \"hidden\";\n\temProbeElement.style.pointerEvents = \"none\";\n\temProbeElement.style.width = \"1em\";\n\temProbeElement.style.height = \"1px\";\n\temProbeElement.style.overflow = \"hidden\";\n\temProbeElement.setAttribute(\"aria-hidden\", \"true\");\n\n\t(document.body ?? document.documentElement).appendChild(emProbeElement);\n\n\tresizeObserver = new ResizeObserver(scheduleRebuild);\n\tresizeObserver.observe(emProbeElement);\n}\n\n/**\n * Полностью освобождает глобальные ресурсы, когда не осталось подписчиков.\n */\nfunction cleanupStore() {\n\tclearMqlSubscriptions();\n\tcurrentMql = null;\n\n\tif (rebuildTimer) {\n\t\tclearTimeout(rebuildTimer);\n\t\trebuildTimer = null;\n\t}\n\n\tif (mutationObserver) {\n\t\tmutationObserver.disconnect();\n\t\tmutationObserver = null;\n\t}\n\n\tif (resizeObserver) {\n\t\tresizeObserver.disconnect();\n\t\tresizeObserver = null;\n\t}\n\n\tif (emProbeElement?.parentNode) {\n\t\temProbeElement.parentNode.removeChild(emProbeElement);\n\t}\n\temProbeElement = null;\n\n\tinitialized = false;\n}\n\n/**\n * Лениво инициализирует singleton-store при первом подписчике.\n */\nfunction initializeStore() {\n\tif (initialized) return;\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tinitialized = true;\n\trebuildIfEmChanged(true);\n\tsetupEmObservers();\n}\n\n/**\n * API подписки для `useSyncExternalStore`.\n * Держит ровно один глобальный набор observer-ов и mql на всё приложение.\n */\nfunction subscribeStore(listener: () => void) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn () => {};\n\t}\n\n\tlisteners.add(listener);\n\tinitializeStore();\n\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0) {\n\t\t\tcleanupStore();\n\t\t}\n\t};\n}\n\n/**\n * Getter клиентского snapshot для `useSyncExternalStore`.\n */\nfunction getSnapshot() {\n\treturn snapshot;\n}\n\n/**\n * SSR-безопасный getter snapshot.\n */\nfunction getServerSnapshot() {\n\treturn DEFAULT_SNAPSHOT;\n}\n\n/**\n * Общий хук брейкпоинтов, работающий через singleton external store.\n */\nexport function useMatchMedia() {\n\treturn useSyncExternalStore(subscribeStore, getSnapshot, getServerSnapshot);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/getCurrentFontSize.ts","../../src/media/pxToEm.ts","../../src/media/responsive.ts","../../src/media/useMediaQuery.ts","../../src/media/useMatchMedia.ts"],"sourcesContent":["export const BREAKPOINTS_EM = {\n\tmobileMax: 767.98 / 16,\n\ttabletMin: 768 / 16,\n\ttabletMax: 1151.98 / 16,\n\tlaptopMin: 1152 / 16\n} as const;\n\nexport type Breakpoint = \"mobile\" | \"tablet\" | \"laptop\";\nexport type MediaMatches = Record<Breakpoint, boolean>;\n","const DEFAULT_FONT_SIZE = 16;\nconst DEFAULT_LINE_HEIGHT = 1.2;\n\nexport function getCurrentFontSize(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst rootFontSize = window.getComputedStyle(target).fontSize;\n\tconst fontSize = Number.parseFloat(rootFontSize);\n\n\tif (!Number.isFinite(fontSize) || fontSize <= 0) {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\treturn fontSize;\n}\n\nexport function getCurrentLineHeight(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst computedStyles = window.getComputedStyle(target);\n\tconst rootLineHeight = computedStyles.lineHeight;\n\tconst lineHeight = Number.parseFloat(rootLineHeight);\n\n\tif (!Number.isFinite(lineHeight) || lineHeight <= 0) {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\treturn lineHeight / DEFAULT_FONT_SIZE;\n}\n\nexport function getControlHeight() {\n\tconst fontSize = getCurrentFontSize();\n\tconst lineHeight = getCurrentLineHeight();\n\n\t// Правильно расчета --control-height\n\treturn fontSize + lineHeight * 10;\n}\n","import { getCurrentFontSize } from \"./getCurrentFontSize\";\n\nexport function pxToEm(px: number, baseEm?: number): string {\n\tconst emSize = baseEm ?? getCurrentFontSize(); // динамическая привязка\n\treturn (px / emSize).toString();\n}\n","import { Breakpoint } from \"./breakpoints\";\n\nexport type ResponsiveMap<T> = {\n\tmobile?: T;\n\ttablet?: T;\n\tlaptop?: T;\n};\nexport type ResponsiveValue<T> = T | ResponsiveMap<T>;\nexport type UnwrapResponsive<T> = T extends ResponsiveValue<infer U> ? U : T;\n\nexport function isResponsiveMap<T>(v: ResponsiveValue<T>): v is ResponsiveMap<T> {\n\tif (typeof v !== \"object\" || v === null) return false;\n\n\t// ключевой момент: отличаем “responsive-map” от произвольного объекта T\n\treturn \"mobile\" in v || \"tablet\" in v || \"laptop\" in v || \"desktop\" in v;\n}\n\n/**\n * Превращает объект пропсов, где некоторые поля ResponsiveValue<...>,\n * в объект, где эти поля уже \"плоские\" (U).\n */\nexport type ResolvedProps<T extends object> = {\n\t[K in keyof T]: UnwrapResponsive<T[K]>;\n};\n\n/**\n * Резолвит responsive значение под текущий брейкпоинт.\n * Приоритеты:\n * - exact match (mobile/tablet/laptop)\n * - fallback на \"первое определённое\" в предсказуемом порядке\n */\nexport function resolveResponsiveValue<T>(value: ResponsiveValue<T> | undefined, bp: Breakpoint): T | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isResponsiveMap(value)) {\n\t\t// здесь value точно T (в т.ч. может быть объектом — и это ок)\n\t\treturn value;\n\t}\n\n\tif (bp === \"mobile\") return value.mobile ?? value.tablet ?? value.laptop;\n\tif (bp === \"tablet\") return value.tablet ?? value.mobile ?? value.laptop;\n\n\treturn value.laptop ?? value.tablet ?? value.mobile;\n}\n\n/**\n * Типобезопасно резолвит все поля объекта по списку ключей, которые responsive.\n * (TS-магия: на выходе будет ResolvedProps<T>.)\n */\nexport function resolveProps<T extends object>(props: T, bp: Breakpoint, responsiveKeys: readonly (keyof T)[]): ResolvedProps<T> {\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst out: any = { ...props };\n\n\tfor (const key of responsiveKeys) {\n\t\tout[key] = resolveResponsiveValue(out[key], bp);\n\t}\n\n\treturn out as ResolvedProps<T>;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\n\ntype UseMediaQueryOptions = {\n\treadonly ssrFallback?: boolean;\n};\n\nconst noop = () => {};\n\nfunction readMediaQuery(query: string, fallback: boolean): boolean {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return fallback;\n\n\treturn window.matchMedia(query).matches;\n}\n\nfunction subscribeMediaQuery(query: string, listener: () => void): () => void {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return noop;\n\n\tconst mql = window.matchMedia(query);\n\n\tif (typeof mql.addEventListener === \"function\") {\n\t\tmql.addEventListener(\"change\", listener);\n\t\treturn () => mql.removeEventListener(\"change\", listener);\n\t}\n\n\tmql.addListener(listener);\n\treturn () => mql.removeListener(listener);\n}\n\n/**\n * SSR-безопасная подписка на произвольный media query.\n * Используется для клиентских предпочтений, где значение должно совпадать с SSR fallback на гидрации.\n */\nexport function useMediaQuery(query: string, { ssrFallback = false }: UseMediaQueryOptions = {}) {\n\tconst subscribe = useCallback((listener: () => void) => subscribeMediaQuery(query, listener), [query]);\n\tconst getSnapshot = useCallback(() => readMediaQuery(query, ssrFallback), [query, ssrFallback]);\n\tconst getServerSnapshot = useCallback(() => ssrFallback, [ssrFallback]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import { useSyncExternalStore } from \"react\";\n\nimport { BREAKPOINTS_EM, type Breakpoint, type MediaMatches } from \"./breakpoints\";\nimport { getCurrentFontSize } from \"./getCurrentFontSize\";\n\n/**\n * Публичный снимок состояния, который возвращает хук/store.\n * Содержит сырые совпадения media query и вычисленный активный брейкпоинт.\n */\ntype MatchMediaSnapshot = {\n\tmatches: MediaMatches;\n\tactiveBreakpoint: Breakpoint;\n};\n\nfunction subscribeMql(mql: MediaQueryList, cb: () => void) {\n\tif (\"addEventListener\" in mql) {\n\t\tmql.addEventListener(\"change\", cb);\n\t\treturn () => mql.removeEventListener(\"change\", cb);\n\t}\n\t// Safari legacy\n\t// @ts-expect-error legacy api\n\tmql.addListener(cb);\n\t// @ts-expect-error legacy api\n\treturn () => mql.removeListener(cb);\n}\n\nfunction buildQueriesPx(emSize: number) {\n\tconst toPx = (em: number) => em * emSize;\n\n\tconst mobileMax = toPx(BREAKPOINTS_EM.mobileMax);\n\tconst tabletMin = toPx(BREAKPOINTS_EM.tabletMin);\n\tconst tabletMax = toPx(BREAKPOINTS_EM.tabletMax);\n\tconst laptopMin = toPx(BREAKPOINTS_EM.laptopMin);\n\n\treturn {\n\t\tmobile: `(width <= ${mobileMax}px)`,\n\t\ttablet: `(width >= ${tabletMin}px) and (width <= ${tabletMax}px)`,\n\t\tlaptop: `(width >= ${laptopMin}px)`\n\t} satisfies Record<Breakpoint, string>;\n}\n\nconst DEFAULT_MATCHES: MediaMatches = {\n\tmobile: false,\n\ttablet: false,\n\tlaptop: false\n};\n\nconst DEFAULT_SNAPSHOT: MatchMediaSnapshot = {\n\tmatches: DEFAULT_MATCHES,\n\tactiveBreakpoint: \"laptop\"\n};\n\n/**\n * Игнорируем микроколебания `em`, чтобы не перестраивать media query без необходимости.\n */\nconst EM_CHANGE_THRESHOLD = 0.25;\n/**\n * Склеиваем частые события observer-ов в одно перестроение.\n */\nconst EM_REBUILD_DEBOUNCE_MS = 80;\n\nlet snapshot: MatchMediaSnapshot = DEFAULT_SNAPSHOT;\nlet currentMql: Record<Breakpoint, MediaQueryList> | null = null;\nlet mqlUnsubs: Array<() => void> = [];\nlet emSizeRef = 0;\nlet initialized = false;\nlet mutationObserver: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet emProbeElement: HTMLDivElement | null = null;\nlet rebuildTimer: ReturnType<typeof setTimeout> | null = null;\n\nconst listeners = new Set<() => void>();\n\n/**\n * Выбирает один активный брейкпоинт по приоритету: mobile -> tablet -> laptop.\n */\nfunction getActiveBreakpoint(matches: MediaMatches): Breakpoint {\n\tif (matches.mobile) return \"mobile\";\n\tif (matches.tablet) return \"tablet\";\n\treturn \"laptop\";\n}\n\n/**\n * Поверхностное сравнение media-состояния, чтобы пропускать пустые уведомления.\n */\nfunction matchesEqual(a: MediaMatches, b: MediaMatches) {\n\treturn a.mobile === b.mobile && a.tablet === b.tablet && a.laptop === b.laptop;\n}\n\n/**\n * Обновляет глобальный snapshot и уведомляет подписчиков только при реальных изменениях.\n */\nfunction emitIfChanged(nextMatches: MediaMatches) {\n\tif (matchesEqual(snapshot.matches, nextMatches)) return;\n\tsnapshot = {\n\t\tmatches: nextMatches,\n\t\tactiveBreakpoint: getActiveBreakpoint(nextMatches)\n\t};\n\tlisteners.forEach((listener) => listener());\n}\n\n/**\n * Читает текущие значения `MediaQueryList` и публикует новый snapshot.\n */\nfunction recomputeFromMql() {\n\tif (!currentMql) return;\n\n\temitIfChanged({\n\t\tmobile: currentMql.mobile.matches,\n\t\ttablet: currentMql.tablet.matches,\n\t\tlaptop: currentMql.laptop.matches\n\t});\n}\n\n/**\n * Снимает активные mql-подписки перед перестроением/очисткой.\n */\nfunction clearMqlSubscriptions() {\n\tmqlUnsubs.forEach((unsubscribe) => unsubscribe());\n\tmqlUnsubs = [];\n}\n\n/**\n * Перестраивает media query, когда корневой `em` изменился достаточно сильно (или принудительно).\n * Это дорогой путь, поэтому он специально ограничен проверками.\n */\nfunction rebuildIfEmChanged(force = false) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tconst em = getCurrentFontSize();\n\tif (!force && currentMql && Math.abs(em - emSizeRef) < EM_CHANGE_THRESHOLD) return;\n\n\temSizeRef = em;\n\tclearMqlSubscriptions();\n\n\tconst queries = buildQueriesPx(em);\n\tcurrentMql = {\n\t\tmobile: window.matchMedia(queries.mobile),\n\t\ttablet: window.matchMedia(queries.tablet),\n\t\tlaptop: window.matchMedia(queries.laptop)\n\t};\n\n\tmqlUnsubs = (Object.keys(currentMql) as Breakpoint[]).map((key) => subscribeMql(currentMql![key], recomputeFromMql));\n\trecomputeFromMql();\n}\n\n/**\n * Дебаунс-обертка для перестроения query по событиям observer-ов.\n */\nfunction scheduleRebuild() {\n\tif (rebuildTimer) clearTimeout(rebuildTimer);\n\trebuildTimer = setTimeout(() => {\n\t\trebuildTimer = null;\n\t\trebuildIfEmChanged();\n\t}, EM_REBUILD_DEBOUNCE_MS);\n}\n\n/**\n * Наблюдает за изменением style/class у root и размером probe-элемента `1em`.\n * Это позволяет ловить изменения font-size/zoom без слушателя window resize.\n */\nfunction setupEmObservers() {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tmutationObserver = new MutationObserver(scheduleRebuild);\n\tmutationObserver.observe(document.documentElement, {\n\t\tattributes: true,\n\t\tattributeFilter: [\"style\", \"class\", \"data-font-size\"]\n\t});\n\n\tif (typeof ResizeObserver === \"undefined\") return;\n\n\temProbeElement = document.createElement(\"div\");\n\temProbeElement.style.position = \"absolute\";\n\temProbeElement.style.visibility = \"hidden\";\n\temProbeElement.style.pointerEvents = \"none\";\n\temProbeElement.style.width = \"1em\";\n\temProbeElement.style.height = \"1px\";\n\temProbeElement.style.overflow = \"hidden\";\n\temProbeElement.setAttribute(\"aria-hidden\", \"true\");\n\n\t(document.body ?? document.documentElement).appendChild(emProbeElement);\n\n\tresizeObserver = new ResizeObserver(scheduleRebuild);\n\tresizeObserver.observe(emProbeElement);\n}\n\n/**\n * Полностью освобождает глобальные ресурсы, когда не осталось подписчиков.\n */\nfunction cleanupStore() {\n\tclearMqlSubscriptions();\n\tcurrentMql = null;\n\n\tif (rebuildTimer) {\n\t\tclearTimeout(rebuildTimer);\n\t\trebuildTimer = null;\n\t}\n\n\tif (mutationObserver) {\n\t\tmutationObserver.disconnect();\n\t\tmutationObserver = null;\n\t}\n\n\tif (resizeObserver) {\n\t\tresizeObserver.disconnect();\n\t\tresizeObserver = null;\n\t}\n\n\tif (emProbeElement?.parentNode) {\n\t\temProbeElement.parentNode.removeChild(emProbeElement);\n\t}\n\temProbeElement = null;\n\n\tinitialized = false;\n}\n\n/**\n * Лениво инициализирует singleton-store при первом подписчике.\n */\nfunction initializeStore() {\n\tif (initialized) return;\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tinitialized = true;\n\trebuildIfEmChanged(true);\n\tsetupEmObservers();\n}\n\n/**\n * API подписки для `useSyncExternalStore`.\n * Держит ровно один глобальный набор observer-ов и mql на всё приложение.\n */\nfunction subscribeStore(listener: () => void) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn () => {};\n\t}\n\n\tlisteners.add(listener);\n\tinitializeStore();\n\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0) {\n\t\t\tcleanupStore();\n\t\t}\n\t};\n}\n\n/**\n * Getter клиентского snapshot для `useSyncExternalStore`.\n */\nfunction getSnapshot() {\n\treturn snapshot;\n}\n\n/**\n * SSR-безопасный getter snapshot.\n */\nfunction getServerSnapshot() {\n\treturn DEFAULT_SNAPSHOT;\n}\n\n/**\n * Общий хук брейкпоинтов, работающий через singleton external store.\n */\nexport function useMatchMedia() {\n\treturn useSyncExternalStore(subscribeStore, getSnapshot, getServerSnapshot);\n}\n"],"mappings":"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"}
|
|
@@ -32,7 +32,7 @@ function i(e) {
|
|
|
32
32
|
function a(e, t, n) {
|
|
33
33
|
if (!e?.length) return [];
|
|
34
34
|
let r = /* @__PURE__ */ new Set();
|
|
35
|
-
return e.filter((e) => Number.isFinite(e.value) && e.value >= t && e.value <= n).slice().sort((e, t) => e.value - t.value).filter((e) => r.has(e.value)
|
|
35
|
+
return e.filter((e) => Number.isFinite(e.value) && e.value >= t && e.value <= n).slice().sort((e, t) => e.value - t.value).filter((e) => !r.has(e.value) && (r.add(e.value), !0));
|
|
36
36
|
}
|
|
37
37
|
function o(n, a, o, s) {
|
|
38
38
|
let c = i(s), l = r(n, a, o), u = Math.max(e(c), e(a), e(o));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/number-scale/numberScale.ts"],"sourcesContent":["import { NumberScaleBounds, NumberScaleMark, NumberScaleMarksPosition, NumberScaleOptions } from \"./types\";\n\nfunction countFractionDigits(value: number) {\n\tif (!Number.isFinite(value)) {\n\t\treturn 0;\n\t}\n\n\tconst normalized = value.toString().toLowerCase();\n\tconst exponentIndex = normalized.indexOf(\"e-\");\n\tif (exponentIndex >= 0) {\n\t\treturn Number(normalized.slice(exponentIndex + 2));\n\t}\n\n\tconst dotIndex = normalized.indexOf(\".\");\n\treturn dotIndex >= 0 ? normalized.length - dotIndex - 1 : 0;\n}\n\nfunction roundWithPrecision(value: number, precision: number) {\n\tif (precision <= 0) {\n\t\treturn Math.round(value);\n\t}\n\n\tconst factor = 10 ** precision;\n\treturn Math.round(value * factor) / factor;\n}\n\n/**\n * Возвращает безопасные границы числовой шкалы и меняет их местами, если min больше max.\n */\nexport function resolveNumberScaleBounds(min: number, max: number): NumberScaleBounds {\n\tif (!Number.isFinite(min) || !Number.isFinite(max)) {\n\t\treturn { min: 0, max: 100 };\n\t}\n\n\treturn min <= max ? { min, max } : { min: max, max: min };\n}\n\n/**\n * Ограничивает значение конечными границами шкалы.\n */\nexport function clampNumberScaleValue(value: number, min: number, max: number) {\n\tif (!Number.isFinite(value)) {\n\t\treturn min;\n\t}\n\n\treturn Math.min(max, Math.max(min, value));\n}\n\n/**\n * Нормализует шаг шкалы: некорректные, нулевые и отрицательные значения заменяет на 1.\n */\nexport function normalizeNumberScaleStep(step?: number) {\n\tif (!Number.isFinite(step) || step === undefined || step <= 0) {\n\t\treturn 1;\n\t}\n\n\treturn step;\n}\n\n/**\n * Фильтрует, сортирует и дедуплицирует marks по finite value внутри границ шкалы.\n */\nexport function prepareNumberScaleMarks<TMark extends NumberScaleMark>(\n\tmarks: readonly TMark[] | undefined,\n\tmin: number,\n\tmax: number\n): TMark[] {\n\tif (!marks?.length) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<number>();\n\n\treturn marks\n\t\t.filter((mark) => Number.isFinite(mark.value) && mark.value >= min && mark.value <= max)\n\t\t.slice()\n\t\t.sort((left, right) => left.value - right.value)\n\t\t.filter((mark) => {\n\t\t\tif (seen.has(mark.value)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tseen.add(mark.value);\n\t\t\treturn true;\n\t\t});\n}\n\n/**\n * Привязывает значение к ближайшему шагу с учётом десятичной точности min/max/step.\n */\nexport function snapNumberScaleValueToStep(value: number, min: number, max: number, step?: number) {\n\tconst normalizedStep = normalizeNumberScaleStep(step);\n\tconst clamped = clampNumberScaleValue(value, min, max);\n\tconst precision = Math.max(countFractionDigits(normalizedStep), countFractionDigits(min), countFractionDigits(max));\n\tconst snapped = min + Math.round((clamped - min) / normalizedStep) * normalizedStep;\n\n\treturn clampNumberScaleValue(roundWithPrecision(snapped, precision), min, max);\n}\n\n/**\n * Возвращает ближайший mark по числовому value.\n */\nexport function findClosestNumberScaleMark<TMark extends NumberScaleMark>(value: number, marks: readonly TMark[]) {\n\tif (!marks.length) {\n\t\treturn undefined;\n\t}\n\n\tlet nearest = marks[0];\n\tlet distance = Math.abs(value - nearest.value);\n\n\tfor (let index = 1; index < marks.length; index += 1) {\n\t\tconst mark = marks[index];\n\t\tconst nextDistance = Math.abs(value - mark.value);\n\n\t\tif (nextDistance < distance) {\n\t\t\tnearest = mark;\n\t\t\tdistance = nextDistance;\n\t\t}\n\t}\n\n\treturn nearest;\n}\n\n/**\n * Привязывает значение к value ближайшего mark.\n */\nexport function snapNumberScaleValueToMarks<TMark extends NumberScaleMark>(\n\tvalue: number,\n\tmarks: readonly TMark[],\n\tmin: number,\n\tmax: number\n) {\n\tif (!marks.length) {\n\t\treturn clampNumberScaleValue(value, min, max);\n\t}\n\n\tconst clamped = clampNumberScaleValue(value, min, max);\n\treturn findClosestNumberScaleMark(clamped, marks)?.value ?? clamped;\n}\n\n/**\n * Привязывает значение к marks, если они есть, иначе к step.\n */\nexport function snapNumberScaleValue<TMark extends NumberScaleMark>(value: number, options: NumberScaleOptions<TMark>) {\n\tconst { min, max } = resolveNumberScaleBounds(options.min, options.max);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\treturn snapNumberScaleValueToMarks(value, preparedMarks, min, max);\n\t}\n\n\treturn snapNumberScaleValueToStep(value, min, max, options.step);\n}\n\nexport function valueToNumberScalePercent(value: number, min: number, max: number) {\n\tif (max <= min) {\n\t\treturn 0;\n\t}\n\n\treturn ((clampNumberScaleValue(value, min, max) - min) / (max - min)) * 100;\n}\n\nexport function percentToNumberScaleValue(percent: number, min: number, max: number) {\n\tif (max <= min) {\n\t\treturn min;\n\t}\n\n\tconst safePercent = clampNumberScaleValue(percent, 0, 100);\n\treturn min + ((max - min) * safePercent) / 100;\n}\n\n/**\n * Возвращает позицию mark в процентах: по числовому value или равномерно по индексу.\n */\nexport function getNumberScaleMarkPercent(\n\tmarkIndex: number,\n\tmarks: readonly NumberScaleMark[],\n\tmin: number,\n\tmax: number,\n\tmarksPosition: NumberScaleMarksPosition = \"value\"\n) {\n\tif (marksPosition === \"index\" && marks.length > 1) {\n\t\treturn (markIndex / (marks.length - 1)) * 100;\n\t}\n\n\tif (marksPosition === \"index\") {\n\t\treturn 0;\n\t}\n\n\treturn valueToNumberScalePercent(marks[markIndex]?.value ?? min, min, max);\n}\n\n/**\n * Находит ближайший mark по визуальной позиции процента.\n */\nexport function findClosestNumberScaleMarkByPercent<TMark extends NumberScaleMark>(\n\tpercent: number,\n\tmarks: readonly TMark[],\n\tmin: number,\n\tmax: number,\n\tmarksPosition: NumberScaleMarksPosition = \"value\"\n) {\n\tif (!marks.length) {\n\t\treturn undefined;\n\t}\n\n\tconst safePercent = clampNumberScaleValue(percent, 0, 100);\n\tlet nearest = marks[0];\n\tlet distance = Math.abs(safePercent - getNumberScaleMarkPercent(0, marks, min, max, marksPosition));\n\n\tfor (let index = 1; index < marks.length; index += 1) {\n\t\tconst nextDistance = Math.abs(safePercent - getNumberScaleMarkPercent(index, marks, min, max, marksPosition));\n\n\t\tif (nextDistance < distance) {\n\t\t\tnearest = marks[index];\n\t\t\tdistance = nextDistance;\n\t\t}\n\t}\n\n\treturn nearest;\n}\n\n/**\n * Преобразует pointer percent в значение шкалы с учётом равномерного позиционирования marks.\n */\nexport function percentToSnappedNumberScaleValue<TMark extends NumberScaleMark>(percent: number, options: NumberScaleOptions<TMark>) {\n\tconst { min, max } = resolveNumberScaleBounds(options.min, options.max);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\tconst closestMark = findClosestNumberScaleMarkByPercent(percent, preparedMarks, min, max, options.marksPosition);\n\t\treturn closestMark?.value ?? min;\n\t}\n\n\treturn snapNumberScaleValueToStep(percentToNumberScaleValue(percent, min, max), min, max, options.step);\n}\n\n/**\n * Смещает значение на offset шагов или на offset marks.\n */\nexport function offsetNumberScaleValue<TMark extends NumberScaleMark>(\n\tcurrentValue: number,\n\toffset: number,\n\toptions: NumberScaleOptions<TMark>,\n\tlowerBound = options.min,\n\tupperBound = options.max\n) {\n\tconst { min, max } = resolveNumberScaleBounds(lowerBound, upperBound);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\tconst values = preparedMarks.map((mark) => mark.value);\n\t\tlet nearestIndex = 0;\n\t\tlet nearestDistance = Math.abs(currentValue - values[0]);\n\n\t\tfor (let index = 1; index < values.length; index += 1) {\n\t\t\tconst nextDistance = Math.abs(currentValue - values[index]);\n\t\t\tif (nextDistance < nearestDistance) {\n\t\t\t\tnearestDistance = nextDistance;\n\t\t\t\tnearestIndex = index;\n\t\t\t}\n\t\t}\n\n\t\tconst nextIndex = clampNumberScaleValue(nearestIndex + offset, 0, values.length - 1);\n\t\treturn values[nextIndex];\n\t}\n\n\tconst normalizedStep = normalizeNumberScaleStep(options.step);\n\treturn snapNumberScaleValueToStep(currentValue + offset * normalizedStep, min, max, normalizedStep);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/number-scale/numberScale.ts"],"sourcesContent":["import { NumberScaleBounds, NumberScaleMark, NumberScaleMarksPosition, NumberScaleOptions } from \"./types\";\n\nfunction countFractionDigits(value: number) {\n\tif (!Number.isFinite(value)) {\n\t\treturn 0;\n\t}\n\n\tconst normalized = value.toString().toLowerCase();\n\tconst exponentIndex = normalized.indexOf(\"e-\");\n\tif (exponentIndex >= 0) {\n\t\treturn Number(normalized.slice(exponentIndex + 2));\n\t}\n\n\tconst dotIndex = normalized.indexOf(\".\");\n\treturn dotIndex >= 0 ? normalized.length - dotIndex - 1 : 0;\n}\n\nfunction roundWithPrecision(value: number, precision: number) {\n\tif (precision <= 0) {\n\t\treturn Math.round(value);\n\t}\n\n\tconst factor = 10 ** precision;\n\treturn Math.round(value * factor) / factor;\n}\n\n/**\n * Возвращает безопасные границы числовой шкалы и меняет их местами, если min больше max.\n */\nexport function resolveNumberScaleBounds(min: number, max: number): NumberScaleBounds {\n\tif (!Number.isFinite(min) || !Number.isFinite(max)) {\n\t\treturn { min: 0, max: 100 };\n\t}\n\n\treturn min <= max ? { min, max } : { min: max, max: min };\n}\n\n/**\n * Ограничивает значение конечными границами шкалы.\n */\nexport function clampNumberScaleValue(value: number, min: number, max: number) {\n\tif (!Number.isFinite(value)) {\n\t\treturn min;\n\t}\n\n\treturn Math.min(max, Math.max(min, value));\n}\n\n/**\n * Нормализует шаг шкалы: некорректные, нулевые и отрицательные значения заменяет на 1.\n */\nexport function normalizeNumberScaleStep(step?: number) {\n\tif (!Number.isFinite(step) || step === undefined || step <= 0) {\n\t\treturn 1;\n\t}\n\n\treturn step;\n}\n\n/**\n * Фильтрует, сортирует и дедуплицирует marks по finite value внутри границ шкалы.\n */\nexport function prepareNumberScaleMarks<TMark extends NumberScaleMark>(\n\tmarks: readonly TMark[] | undefined,\n\tmin: number,\n\tmax: number\n): TMark[] {\n\tif (!marks?.length) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<number>();\n\n\treturn marks\n\t\t.filter((mark) => Number.isFinite(mark.value) && mark.value >= min && mark.value <= max)\n\t\t.slice()\n\t\t.sort((left, right) => left.value - right.value)\n\t\t.filter((mark) => {\n\t\t\tif (seen.has(mark.value)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tseen.add(mark.value);\n\t\t\treturn true;\n\t\t});\n}\n\n/**\n * Привязывает значение к ближайшему шагу с учётом десятичной точности min/max/step.\n */\nexport function snapNumberScaleValueToStep(value: number, min: number, max: number, step?: number) {\n\tconst normalizedStep = normalizeNumberScaleStep(step);\n\tconst clamped = clampNumberScaleValue(value, min, max);\n\tconst precision = Math.max(countFractionDigits(normalizedStep), countFractionDigits(min), countFractionDigits(max));\n\tconst snapped = min + Math.round((clamped - min) / normalizedStep) * normalizedStep;\n\n\treturn clampNumberScaleValue(roundWithPrecision(snapped, precision), min, max);\n}\n\n/**\n * Возвращает ближайший mark по числовому value.\n */\nexport function findClosestNumberScaleMark<TMark extends NumberScaleMark>(value: number, marks: readonly TMark[]) {\n\tif (!marks.length) {\n\t\treturn undefined;\n\t}\n\n\tlet nearest = marks[0];\n\tlet distance = Math.abs(value - nearest.value);\n\n\tfor (let index = 1; index < marks.length; index += 1) {\n\t\tconst mark = marks[index];\n\t\tconst nextDistance = Math.abs(value - mark.value);\n\n\t\tif (nextDistance < distance) {\n\t\t\tnearest = mark;\n\t\t\tdistance = nextDistance;\n\t\t}\n\t}\n\n\treturn nearest;\n}\n\n/**\n * Привязывает значение к value ближайшего mark.\n */\nexport function snapNumberScaleValueToMarks<TMark extends NumberScaleMark>(\n\tvalue: number,\n\tmarks: readonly TMark[],\n\tmin: number,\n\tmax: number\n) {\n\tif (!marks.length) {\n\t\treturn clampNumberScaleValue(value, min, max);\n\t}\n\n\tconst clamped = clampNumberScaleValue(value, min, max);\n\treturn findClosestNumberScaleMark(clamped, marks)?.value ?? clamped;\n}\n\n/**\n * Привязывает значение к marks, если они есть, иначе к step.\n */\nexport function snapNumberScaleValue<TMark extends NumberScaleMark>(value: number, options: NumberScaleOptions<TMark>) {\n\tconst { min, max } = resolveNumberScaleBounds(options.min, options.max);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\treturn snapNumberScaleValueToMarks(value, preparedMarks, min, max);\n\t}\n\n\treturn snapNumberScaleValueToStep(value, min, max, options.step);\n}\n\nexport function valueToNumberScalePercent(value: number, min: number, max: number) {\n\tif (max <= min) {\n\t\treturn 0;\n\t}\n\n\treturn ((clampNumberScaleValue(value, min, max) - min) / (max - min)) * 100;\n}\n\nexport function percentToNumberScaleValue(percent: number, min: number, max: number) {\n\tif (max <= min) {\n\t\treturn min;\n\t}\n\n\tconst safePercent = clampNumberScaleValue(percent, 0, 100);\n\treturn min + ((max - min) * safePercent) / 100;\n}\n\n/**\n * Возвращает позицию mark в процентах: по числовому value или равномерно по индексу.\n */\nexport function getNumberScaleMarkPercent(\n\tmarkIndex: number,\n\tmarks: readonly NumberScaleMark[],\n\tmin: number,\n\tmax: number,\n\tmarksPosition: NumberScaleMarksPosition = \"value\"\n) {\n\tif (marksPosition === \"index\" && marks.length > 1) {\n\t\treturn (markIndex / (marks.length - 1)) * 100;\n\t}\n\n\tif (marksPosition === \"index\") {\n\t\treturn 0;\n\t}\n\n\treturn valueToNumberScalePercent(marks[markIndex]?.value ?? min, min, max);\n}\n\n/**\n * Находит ближайший mark по визуальной позиции процента.\n */\nexport function findClosestNumberScaleMarkByPercent<TMark extends NumberScaleMark>(\n\tpercent: number,\n\tmarks: readonly TMark[],\n\tmin: number,\n\tmax: number,\n\tmarksPosition: NumberScaleMarksPosition = \"value\"\n) {\n\tif (!marks.length) {\n\t\treturn undefined;\n\t}\n\n\tconst safePercent = clampNumberScaleValue(percent, 0, 100);\n\tlet nearest = marks[0];\n\tlet distance = Math.abs(safePercent - getNumberScaleMarkPercent(0, marks, min, max, marksPosition));\n\n\tfor (let index = 1; index < marks.length; index += 1) {\n\t\tconst nextDistance = Math.abs(safePercent - getNumberScaleMarkPercent(index, marks, min, max, marksPosition));\n\n\t\tif (nextDistance < distance) {\n\t\t\tnearest = marks[index];\n\t\t\tdistance = nextDistance;\n\t\t}\n\t}\n\n\treturn nearest;\n}\n\n/**\n * Преобразует pointer percent в значение шкалы с учётом равномерного позиционирования marks.\n */\nexport function percentToSnappedNumberScaleValue<TMark extends NumberScaleMark>(percent: number, options: NumberScaleOptions<TMark>) {\n\tconst { min, max } = resolveNumberScaleBounds(options.min, options.max);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\tconst closestMark = findClosestNumberScaleMarkByPercent(percent, preparedMarks, min, max, options.marksPosition);\n\t\treturn closestMark?.value ?? min;\n\t}\n\n\treturn snapNumberScaleValueToStep(percentToNumberScaleValue(percent, min, max), min, max, options.step);\n}\n\n/**\n * Смещает значение на offset шагов или на offset marks.\n */\nexport function offsetNumberScaleValue<TMark extends NumberScaleMark>(\n\tcurrentValue: number,\n\toffset: number,\n\toptions: NumberScaleOptions<TMark>,\n\tlowerBound = options.min,\n\tupperBound = options.max\n) {\n\tconst { min, max } = resolveNumberScaleBounds(lowerBound, upperBound);\n\tconst preparedMarks = prepareNumberScaleMarks(options.marks, min, max);\n\n\tif (preparedMarks.length > 0) {\n\t\tconst values = preparedMarks.map((mark) => mark.value);\n\t\tlet nearestIndex = 0;\n\t\tlet nearestDistance = Math.abs(currentValue - values[0]);\n\n\t\tfor (let index = 1; index < values.length; index += 1) {\n\t\t\tconst nextDistance = Math.abs(currentValue - values[index]);\n\t\t\tif (nextDistance < nearestDistance) {\n\t\t\t\tnearestDistance = nextDistance;\n\t\t\t\tnearestIndex = index;\n\t\t\t}\n\t\t}\n\n\t\tconst nextIndex = clampNumberScaleValue(nearestIndex + offset, 0, values.length - 1);\n\t\treturn values[nextIndex];\n\t}\n\n\tconst normalizedStep = normalizeNumberScaleStep(options.step);\n\treturn snapNumberScaleValueToStep(currentValue + offset * normalizedStep, min, max, normalizedStep);\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as e, A as t, At as n, B as r, C as i, Ct as a, D as o, Dt as s, E as c, Et as l, F as u, G as d, H as f, I as p, J as m, K as h, L as g, M as _, N as v, O as y, Ot as b, P as x, Q as S, R as C, S as w, St as T, T as E, Tt as D, U as O, V as k, W as A, X as j, Y as M, Z as N, _ as P, _t as F, a as I, at as L, b as R, bt as z, c as B, ct as V, d as H, dt as U, et as W, f as G, ft as K, g as q, gt as J, h as Y, ht as X, i as Z, it as Q, j as $, k as ee, kt as te, l as ne, lt as re, m as ie, mt as ae, n as oe, nt as se, o as ce, ot as le, p as ue, pt as de, q as fe, r as pe, rt as me, s as he, st as ge, t as _e, tt as ve, u as ye, ut as be, v as xe, vt as Se, w as Ce, wt as we, x as Te, xt as Ee, y as De, yt as Oe, z as ke } from "../chunks/odata-service-
|
|
1
|
+
import { $ as e, A as t, At as n, B as r, C as i, Ct as a, D as o, Dt as s, E as c, Et as l, F as u, G as d, H as f, I as p, J as m, K as h, L as g, M as _, N as v, O as y, Ot as b, P as x, Q as S, R as C, S as w, St as T, T as E, Tt as D, U as O, V as k, W as A, X as j, Y as M, Z as N, _ as P, _t as F, a as I, at as L, b as R, bt as z, c as B, ct as V, d as H, dt as U, et as W, f as G, ft as K, g as q, gt as J, h as Y, ht as X, i as Z, it as Q, j as $, k as ee, kt as te, l as ne, lt as re, m as ie, mt as ae, n as oe, nt as se, o as ce, ot as le, p as ue, pt as de, q as fe, r as pe, rt as me, s as he, st as ge, t as _e, tt as ve, u as ye, ut as be, v as xe, vt as Se, w as Ce, wt as we, x as Te, xt as Ee, y as De, yt as Oe, z as ke } from "../chunks/odata-service-B5w4YBao.js";
|
|
2
2
|
export { s as BASE_META_TYPES, b as BASE_META_TYPE_LABELS, k as FILTER_OPERATIONS, i as FORCED_CODE_TEXT_IDS, f as FilterOperation, H as assertSqlFilterFieldId, ve as buildEntityOperationPath, se as buildEntityParameters, me as buildEntityPath, Q as buildFunctionImportParameters, L as buildFunctionImportPath, O as buildODataFilter, A as buildODataFilterRecursive, P as buildODataOrder, xe as buildODataOrderBy, le as buildParameterEntries, G as buildSqlFilter, y as collapseChainedODataSegmentFilterValues, e as collectFilterableColumns, W as collectFilterableColumnsIds, ee as compileFiltersToExpression, ge as createBooleanFormatter, V as createDateFormatter, d as createFilter, h as createFilterBetween, fe as createFilterContains, m as createFilterEqual, M as createFilterEqualFalsy, re as createFormatter, be as createNumberFormatter, ue as createSqlFilterEqual, ie as createSqlFilterGroup, Y as createSqlFilterIn, U as createStringFormatter, K as defaultControlTypeValue, de as defaultODataTypeValue, t as flattenFilterValuesToODataDependencies, ae as getBaseTypeFromODataType, X as getFormatter, J as getFormatterForBaseType, F as getFormattersFor, Se as getFormattersForBaseType, De as getSortIndex, R as getSortIndicator, te as isBaseMetaType, pe as isBaseValue, T as isBooleanSafe, a as isDateSafe, $ as isFilterLocalComponentId, j as isFilterOperation, Ce as isForcedCodeTextFamilyId, E as isForcedCodeTextId, we as isNumberSafe, he as isODataBooleanType, B as isODataDateType, ne as isODataIntegerType, ye as isODataNumericType, q as isSqlFilterScalarValue, D as isStringSafe, N as mergeFilterExpressions, _ as mergeFilterValuePatch, Z as normalizeBaseValue, v as normalizeDictionaryCodeKey, I as normalizeRangeValue, Oe as odataFormatValue, z as odataParseValue, Ee as odataParseValueByMetadata, l as odataTypeSchemas, c as parseServiceMetadata, n as resolveBaseTypeLabel, Te as resolveEffectiveSorts, o as resolveMetadataColumnLabel, x as resolveODataFilterDefinitionActiveColumnIds, u as resolveODataFilterDefinitionColumnIds, S as resolveOperationLabel, p as sanitizeFilterBinding, g as sanitizeFilterConditionGroup, C as sanitizeFilterDefinitions, ke as sanitizeFilterValue, r as sanitizeFilterValues, w as toggleSort, _e as unwrapODataParams, ce as unwrapODataQueryResult, oe as wrapODataParams };
|
package/dist/pwa/index.js
CHANGED
|
@@ -90,7 +90,7 @@ async function I() {
|
|
|
90
90
|
}
|
|
91
91
|
//#endregion
|
|
92
92
|
//#region src/pwa/useServiceWorkerUpdate.ts
|
|
93
|
-
var L =
|
|
93
|
+
var L = 3e5, R = 3e4;
|
|
94
94
|
function z() {
|
|
95
95
|
let [e] = performance.getEntriesByType("navigation");
|
|
96
96
|
return e?.type === "reload" || e?.type === "navigate";
|
package/dist/pwa/index.js.map
CHANGED
|
@@ -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/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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"}
|
|
@@ -16,9 +16,9 @@ function n(e) {
|
|
|
16
16
|
let a = Math.round(r * {
|
|
17
17
|
ms: 1,
|
|
18
18
|
s: 1e3,
|
|
19
|
-
m:
|
|
20
|
-
h:
|
|
21
|
-
d:
|
|
19
|
+
m: 6e4,
|
|
20
|
+
h: 36e5,
|
|
21
|
+
d: 864e5
|
|
22
22
|
}[i]);
|
|
23
23
|
if (!(!Number.isFinite(a) || a <= 0 || a > 31536e6)) return {
|
|
24
24
|
durationLabel: `${n[1]}${i}`.replace(".", "_"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-cache-policy.js","names":[],"sources":["../../src/pwa/swCachePolicy.ts"],"sourcesContent":["export type SwCachePolicyMode = \"off\" | \"ttl\" | \"bust\";\nexport type SwCacheDurationUnit = \"ms\" | \"s\" | \"m\" | \"h\" | \"d\";\n\nexport interface ParsedSwCachePolicy {\n\tmode: SwCachePolicyMode;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tname?: string;\n\tmaxEntries?: number;\n\tresolvedMaxEntries?: number;\n\tttlMs?: number;\n\tcacheName?: string;\n}\n\nexport interface SwCachePolicyDraft {\n\tmode: SwCachePolicyMode;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tname?: string;\n\tmaxEntries?: number;\n}\n\nconst DEFAULT_ODATA_CACHE_MAX_ENTRIES = 100;\nconst MAX_ODATA_CACHE_MAX_ENTRIES = 5000;\nconst MAX_ODATA_CACHE_TTL_MS = 365 * 24 * 60 * 60 * 1000;\n\ntype DurationParseResult = {\n\tdurationLabel: string;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tttlMs: number;\n};\n\nexport function normalizeSwCacheName(value: string | undefined): string | undefined {\n\tconst normalized = value\n\t\t?.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\");\n\treturn normalized || undefined;\n}\n\nfunction parseDurationToken(value: string): DurationParseResult | undefined {\n\tconst normalized = value.trim().toLowerCase();\n\tif (normalized === \"forever\") {\n\t\treturn {\n\t\t\tdurationLabel: \"forever\",\n\t\t\tttlMs: Infinity\n\t\t};\n\t}\n\n\tconst match = normalized.match(/^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/);\n\tif (!match) return undefined;\n\n\tconst amount = Number(match[1]);\n\tconst unit = match[2] as SwCacheDurationUnit;\n\tif (!Number.isFinite(amount) || amount <= 0) return undefined;\n\n\tconst multiplierByUnit: Record<SwCacheDurationUnit, number> = {\n\t\tms: 1,\n\t\ts: 1000,\n\t\tm: 60 * 1000,\n\t\th: 60 * 60 * 1000,\n\t\td: 24 * 60 * 60 * 1000\n\t};\n\tconst ttlMs = Math.round(amount * multiplierByUnit[unit]);\n\tif (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_ODATA_CACHE_TTL_MS) return undefined;\n\n\tconst durationLabel = `${match[1]}${unit}`.replace(\".\", \"_\");\n\treturn {\n\t\tdurationLabel,\n\t\tamount,\n\t\tunit,\n\t\tttlMs\n\t};\n}\n\nfunction parseMaxEntries(value: string | undefined): number | undefined {\n\tif (!value) return undefined;\n\n\tconst parsed = Number(value.trim());\n\tif (!Number.isInteger(parsed) || parsed <= 0) return undefined;\n\n\treturn Math.min(parsed, MAX_ODATA_CACHE_MAX_ENTRIES);\n}\n\nfunction createCacheName(duration: DurationParseResult, name: string | undefined, maxEntries: number | undefined) {\n\tconst cacheLabel = name ? `${name}-${duration.durationLabel}` : duration.durationLabel;\n\treturn `odata-${cacheLabel}${maxEntries ? `-max${maxEntries}` : \"\"}`;\n}\n\nexport function parseSwCachePolicy(value: string | null | undefined): ParsedSwCachePolicy | undefined {\n\tconst normalized = value?.trim();\n\tif (!normalized) return undefined;\n\tif (normalized.toLowerCase() === \"off\") return { mode: \"off\" };\n\n\tconst parts = normalized\n\t\t.split(/[;,]/)\n\t\t.map((part) => part.trim())\n\t\t.filter(Boolean);\n\tconst [modePart, ...optionParts] = parts;\n\tconst [rawMode, rawDuration] = modePart?.split(\"=\", 2) ?? [];\n\tconst mode = rawMode?.toLowerCase();\n\tif ((mode !== \"ttl\" && mode !== \"bust\") || !rawDuration) return undefined;\n\n\tconst duration = parseDurationToken(rawDuration);\n\tif (!duration) return undefined;\n\n\tconst options = new Map(\n\t\toptionParts\n\t\t\t.map((part) => part.split(\"=\", 2).map((item) => item.trim()) as [string, string | undefined])\n\t\t\t.map(([key, optionValue]) => [key.toLowerCase(), optionValue] as [string, string | undefined])\n\t\t\t.filter(([key, optionValue]) => Boolean(key && optionValue))\n\t);\n\tconst maxEntries = parseMaxEntries(options.get(\"maxentries\") ?? options.get(\"max\") ?? options.get(\"entries\"));\n\tconst name = normalizeSwCacheName(options.get(\"cachename\") ?? options.get(\"cache\") ?? options.get(\"name\"));\n\tconst resolvedMaxEntries = maxEntries ?? DEFAULT_ODATA_CACHE_MAX_ENTRIES;\n\n\treturn {\n\t\tmode,\n\t\tamount: duration.amount,\n\t\tunit: duration.unit,\n\t\tname,\n\t\tmaxEntries,\n\t\tresolvedMaxEntries,\n\t\tttlMs: duration.ttlMs,\n\t\tcacheName: createCacheName(duration, name, maxEntries)\n\t};\n}\n\nexport function resolveSwCacheCacheNameByPolicy(value: string | undefined): string | undefined {\n\tconst parsed = parseSwCachePolicy(value);\n\treturn parsed?.mode === \"ttl\" || parsed?.mode === \"bust\" ? parsed.cacheName : undefined;\n}\n\nexport function buildSwCachePolicyValue(draft: SwCachePolicyDraft): string {\n\tif (draft.mode === \"off\") return \"off\";\n\n\tconst amount = typeof draft.amount === \"number\" && Number.isFinite(draft.amount) && draft.amount > 0 ? draft.amount : 24;\n\tconst unit = draft.unit ?? \"h\";\n\tconst maxEntries =\n\t\ttypeof draft.maxEntries === \"number\" && Number.isInteger(draft.maxEntries) && draft.maxEntries > 0\n\t\t\t? Math.min(draft.maxEntries, MAX_ODATA_CACHE_MAX_ENTRIES)\n\t\t\t: undefined;\n\tconst name = normalizeSwCacheName(draft.name);\n\tconst options = [maxEntries ? `max=${maxEntries}` : undefined, name ? `name=${name}` : undefined].filter(Boolean);\n\n\treturn `${draft.mode}=${amount}${unit}${options.length ? `;${options.join(\";\")}` : \"\"}`;\n}\n"],"mappings":";AAsBA,IACM,IAA8B;AAUpC,SAAgB,EAAqB,GAA+C;CAMnF,OALmB,GAChB,KAAK,CAAC,CACP,YAAY,CAAC,CACb,QAAQ,iBAAiB,GAAG,CAAC,CAC7B,QAAQ,YAAY,EAAE,KACH,KAAA;AACtB;AAEA,SAAS,EAAmB,GAAgD;CAC3E,IAAM,IAAa,EAAM,KAAK,CAAC,CAAC,YAAY;CAC5C,IAAI,MAAe,WAClB,OAAO;EACN,eAAe;EACf,OAAO;CACR;CAGD,IAAM,IAAQ,EAAW,MAAM,+BAA+B;CAC9D,IAAI,CAAC,GAAO;CAEZ,IAAM,IAAS,OAAO,EAAM,EAAE,GACxB,IAAO,EAAM;CACnB,IAAI,CAAC,OAAO,SAAS,CAAM,KAAK,KAAU,GAAG;CAS7C,IAAM,IAAQ,KAAK,MAAM,IAAS;EANjC,IAAI;EACJ,GAAG;EACH,GAAG
|
|
1
|
+
{"version":3,"file":"sw-cache-policy.js","names":[],"sources":["../../src/pwa/swCachePolicy.ts"],"sourcesContent":["export type SwCachePolicyMode = \"off\" | \"ttl\" | \"bust\";\nexport type SwCacheDurationUnit = \"ms\" | \"s\" | \"m\" | \"h\" | \"d\";\n\nexport interface ParsedSwCachePolicy {\n\tmode: SwCachePolicyMode;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tname?: string;\n\tmaxEntries?: number;\n\tresolvedMaxEntries?: number;\n\tttlMs?: number;\n\tcacheName?: string;\n}\n\nexport interface SwCachePolicyDraft {\n\tmode: SwCachePolicyMode;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tname?: string;\n\tmaxEntries?: number;\n}\n\nconst DEFAULT_ODATA_CACHE_MAX_ENTRIES = 100;\nconst MAX_ODATA_CACHE_MAX_ENTRIES = 5000;\nconst MAX_ODATA_CACHE_TTL_MS = 365 * 24 * 60 * 60 * 1000;\n\ntype DurationParseResult = {\n\tdurationLabel: string;\n\tamount?: number;\n\tunit?: SwCacheDurationUnit;\n\tttlMs: number;\n};\n\nexport function normalizeSwCacheName(value: string | undefined): string | undefined {\n\tconst normalized = value\n\t\t?.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\");\n\treturn normalized || undefined;\n}\n\nfunction parseDurationToken(value: string): DurationParseResult | undefined {\n\tconst normalized = value.trim().toLowerCase();\n\tif (normalized === \"forever\") {\n\t\treturn {\n\t\t\tdurationLabel: \"forever\",\n\t\t\tttlMs: Infinity\n\t\t};\n\t}\n\n\tconst match = normalized.match(/^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/);\n\tif (!match) return undefined;\n\n\tconst amount = Number(match[1]);\n\tconst unit = match[2] as SwCacheDurationUnit;\n\tif (!Number.isFinite(amount) || amount <= 0) return undefined;\n\n\tconst multiplierByUnit: Record<SwCacheDurationUnit, number> = {\n\t\tms: 1,\n\t\ts: 1000,\n\t\tm: 60 * 1000,\n\t\th: 60 * 60 * 1000,\n\t\td: 24 * 60 * 60 * 1000\n\t};\n\tconst ttlMs = Math.round(amount * multiplierByUnit[unit]);\n\tif (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_ODATA_CACHE_TTL_MS) return undefined;\n\n\tconst durationLabel = `${match[1]}${unit}`.replace(\".\", \"_\");\n\treturn {\n\t\tdurationLabel,\n\t\tamount,\n\t\tunit,\n\t\tttlMs\n\t};\n}\n\nfunction parseMaxEntries(value: string | undefined): number | undefined {\n\tif (!value) return undefined;\n\n\tconst parsed = Number(value.trim());\n\tif (!Number.isInteger(parsed) || parsed <= 0) return undefined;\n\n\treturn Math.min(parsed, MAX_ODATA_CACHE_MAX_ENTRIES);\n}\n\nfunction createCacheName(duration: DurationParseResult, name: string | undefined, maxEntries: number | undefined) {\n\tconst cacheLabel = name ? `${name}-${duration.durationLabel}` : duration.durationLabel;\n\treturn `odata-${cacheLabel}${maxEntries ? `-max${maxEntries}` : \"\"}`;\n}\n\nexport function parseSwCachePolicy(value: string | null | undefined): ParsedSwCachePolicy | undefined {\n\tconst normalized = value?.trim();\n\tif (!normalized) return undefined;\n\tif (normalized.toLowerCase() === \"off\") return { mode: \"off\" };\n\n\tconst parts = normalized\n\t\t.split(/[;,]/)\n\t\t.map((part) => part.trim())\n\t\t.filter(Boolean);\n\tconst [modePart, ...optionParts] = parts;\n\tconst [rawMode, rawDuration] = modePart?.split(\"=\", 2) ?? [];\n\tconst mode = rawMode?.toLowerCase();\n\tif ((mode !== \"ttl\" && mode !== \"bust\") || !rawDuration) return undefined;\n\n\tconst duration = parseDurationToken(rawDuration);\n\tif (!duration) return undefined;\n\n\tconst options = new Map(\n\t\toptionParts\n\t\t\t.map((part) => part.split(\"=\", 2).map((item) => item.trim()) as [string, string | undefined])\n\t\t\t.map(([key, optionValue]) => [key.toLowerCase(), optionValue] as [string, string | undefined])\n\t\t\t.filter(([key, optionValue]) => Boolean(key && optionValue))\n\t);\n\tconst maxEntries = parseMaxEntries(options.get(\"maxentries\") ?? options.get(\"max\") ?? options.get(\"entries\"));\n\tconst name = normalizeSwCacheName(options.get(\"cachename\") ?? options.get(\"cache\") ?? options.get(\"name\"));\n\tconst resolvedMaxEntries = maxEntries ?? DEFAULT_ODATA_CACHE_MAX_ENTRIES;\n\n\treturn {\n\t\tmode,\n\t\tamount: duration.amount,\n\t\tunit: duration.unit,\n\t\tname,\n\t\tmaxEntries,\n\t\tresolvedMaxEntries,\n\t\tttlMs: duration.ttlMs,\n\t\tcacheName: createCacheName(duration, name, maxEntries)\n\t};\n}\n\nexport function resolveSwCacheCacheNameByPolicy(value: string | undefined): string | undefined {\n\tconst parsed = parseSwCachePolicy(value);\n\treturn parsed?.mode === \"ttl\" || parsed?.mode === \"bust\" ? parsed.cacheName : undefined;\n}\n\nexport function buildSwCachePolicyValue(draft: SwCachePolicyDraft): string {\n\tif (draft.mode === \"off\") return \"off\";\n\n\tconst amount = typeof draft.amount === \"number\" && Number.isFinite(draft.amount) && draft.amount > 0 ? draft.amount : 24;\n\tconst unit = draft.unit ?? \"h\";\n\tconst maxEntries =\n\t\ttypeof draft.maxEntries === \"number\" && Number.isInteger(draft.maxEntries) && draft.maxEntries > 0\n\t\t\t? Math.min(draft.maxEntries, MAX_ODATA_CACHE_MAX_ENTRIES)\n\t\t\t: undefined;\n\tconst name = normalizeSwCacheName(draft.name);\n\tconst options = [maxEntries ? `max=${maxEntries}` : undefined, name ? `name=${name}` : undefined].filter(Boolean);\n\n\treturn `${draft.mode}=${amount}${unit}${options.length ? `;${options.join(\";\")}` : \"\"}`;\n}\n"],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ function u(e, r) {
|
|
|
20
20
|
queryClient: e,
|
|
21
21
|
broadcastChannel: r.queriesChannel
|
|
22
22
|
}), o = t(), s = new BroadcastChannel(r.eventsChannel), c = e.getQueryCache().subscribe((e) => {
|
|
23
|
-
i || e.type
|
|
23
|
+
i || e.type === "updated" && e.action.type === "invalidate" && l({
|
|
24
24
|
type: "invalidate",
|
|
25
25
|
keys: [e.query.queryKey],
|
|
26
26
|
refetchType: "none"
|
|
@@ -66,7 +66,7 @@ function d(t) {
|
|
|
66
66
|
return !e(t) || typeof t.tabId != "string" || !("event" in t) ? !1 : f(t.event);
|
|
67
67
|
}
|
|
68
68
|
function f(t) {
|
|
69
|
-
return e(t) ? t.type === "invalidate" ? Array.isArray(t.keys) ? t.refetchType === void 0
|
|
69
|
+
return e(t) ? t.type === "invalidate" ? Array.isArray(t.keys) ? t.refetchType === void 0 || t.refetchType === "none" || t.refetchType === "active" || t.refetchType === "inactive" || t.refetchType === "all" : !1 : t.type === "setQueryData" && "key" in t && "data" in t : !1;
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/query-client/queryPersistenceAdapter.ts
|
|
@@ -79,7 +79,7 @@ function h() {
|
|
|
79
79
|
}
|
|
80
80
|
//#endregion
|
|
81
81
|
//#region src/query-client/persistence.ts
|
|
82
|
-
var g = "queries", _ = __REACT_QUERY_PERSISTENCE_BUSTER__, v =
|
|
82
|
+
var g = "queries", _ = __REACT_QUERY_PERSISTENCE_BUSTER__, v = 7776e6;
|
|
83
83
|
function y(e) {
|
|
84
84
|
if (e) return e;
|
|
85
85
|
if (globalThis.indexedDB !== void 0) return globalThis.indexedDB;
|
|
@@ -171,8 +171,8 @@ function O({ onQueryError: e, onMutationError: t }) {
|
|
|
171
171
|
mutationCache: new i({ onError: t }),
|
|
172
172
|
defaultOptions: {
|
|
173
173
|
queries: {
|
|
174
|
-
staleTime:
|
|
175
|
-
gcTime:
|
|
174
|
+
staleTime: 6e5,
|
|
175
|
+
gcTime: 18e5,
|
|
176
176
|
throwOnError: !1,
|
|
177
177
|
retry: !1,
|
|
178
178
|
...n ? { persister: n.persisterFn } : {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/query-client/broadcast.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","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\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): AsyncStorage<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\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\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 * Создаёт singleton QueryClient (SPA).\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 * ### не использвать, только планируется реализация\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/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","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\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): AsyncStorage<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\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\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 * Создаёт singleton QueryClient (SPA).\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 * ### не использвать, только планируется реализация\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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"}
|
|
@@ -5,7 +5,7 @@ function t(t, n) {
|
|
|
5
5
|
let r = t.outputValueFallback;
|
|
6
6
|
if (!e(r)) return;
|
|
7
7
|
let i = n(r.start), a = n(r.end);
|
|
8
|
-
if (
|
|
8
|
+
if (i !== void 0 || a !== void 0) return {
|
|
9
9
|
...i !== void 0 && { start: i },
|
|
10
10
|
...a !== void 0 && { end: a }
|
|
11
11
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/range-output/rangeOutput.ts"],"sourcesContent":["import { isRecord } from \"../validators\";\n\nimport type { BaseType } from \"../types\";\n\nexport type RangeOutputValueFallback<TValue extends BaseType = BaseType> = {\n\tstart?: TValue | null;\n\tend?: TValue | null;\n};\n\nexport type RangeOutputFallbackEndpointParser<TValue extends BaseType> = (value: unknown) => TValue | null | undefined;\n\n/**\n * Читает сериализуемые подстановки открытых границ из объекта с `outputValueFallback`.\n */\nexport function readRangeOutputValueFallback<TValue extends BaseType>(\n\tprops: unknown,\n\tparseEndpoint: RangeOutputFallbackEndpointParser<TValue>\n): RangeOutputValueFallback<TValue> | undefined {\n\tif (!isRecord(props)) {\n\t\treturn undefined;\n\t}\n\n\tconst rawFallback = props.outputValueFallback;\n\tif (!isRecord(rawFallback)) {\n\t\treturn undefined;\n\t}\n\n\tconst start = parseEndpoint(rawFallback.start);\n\tconst end = parseEndpoint(rawFallback.end);\n\n\tif (start === undefined && end === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(start !== undefined && { start }),\n\t\t...(end !== undefined && { end })\n\t};\n}\n\nfunction resolveRangeOutputEndpoint<TValue extends BaseType>(value: TValue | null, fallback: TValue | null | undefined) {\n\tif (value !== null) {\n\t\treturn value;\n\t}\n\n\treturn fallback ?? null;\n}\n\n/**\n * Возвращает range-значение для внешней сериализации, не меняя исходный UI-контракт с `null`.\n */\nexport function resolveRangeOutputValue<TValue extends BaseType>(\n\tvalue: readonly [TValue | null, TValue | null],\n\tfallback: RangeOutputValueFallback<TValue> | undefined\n): [TValue | null, TValue | null] {\n\tif (!fallback) {\n\t\treturn [value[0], value[1]];\n\t}\n\n\treturn [resolveRangeOutputEndpoint(value[0], fallback.start), resolveRangeOutputEndpoint(value[1], fallback.end)];\n}\n"],"mappings":";;AAcA,SAAgB,EACf,GACA,GAC+C;CAC/C,IAAI,CAAC,EAAS,CAAK,GAClB;CAGD,IAAM,IAAc,EAAM;CAC1B,IAAI,CAAC,EAAS,CAAW,GACxB;CAGD,IAAM,IAAQ,EAAc,EAAY,KAAK,GACvC,IAAM,EAAc,EAAY,GAAG;CAErC,
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/range-output/rangeOutput.ts"],"sourcesContent":["import { isRecord } from \"../validators\";\n\nimport type { BaseType } from \"../types\";\n\nexport type RangeOutputValueFallback<TValue extends BaseType = BaseType> = {\n\tstart?: TValue | null;\n\tend?: TValue | null;\n};\n\nexport type RangeOutputFallbackEndpointParser<TValue extends BaseType> = (value: unknown) => TValue | null | undefined;\n\n/**\n * Читает сериализуемые подстановки открытых границ из объекта с `outputValueFallback`.\n */\nexport function readRangeOutputValueFallback<TValue extends BaseType>(\n\tprops: unknown,\n\tparseEndpoint: RangeOutputFallbackEndpointParser<TValue>\n): RangeOutputValueFallback<TValue> | undefined {\n\tif (!isRecord(props)) {\n\t\treturn undefined;\n\t}\n\n\tconst rawFallback = props.outputValueFallback;\n\tif (!isRecord(rawFallback)) {\n\t\treturn undefined;\n\t}\n\n\tconst start = parseEndpoint(rawFallback.start);\n\tconst end = parseEndpoint(rawFallback.end);\n\n\tif (start === undefined && end === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(start !== undefined && { start }),\n\t\t...(end !== undefined && { end })\n\t};\n}\n\nfunction resolveRangeOutputEndpoint<TValue extends BaseType>(value: TValue | null, fallback: TValue | null | undefined) {\n\tif (value !== null) {\n\t\treturn value;\n\t}\n\n\treturn fallback ?? null;\n}\n\n/**\n * Возвращает range-значение для внешней сериализации, не меняя исходный UI-контракт с `null`.\n */\nexport function resolveRangeOutputValue<TValue extends BaseType>(\n\tvalue: readonly [TValue | null, TValue | null],\n\tfallback: RangeOutputValueFallback<TValue> | undefined\n): [TValue | null, TValue | null] {\n\tif (!fallback) {\n\t\treturn [value[0], value[1]];\n\t}\n\n\treturn [resolveRangeOutputEndpoint(value[0], fallback.start), resolveRangeOutputEndpoint(value[1], fallback.end)];\n}\n"],"mappings":";;AAcA,SAAgB,EACf,GACA,GAC+C;CAC/C,IAAI,CAAC,EAAS,CAAK,GAClB;CAGD,IAAM,IAAc,EAAM;CAC1B,IAAI,CAAC,EAAS,CAAW,GACxB;CAGD,IAAM,IAAQ,EAAc,EAAY,KAAK,GACvC,IAAM,EAAc,EAAY,GAAG;CAErC,UAAU,KAAA,KAAa,MAAQ,KAAA,GAInC,OAAO;EACN,GAAI,MAAU,KAAA,KAAa,EAAE,SAAM;EACnC,GAAI,MAAQ,KAAA,KAAa,EAAE,OAAI;CAChC;AACD;AAEA,SAAS,EAAoD,GAAsB,GAAqC;CAKvH,OAJI,MAAU,OAIP,KAAY,OAHX;AAIT;AAKA,SAAgB,EACf,GACA,GACiC;CAKjC,OAJK,IAIE,CAAC,EAA2B,EAAM,IAAI,EAAS,KAAK,GAAG,EAA2B,EAAM,IAAI,EAAS,GAAG,CAAC,IAHxG,CAAC,EAAM,IAAI,EAAM,EAAE;AAI5B"}
|
package/dist/table/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as e, i as t, u as n } from "../chunks/array-
|
|
1
|
+
import { d as e, i as t, u as n } from "../chunks/array-D-_yMMUK.js";
|
|
2
2
|
import { useCallback as r, useEffect as i, useEffectEvent as a, useRef as o, useState as s } from "react";
|
|
3
3
|
//#region src/table/columnLayout.ts
|
|
4
4
|
function c({ ids: n, order: r }) {
|
|
@@ -73,7 +73,7 @@ function b(e) {
|
|
|
73
73
|
return e.role === "measure" ? "right" : "left";
|
|
74
74
|
}
|
|
75
75
|
function x(e, t) {
|
|
76
|
-
return e.semanticType !== "code" || !e.linkedColumnId
|
|
76
|
+
return e.semanticType !== "code" || !e.linkedColumnId || t.get(e.linkedColumnId)?.semanticType !== "text";
|
|
77
77
|
}
|
|
78
78
|
function S(e) {
|
|
79
79
|
if (typeof e.id == "string" && e.id.trim()) return e.id;
|
|
@@ -167,7 +167,7 @@ function k(e, t, n) {
|
|
|
167
167
|
function A({ availableRowIds: e, rowById: t, selectionMode: n, selectedRowIds: c, onRowSelectionChange: l }) {
|
|
168
168
|
let [u, d] = s({}), f = o(!1), p = r((e, t) => {
|
|
169
169
|
let n = Object.keys(e).filter((t) => e[t]), r = Object.keys(t).filter((e) => t[e]);
|
|
170
|
-
return n.length === r.length
|
|
170
|
+
return n.length === r.length && n.every((e) => t[e]);
|
|
171
171
|
}, []), m = a((e) => {
|
|
172
172
|
d((t) => O(t, e, n));
|
|
173
173
|
}), h = a((e, t) => {
|