PageSourceSearch

https://memorizun.com/legacy-migration.js?v=20260702-3

js memorizun.com collected 2026-09-24 14:47:52 UTC 8,813 bytes, 190 lines download raw bytes

1// legacy-migration.js
2// Сохранность данных существующих гостей старой системы.
3// Канал A: избранное (favorites/{sid}) → живые тексты гостя (guestStore).
4// Канал B: история (texts/{sid}) → скачиваемый .txt-архив («Вот ваши тексты»).
5// Реальные легаси-данные есть только на origin прода (memorizun.com); на бете
6// тестируется на засеянных фейк-данных. См. docs/superpowers/specs/2026-06-04-*.
7//
8// Чистые помощники экспортируются для Node-тестов; браузерная обвязка — под
9// `typeof window` (паттерн как в userState.js).
10
11const LEGACY_MIN_LEN = 30;          // короче — обрезок/мусор
12const LEGACY_MAX_CHARS = 50000;     // длиннее — выброс (был текст на 1.66 млн)
13const LEGACY_NORM_PREFIX = 300;     // длина ключа дедупа по содержимому
14
15function legacyNormalizeKey(content) {
16  return String(content == null ? '' : content).trim().slice(0, LEGACY_NORM_PREFIX);
17}
18
19function legacyIsAcceptable(content) {
20  const c = String(content == null ? '' : content).trim();
21  return c.length >= LEGACY_MIN_LEN && c.length <= LEGACY_MAX_CHARS;
22}
23
24function legacySavedAtToMs(savedAt) {
25  if (typeof savedAt === 'number' && isFinite(savedAt)) return savedAt;
26  if (typeof savedAt === 'string') {
27    const t = Date.parse(savedAt);
28    if (!isNaN(t)) return t;
29  }
30  return null;
31}
32
33function legacyRecordsFromNode(node) {
34  if (!node || typeof node !== 'object' || Array.isArray(node)) return [];
35  return Object.values(node).filter(r => r && typeof r === 'object');
36}
37
38// План мержа избранного в живые тексты.
39// favNode: RTDB-нода favorites/{sid}; existingTexts: тексты guestStore (с .text).
40// → массив {text, createdAt} к сохранению: дедуп против existing и внутри,
41//   сортировка по возрастанию savedAt (без даты — в конец).
42function legacyPlanFavoritesMerge(favNode, existingTexts) {
43  const seen = new Set();
44  (existingTexts || []).forEach(t => {
45    const c = t && (t.text != null ? t.text : t.content);
46    if (c) seen.add(legacyNormalizeKey(c));
47  });
48  const recs = legacyRecordsFromNode(favNode)
49    .map(r => ({ content: r.content != null ? r.content : r.text, savedAt: r.savedAt }))
50    .filter(r => legacyIsAcceptable(r.content));
51  recs.sort((a, b) => {
52    const ta = legacySavedAtToMs(a.savedAt), tb = legacySavedAtToMs(b.savedAt);
53    return (ta == null ? Infinity : ta) - (tb == null ? Infinity : tb);
54  });
55  const out = [];
56  for (const r of recs) {
57    const key = legacyNormalizeKey(r.content);
58    if (seen.has(key)) continue;
59    seen.add(key);
60    out.push({ text: String(r.content).trim(), createdAt: legacySavedAtToMs(r.savedAt) });
61  }
62  return out;
63}
64
65// Имя файла для одного текста в ZIP: "NN-<первая строка, очищенная>.txt".
66function legacyExportFilename(content, index) {
67  const firstLine = String(content == null ? '' : content).trim().split('\n')[0] || '';
68  const base = firstLine.slice(0, 40)
69    .replace(/[\/\\:*?"<>|]+/g, ' ')   // запрещённые в имени файла → пробел
70    .replace(/\s+/g, ' ')
71    .trim();
72  const num = String(index + 1).padStart(2, '0');
73  return `${num}-${base || 'text'}.txt`;
74}
75
76// План экспорта «всех текстов» в отдельные файлы.
77// current: текущие тексты (с .text/.content) — реальные тексты юзера, отсев только пустых.
78// textsNode/favNode: легаси-ноды RTDB — отсев <30/>50000, сортировка по убыванию savedAt.
79// Дедуп: current приоритетнее, легаси добавляется без дублей. → [{filename, content}].
80function legacyPlanExportFiles(current, textsNode, favNode) {
81  const seen = new Set();
82  const contents = [];
83  (current || []).forEach(t => {
84    const raw = t && (t.text != null ? t.text : t.content);
85    const s = String(raw == null ? '' : raw).trim();
86    if (!s) return;
87    const key = legacyNormalizeKey(s);
88    if (seen.has(key)) return;
89    seen.add(key);
90    contents.push(s);
91  });
92  const legacy = legacyRecordsFromNode(textsNode)
93    .concat(legacyRecordsFromNode(favNode))
94    .map(r => ({ content: r.content != null ? r.content : r.text, savedAt: r.savedAt }))
95    .filter(r => legacyIsAcceptable(r.content));
96  legacy.sort((a, b) => {
97    const ta = legacySavedAtToMs(a.savedAt), tb = legacySavedAtToMs(b.savedAt);
98    return (tb == null ? -Infinity : tb) - (ta == null ? -Infinity : ta);
99  });
100  legacy.forEach(r => {
101    const s = String(r.content).trim();
102    const key = legacyNormalizeKey(s);
103    if (seen.has(key)) return;
104    seen.add(key);
105    contents.push(s);
106  });
107  return contents.map((content, i) => ({ filename: legacyExportFilename(content, i), content }));
108}
109
110const legacyMigrationApi = {
111  LEGACY_MIN_LEN, LEGACY_MAX_CHARS, LEGACY_NORM_PREFIX,
112  legacyNormalizeKey, legacyIsAcceptable, legacySavedAtToMs, legacyRecordsFromNode,
113  legacyPlanFavoritesMerge, legacyExportFilename, legacyPlanExportFiles,
114};
115
116if (typeof module !== 'undefined' && module.exports) {
117  module.exports = legacyMigrationApi;
118}
119
120// ===== Браузерная обвязка =====
121if (typeof window !== 'undefined') {
122  const FLAG_CHECKED = 'memorizun_legacy_checked'; // канал A (перенос избранного) отработал
123
124  async function readNode(path) {
125    if (typeof firebase === 'undefined' || !firebase.database) return null;
126    const snap = await firebase.database().ref(path).once('value');
127    return snap.val();
128  }
129
130  // Канал A: избранное (favorites/{sid}) → живые тексты гостя. Одноразово (FLAG_CHECKED).
131  async function runOnce() {
132    try {
133      if (localStorage.getItem(FLAG_CHECKED)) return;
134      const sid = localStorage.getItem('memorizunSessionId');
135      if (!sid) { localStorage.setItem(FLAG_CHECKED, '1'); return; }
136      const favNode = await readNode('favorites/' + sid);
137      const existing = window.guestStore ? window.guestStore.loadTexts() : [];
138      const toSave = legacyPlanFavoritesMerge(favNode, existing);
139      toSave.forEach(rec => window.guestStore && window.guestStore.saveText(rec));
140      if (toSave.length) console.log('📥 Легаси избранное → тексты:', toSave.length);
141      localStorage.setItem(FLAG_CHECKED, '1');
142    } catch (e) {
143      console.warn('⚠️ Перенос легаси-избранного не удался (не критично):', (e && e.message) || e);
144      localStorage.setItem(FLAG_CHECKED, '1');
145    }
146  }
147
148  // Постоянная кнопка: «Скачать все мои тексты» → ZIP из отдельных .txt.
149  // Текущие тексты (любой провайдер) + легаси по sessionId. Для всех юзеров.
150  async function downloadAllTexts() {
151    try {
152      const current = (window.historySidebar && window.historySidebar.texts) || [];
153      const sid = localStorage.getItem('memorizunSessionId');
154      let favNode = null, textsNode = null;
155      if (sid) {
156        [favNode, textsNode] = await Promise.all([
157          readNode('favorites/' + sid),
158          readNode('texts/' + sid),
159        ]);
160      }
161      const files = legacyPlanExportFiles(current, textsNode, favNode);
162      if (!files.length) {
163        console.log('ℹ️ Нет текстов для скачивания');
164        return;
165      }
166      if (typeof JSZip === 'undefined') {
167        console.warn('⚠️ JSZip не загружен — скачивание недоступно');
168        return;
169      }
170      const zip = new JSZip();
171      files.forEach(f => zip.file(f.filename, f.content));
172      const blob = await zip.generateAsync({ type: 'blob' });
173      const url = URL.createObjectURL(blob);
174      const a = document.createElement('a');
175      a.href = url;
176      a.download = 'memorizun-texts.zip';
177      document.body.appendChild(a);
178      a.click();
179      document.body.removeChild(a);
180      URL.revokeObjectURL(url);
181      console.log('💾 Скачано текстов:', files.length);
182      if (window.track) window.track('download_all_texts', { count: files.length });
183    } catch (e) {
184      console.warn('⚠️ Скачивание не удалось:', (e && e.message) || e);
185    }
186  }
187
188  window.legacyMigration = { runOnce, downloadAllTexts };
189  console.log('✅ legacyMigration инициализирован');
190}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.