OASIS_MOBILE/src/backend/dedupe.js
oasis-bot c332afd6ea upstream: integrar Oasis 1.3.1
Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.

Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).

Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
2026-10-18 02:52:17 +02:00

127 lines
4.4 KiB
JavaScript

const norm = (v) => String(v == null ? '' : v).trim().toLowerCase();
const dedupeBy = (items, keyFn) => {
const seen = new Set();
const out = [];
for (const it of (Array.isArray(items) ? items : [])) {
let k = null;
try { k = keyFn(it); } catch (_) { k = null; }
if (k == null || k === '') { out.push(it); continue; }
if (seen.has(k)) continue;
seen.add(k);
out.push(it);
}
return out;
};
const mergeDuplicatesBy = (items, keyFn, mergeFn) => {
const slots = new Map();
const out = [];
for (const it of (Array.isArray(items) ? items : [])) {
let k = null;
try { k = keyFn(it); } catch (_) { k = null; }
if (k == null || k === '') { out.push(it); continue; }
if (!slots.has(k)) {
const slot = { i: out.length, v: it };
slots.set(k, slot);
out.push(it);
} else {
const slot = slots.get(k);
slot.v = mergeFn(slot.v, it);
out[slot.i] = slot.v;
}
}
return out;
};
const dedupeByPreferring = (items, keyFn, scoreFn) => {
const best = new Map();
const order = [];
for (const it of (Array.isArray(items) ? items : [])) {
let k = null;
try { k = keyFn(it); } catch (_) { k = null; }
if (k == null || k === '') { order.push({ solo: it }); continue; }
let s = 0;
try { s = Number(scoreFn(it)) || 0; } catch (_) { s = 0; }
if (!best.has(k)) { const slot = { v: it, s }; best.set(k, slot); order.push({ slot }); }
else { const slot = best.get(k); if (s > slot.s) { slot.v = it; slot.s = s; } }
}
return order.map(o => (o.solo !== undefined ? o.solo : o.slot.v));
};
const versionGroups = (entries) => {
const byKey = new Map();
for (const e of (entries || [])) if (e && e.key) byKey.set(e.key, e);
const memo = new Map();
const rootOf = (key) => {
if (memo.has(key)) return memo.get(key);
const path = [];
const seen = new Set([key]);
let cur = key;
let root = null;
while (root === null) {
if (memo.has(cur)) { root = memo.get(cur); break; }
const e = byKey.get(cur);
const p = e && e.replaces;
if (!p || !byKey.has(p) || seen.has(p)) { root = cur; break; }
path.push(cur);
seen.add(p);
cur = p;
}
memo.set(cur, root);
for (const k of path) memo.set(k, root);
return root;
};
const isOrphan = (key) => {
const e = byKey.get(key);
return !!(e && e.replaces && !byKey.has(e.replaces));
};
const isForeignCopy = (key) => {
const e = byKey.get(key);
return !!(e && isOrphan(key) && typeof e.claimedAuthor === 'string' && e.claimedAuthor && e.claimedAuthor !== e.author);
};
const groups = new Map();
for (const k of byKey.keys()) {
const r = rootOf(k);
if (!groups.has(r)) groups.set(r, []);
groups.get(r).push(k);
}
const later = (a, b) => (a.seq || 0) > (b.seq || 0) || ((a.seq || 0) === (b.seq || 0) && (a.ts || 0) >= (b.ts || 0));
for (const [r, keys] of Array.from(groups.entries())) {
if (!isForeignCopy(r)) continue;
const owner = byKey.get(r).claimedAuthor;
let first = null;
for (const k of keys) { const e = byKey.get(k); if (e.author === owner && (!first || later(byKey.get(first), e))) first = k; }
if (!first) continue;
groups.delete(r);
groups.set(first, keys);
for (const k of keys) memo.set(k, first);
}
const ownerTip = new Map();
for (const [r, keys] of groups) {
const re = byKey.get(r);
for (const k of keys) {
const e = byKey.get(k);
if (!re || e.author !== re.author) continue;
const prev = ownerTip.has(r) ? byKey.get(ownerTip.get(r)) : null;
if (!prev || later(e, prev)) ownerTip.set(r, k);
}
}
const isForeign = (key) => {
const e = byKey.get(key);
const re = byKey.get(rootOf(key));
return !!(e && re && e.author !== re.author);
};
const roots = new Set();
for (const r of groups.keys()) if (!isForeignCopy(r)) roots.add(r);
const tipOf = (key) => { const r = rootOf(key); return ownerTip.get(r) || r; };
return { rootOf, tipOf, roots, groups, isOrphan, isForeign, isForeignCopy };
};
const versionEntry = (m) => {
const v = (m && m.value) || {};
const c = v.content || {};
return { key: m && m.key, author: v.author, seq: v.sequence || 0, ts: v.timestamp || (m && m.timestamp) || 0, replaces: typeof c.replaces === 'string' ? c.replaces : null, claimedAuthor: typeof c.author === 'string' ? c.author : null };
};
module.exports = { dedupeBy, mergeDuplicatesBy, dedupeByPreferring, norm, versionGroups, versionEntry };