itube-specs 0.0.800 → 0.0.802

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,11 @@
1
1
  import type { Language } from '../../runtime';
2
- import { AsyncData, convertString } from '../../runtime';
2
+ import { AsyncData } from '../../runtime';
3
3
  import type { IVideoData } from '../../types';
4
+ import { resolveReferer } from '../../utils/use-fetch-video/resolve-referer';
4
5
 
5
- // Роут-источник (без суффикса локали) тип referer'а и параметр роута со значением.
6
- // Расширяется добавлением строки: позже сюда лягут model, channel и т.д.
7
- const REFERER_BY_ROUTE: Record<string, { type: string; param: string }> = {
8
- 'categories-slug': { type: 'category', param: 'slug' },
9
- 'tag-tag': { type: 'tag', param: 'tag' },
10
- 'models-slug-tag-tag': { type: 'tag', param: 'tag' },
11
- 'search-slug': { type: 'search', param: 'slug' },
12
- 'search-slug-videos': { type: 'search', param: 'slug' },
13
- 'search-slug-models': { type: 'search', param: 'slug' },
14
- };
15
-
16
- // Debug-override слоя: добавлен ?view — id/url ролика, на который переключились
17
- // через плеер (next / автопереход). Пока он есть и меняется, useAsyncData
18
- // подсасывает свежие данные этого ролика, не трогая path (landing остаётся в URL).
6
+ // ?view id/url ролика, на который переключились через плеер (next / автопереход).
7
+ // Пока он есть и меняется, useAsyncData подсасывает свежие данные этого ролика,
8
+ // не трогая path (landing остаётся в URL).
19
9
  export const useFetchVideo = async (
20
10
  apiMethod: (...args: any[]) => Promise<IVideoData>,
21
11
  ) => {
@@ -25,26 +15,43 @@ export const useFetchVideo = async (
25
15
  const router = useRouter();
26
16
  const key = AsyncData.Video;
27
17
 
28
- // referer вычисляем из предыдущей записи History API (нативно, без middleware).
29
- // Есть только при клиентской навигации; при прямом заходе/рефреше back пустой referer не шлём.
30
- const referer = computed(() => {
18
+ // Cookie-зеркало referer'а: единственное клиент-записываемое хранилище, которое уходит
19
+ // на сервер с запросом при reload доступно во время SSR-фетча (sessionStorage/localStorage
20
+ // клиентские, до SSR-фетча недоступны). Session-cookie (без maxAge/expires) семантике
21
+ // сессии просмотра. sameSite: 'lax' — referer нужен только на своих переходах.
22
+ const refererCookie = useCookie<{ type: string; value: string } | null>('video-referer', {
23
+ path: '/',
24
+ sameSite: 'lax',
25
+ });
26
+
27
+ // Разбор предыдущей записи History API (нативно, без middleware). Три исхода:
28
+ // - navigated:false — back нет: reload/прямой заход. referer из history неизвестен.
29
+ // На сервере back пуст ВСЕГДА → SSR-фетч при reload попадает сюда и падает на cookie.
30
+ // - navigated:true, referer:{...} — пришли из листинга (категория/тег/поиск): карточка-источник.
31
+ // - navigated:true, referer:null — навигация была, но не из листинга (video→video related,
32
+ // главная и т.п.): исходной карточки нет → дефолтный плейсхолдер, cookie сбрасываем.
33
+ const historyState = computed(() => {
31
34
  const back = router.options.history.state.back;
32
- if (!back || typeof back !== 'string') return null;
35
+ if (!back || typeof back !== 'string') return { navigated: false, referer: null };
33
36
 
34
37
  const from = router.resolve(back);
35
- const name = String(from.name || '').replace(/___[a-zA-Z-]+$/, '');
36
- const rule = REFERER_BY_ROUTE[name];
37
- if (!rule) return null;
38
+ return { navigated: true, referer: resolveReferer(String(from.name || ''), from.params) };
39
+ });
38
40
 
39
- const raw = String(from.params[rule.param] || '');
40
- if (!raw) return null;
41
+ // Была навигация → берём вычисленный из history referer (в т.ч. дефолт при video→video).
42
+ // Не было (reload/прямой заход) падаем на cookie от прошлого перехода.
43
+ const referer = computed(() =>
44
+ (historyState.value.navigated ? historyState.value.referer : refererCookie.value) ?? null,
45
+ );
41
46
 
42
- // Декодируем так же, как useSlug бек принимает referer_value в том же виде,
43
- // что и slug в остальных методах (напр. useFetchCategoriesByName).
44
- const decoded = convertString().fromSlug(raw);
45
- const value = String(Array.isArray(decoded) ? decoded[0] : decoded);
46
- return value ? { type: rule.type, value } : null;
47
- });
47
+ // Синхронизируем cookie ТОЛЬКО при реальной навигации: из листинга пишем карточку-источник,
48
+ // из не-листинга сбрасываем в дефолт, чтобы последующий reload не унаследовал чужую тумбу.
49
+ // Пишем только на клиенте — на сервере cookie read-only.
50
+ if (import.meta.client) {
51
+ watch(historyState, (state) => {
52
+ if (state.navigated) refererCookie.value = state.referer;
53
+ }, { immediate: true });
54
+ }
48
55
 
49
56
  // view кладётся тем же ключом, что и основной фетч (url при FetchByUrlEnabled,
50
57
  // иначе id) — поэтому тот же apiMethod принимает и его без переключений.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.800",
4
+ "version": "0.0.802",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -0,0 +1,9 @@
1
+ import type { Niche } from '../runtime';
2
+
3
+ /** Хелпер для работы с нишами */
4
+ export class NicheHelper {
5
+ /** Метод для получения дефолтной ниши сайта из окружения сайта */
6
+ public static getSiteDefaultNiche(): Niche {
7
+ return useRuntimeConfig()?.public.defaultNiche as Niche;
8
+ }
9
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveReferer } from '../resolve-referer';
3
+
4
+ describe('resolveReferer', () => {
5
+ it('категория → referer типа category с декодированным slug', () => {
6
+ expect(resolveReferer('categories-slug', { slug: 'big-tits' }))
7
+ .toEqual({ type: 'category', value: 'big tits' });
8
+ });
9
+
10
+ it('тег → referer типа tag', () => {
11
+ expect(resolveReferer('tag-tag', { tag: 'anal' }))
12
+ .toEqual({ type: 'tag', value: 'anal' });
13
+ });
14
+
15
+ it('тег внутри модели → referer типа tag', () => {
16
+ expect(resolveReferer('models-slug-tag-tag', { slug: 'john-doe', tag: 'solo' }))
17
+ .toEqual({ type: 'tag', value: 'solo' });
18
+ });
19
+
20
+ it('поиск → referer типа search', () => {
21
+ expect(resolveReferer('search-slug', { slug: 'gay-porn' }))
22
+ .toEqual({ type: 'search', value: 'gay porn' });
23
+ });
24
+
25
+ it('вкладки поиска (videos/models) → тот же search-referer', () => {
26
+ expect(resolveReferer('search-slug-videos', { slug: 'twink' }))
27
+ .toEqual({ type: 'search', value: 'twink' });
28
+ expect(resolveReferer('search-slug-models', { slug: 'twink' }))
29
+ .toEqual({ type: 'search', value: 'twink' });
30
+ });
31
+
32
+ it('снимает суффикс локали перед лукапом', () => {
33
+ expect(resolveReferer('categories-slug___zh-hans', { slug: 'bdsm' }))
34
+ .toEqual({ type: 'category', value: 'bdsm' });
35
+ });
36
+
37
+ it('декодирует подчёркивания и тильды как useSlug (fromSlug)', () => {
38
+ expect(resolveReferer('categories-slug', { slug: 'a_b~c' }))
39
+ .toEqual({ type: 'category', value: 'a-b_c' });
40
+ });
41
+
42
+ it('роут не из листинга (video→video) → null', () => {
43
+ expect(resolveReferer('videos-slug', { slug: 'some-video' })).toBeNull();
44
+ });
45
+
46
+ it('главная → null', () => {
47
+ expect(resolveReferer('index', {})).toBeNull();
48
+ });
49
+
50
+ it('листинговый роут без нужного параметра → null', () => {
51
+ expect(resolveReferer('categories-slug', {})).toBeNull();
52
+ });
53
+
54
+ it('пустое значение параметра → null', () => {
55
+ expect(resolveReferer('tag-tag', { tag: '' })).toBeNull();
56
+ });
57
+ });
@@ -0,0 +1,42 @@
1
+ import { convertString } from '../../runtime';
2
+
3
+ export interface Referer {
4
+ type: string;
5
+ value: string;
6
+ }
7
+
8
+ // Роут-источник (без суффикса локали) → тип referer'а и параметр роута со значением.
9
+ // Расширяется добавлением строки: позже сюда лягут model, channel и т.д.
10
+ export const REFERER_BY_ROUTE: Record<string, { type: string; param: string }> = {
11
+ 'categories-slug': { type: 'category', param: 'slug' },
12
+ 'tag-tag': { type: 'tag', param: 'tag' },
13
+ 'models-slug-tag-tag': { type: 'tag', param: 'tag' },
14
+ 'search-slug': { type: 'search', param: 'slug' },
15
+ 'search-slug-videos': { type: 'search', param: 'slug' },
16
+ 'search-slug-models': { type: 'search', param: 'slug' },
17
+ };
18
+
19
+ /**
20
+ * Сопоставляет предыдущий роут (имя + параметры из History API) карточке-источнику ролика.
21
+ * Возвращает referer, если пришли из листинга (категория/тег/поиск); иначе null — навигация
22
+ * была не из листинга (video→video, главная и т.п.), исходной карточки нет → дефолтный плейсхолдер.
23
+ * @param routeName - имя предыдущего роута (возможно с суффиксом локали `___xx`)
24
+ * @param params - параметры предыдущего роута
25
+ */
26
+ export function resolveReferer(
27
+ routeName: string,
28
+ params: Record<string, unknown>,
29
+ ): Referer | null {
30
+ const name = routeName.replace(/___[a-zA-Z-]+$/, '');
31
+ const rule = REFERER_BY_ROUTE[name];
32
+ if (!rule) return null;
33
+
34
+ const raw = String(params[rule.param] || '');
35
+ if (!raw) return null;
36
+
37
+ // Декодируем так же, как useSlug — бек принимает referer_value в том же виде,
38
+ // что и slug в остальных методах (напр. useFetchCategoriesByName).
39
+ const decoded = convertString().fromSlug(raw);
40
+ const value = String(Array.isArray(decoded) ? decoded[0] : decoded);
41
+ return value ? { type: rule.type, value } : null;
42
+ }