import type { EntityID, MappedThread, Thread, ThreadOnlyCurrentNoDeleted } from '@wovin/core' import type { IObservableArray } from '@wovin/core/mobx' import type { TiptapContent } from './VMs/TypeMap' import { createDebugName, isEncryptedApplog, joinThreads, lastWriteWins, observableArrayMap, query, queryAndMap, removeDuplicateAppLogs, rollingFilter, withoutDeleted, } from '@wovin/core' import { autorun, comparer, toJS } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import escapeStringRegexp from 'escape-string-regexp' import stringify from 'safe-stable-stringify' import { BLOCK_DEF, REL_DEF } from './data-types' import { RE_ANY_TAG_ONLY, RE_TAG_CONTEXT_POST, RE_TAG_CONTEXT_PRE, TIPTAP_EMPTY, } from './note3-regex-constants' import { contentVlToPlaintext, parseBlockContentValue, plainContentMatchTagGeneric, } from './note3-utils-nodeps' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export function mapAndRecurseKids( thread: ThreadOnlyCurrentNoDeleted, blockID: EntityID, mapKid: (kid: { relID: EntityID, blockID: EntityID }) => R, joinResults: (blockResult: R, kidResults: R[]) => R, ) { const recurse = function recurseOuter(blockID: EntityID, relID: EntityID, trace: EntityID[]) { // Recursion checks if (trace.includes(blockID)) { ERROR(`Kids loop`, { blockID, _relID: relID, _trace: trace }, /* , 'fixIt:', function fixIt() { insertApplogsInAppDB({ en: relID, at: ENTITY_DEF.isDeleted, vl: true }) } */) throw new Error(`[recursiveKids.loop] time travel?`) } if (trace.length > 42) { ERROR(`Depth limit reached`, trace) throw new Error(`[recursiveKidsCount.maxDepth] How deep can you go?`) // ? is 42 the answers.length) } // Process this block const result = mapKid({ blockID, relID }) // Process the children const kidsQuery = query(thread, [ { en: '?kidRelID', at: 'relation/childOf', vl: blockID }, { en: '?kidRelID', at: 'relation/block', vl: '?kidID' }, ]) const kidResults = observableArrayMap(function obsArrMapperForKidResults() { // TODO avoid expensive createDebugName if possible return kidsQuery.records.map(function kidsQueryRecordsInnerMapper({ kidID, kidRelID }) { return recurse(kidID as EntityID, kidRelID as EntityID, [...trace, blockID]) }) }, { name: createDebugName({ caller: `mapAndRecurseKids.kidResults`, thread, args: { blockID } }) }) return joinResults(result, kidResults) } return recurse(blockID, null, []) } export function getBlocksWithTags(thread: ThreadOnlyCurrentNoDeleted, tags: readonly string[]) { if (!tags.length) throw new Error(`[useBlocksWithTags] Empty tags list`) tags.forEach((tag) => { if (!RE_ANY_TAG_ONLY.exec(tag)) throw new Error(`[useBlocksWithTags] Tag too short: '${tag}'`) }) const blocks = queryAndMap(thread, [{ en: '?blockID', at: 'block/content', vl: vl => tags.every((tag) => { return !!plainContentMatchSpecificTag(contentVlToPlaintext(vl as string), tag) }), }], 'blockID') as IObservableArray// TODO: serializable REGEXSearchContext. if (DEBUG.isEnabled) { autorun(() => { DEBUG(`[useBlocksWithTags] updated`, toJS(blocks)) }) } return blocks } export function blockThreadWithRecursiveKids(thread: Thread, blockID: EntityID): MappedThread { const currentThread = withoutDeleted(lastWriteWins(thread)) as ThreadOnlyCurrentNoDeleted // HACK: how to do actual withHistory? const mappedKids = mapAndRecurseKids(currentThread, blockID, ({ blockID, relID }) => { const blockLogsWithHistory = query(thread, [ { en: blockID, at: BLOCK_DEF._attrsFull }, ]) if (blockLogsWithHistory.isEmpty) { WARN(`threadRecurse encountered missing block - skipping`, { blockID, relID }) return null } let relLogsWithHistory if (relID) { relLogsWithHistory = query(thread, [ { en: relID, at: REL_DEF._attrsFull }, ]) if (relLogsWithHistory.isEmpty) { throw ERROR(`threadRecurse encountered missing relation (how did we get here?!)`, { blockID, relID }) } } if (!relID) return blockLogsWithHistory.threadOfAllTrails return joinThreads([ blockLogsWithHistory.threadOfAllTrails, relLogsWithHistory.threadOfAllTrails, ]) }, (blockThread, kidThreads) => { return joinThreads(observableArrayMap(() => [...(blockThread ? [blockThread] : []), ...kidThreads])) }) DEBUG('afterMappingKids', { mappedKids }) return mappedKids } export const TIPTAP_EMPTY_SERIALIZED = serializeTiptapToVl(TIPTAP_EMPTY) export function compareBlockContent(contentA: TiptapContent, contentB) { if (comparer.structural(contentA, contentB)) return true if (comparer.structural(contentA, TIPTAP_EMPTY) && !contentB) return true // HACK suppress setting initial content if anyways nothing set yet return false } export function serializeTiptapToVl(tiptap: TiptapContent) { return stringify(tiptap) } export function onlyFromAgent( thread: Thread, ag: string, ) { return rollingFilter(thread, { ag }) } export function sanityCheckLogs(applogs: any) { DEBUG('sanityCheckLogs', { applogs }) return removeDuplicateAppLogs(applogs.map((log) => { if (isEncryptedApplog(log)) { VERBOSE('encrypted log', log) return log } return { ...log, pv: log.pv ?? null } // HACK: tolerate old logs without pv })) } export function replaceInTiptap(content: TiptapContent, needle: string | RegExp, replace: string) { // HACK json replace... const newContent = serializeTiptapToVl(content).replace(needle, replace) return parseBlockContentValue(newContent) } export function getRegexForTagInContext(tag: string, flags = ''): RegExp { return new RegExp(`${RE_TAG_CONTEXT_PRE.source}(${escapeStringRegexp(tag)})${RE_TAG_CONTEXT_POST.source}`, flags) } export function plainContentMatchSpecificTag(content: string, tag: string) { return plainContentMatchTagGeneric( getRegexForTagInContext(tag), content, ) !== null // returns empty string on match }