import type { ApplogForInsert, EntityID } from '@wovin/core/applog' import type { RelationModelDef } from './Relations' import type { RelationVM } from './VMs/RelationVM' import { Logger } from 'besonders-logger' import { insertApplogsInAppDB } from './ApplogDB' import { REL } from './VMs/RelationVM' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.DEBUG) /** Get the timestamp of when a relation was added to its parent (relation/childOf applog) */ function getRelationChildOfTs(rel: RelationVM): string | undefined { return rel.entityThread?.applogs?.findLast(l => l.at === 'relation/childOf')?.ts } export function orderBlockRelations(originalList: readonly RelationVM[]) { if (!originalList?.length) return [] const list = originalList.filter(eachRel => !eachRel.isDeleted) // sort by CID to make it (more) deterministic list.sort((a, b) => (a.en < b.en) ? -1 : 1) const uniqueParents = new Set(list.map(eachRel => eachRel.childOf)) if (uniqueParents.size != 1) throw ERROR(`[orderBlockRelations] Unexpected parent count:`, uniqueParents.size, { uniqueParents, list }) const parentID = uniqueParents.values().next().value const orderedRelationIDs: EntityID[] = [] // Track insertion position for relations without 'after' - they should be placed in timestamp order let noAfterInsertPos = 0 const relIDMap = new Map() for (const rel of list) { relIDMap.set(rel.en, rel) } const blockToRel = new Map() for (const rel of list) { blockToRel.set(rel.block, rel) } function placeRecursively(rel: RelationVM, trace: EntityID[] = []) { if (trace.includes(rel.en)) { ERROR(`Relation loop:`, { rel, trace, list }) ERROR(`Relation loop error: ${trace.join('→')}`) list.splice(list.indexOf(rel), 1) return // exclude from the list } trace.push(rel.en) if (!rel.after) { // Insert at current position and increment for next no-after relation // (no-after relations are processed in timestamp order, so this maintains that order) orderedRelationIDs.splice(noAfterInsertPos++, 0, rel.en) } else { const relWeAreAfter = blockToRel.get(rel.after) if (!relWeAreAfter) { // WARN('[sort] unknown `after`:', rel, { blockToRel }) orderedRelationIDs.push(rel.en) // add to bottom } else { if (!orderedRelationIDs.includes(relWeAreAfter?.en)) { placeRecursively(relWeAreAfter, trace) } const posOfNodeWeAreAfter = orderedRelationIDs.indexOf(relWeAreAfter?.en) if (posOfNodeWeAreAfter === -1) { ERROR(`Did not find posOfNodeWeAreAfter after placing ${relWeAreAfter.en}`) // e.g. when there is a relation loop orderedRelationIDs.push(rel.en) // just slather it onto the end } else { // We found our position 🎉 orderedRelationIDs.splice(posOfNodeWeAreAfter + 1, 0, rel.en) } } } list.splice(list.indexOf(rel), 1) // remove from 'to be placed' list } // First, process relations without 'after' in timestamp order (oldest first) // This ensures they are placed at the beginning in creation order const noAfterRels = list.filter(rel => !rel.after) noAfterRels.sort((a, b) => { const tsA = getRelationChildOfTs(a) ?? '' const tsB = getRelationChildOfTs(b) ?? '' return tsA.localeCompare(tsB) // ascending order (oldest first) }) for (const rel of noAfterRels) { if (list.includes(rel)) { // might have been removed if it was part of a dependency chain placeRecursively(rel) } } // Then process remaining relations (those with 'after' set) while (list.length) { const toPlace = list[0] placeRecursively(toPlace) // VERBOSE('[sort] place iteration done, parent:',toPlace.childOf,'block:',toPlace.block, // JSON.parse(JSON.stringify({ toPlace,list,relationIDs: orderedRelationIDs, })), ) } const sortedList: RelationVM[] = [] for (const relID of orderedRelationIDs) { sortedList.push(relIDMap.get(relID)!) } if (sortedList.length) { VERBOSE(`[orderRelations#${parentID}]`, sortedList[0].childOf, { list, sortedList, relationIDs: orderedRelationIDs, }) } // Sanity check const positionDiffThanAfter = sortedList.map((rel, index) => { if (index === 0) { return rel.after === null ? false : { rel, index, after: rel.after, actuallyAfter: null } } const relBefore = sortedList[index - 1] return rel.after === relBefore?.block ? false : { index, after: rel.after, rel, relBefore, actuallyAfter: relBefore?.block } }).filter(p => p !== false) if (positionDiffThanAfter.length) { function fixIt() { insertApplogsInAppDB(positionDiffThanAfter.map(({ rel, actuallyAfter }) => ( { en: rel.en, at: REL.after, vl: actuallyAfter } ))) } WARN( `[orderRelations#${parentID}] positions do not align with their 'after':`, positionDiffThanAfter, { sortedList }, { fixIt /* i think running this automatically led to data loss - result: fixIt() */ }, ) } return sortedList } interface reorderOptions { up?: number down?: number after?: string } export function reorderRelation( thisRelation: RelationModelDef, whereTo: reorderOptions, relationsOfParent: Array, ag: string, ) { // const fullArray: Array> = await store.resolve([RelationModel], { parent }) VERBOSE('[reorder]', { relationToReorder: thisRelation, parent: thisRelation.childOf, fullArray: relationsOfParent, whereTo }) const { up, down, after } = whereTo // if (!relationsOfParent) { // relationsOfParent = queryBlockKids(thisRelation.childOf) // } const index = relationsOfParent.findIndex(rel => rel.en === thisRelation.en) if (index === -1) { throw ERROR('Relation not found in parentRelations', { thisRelation, relationsOfParent }) } let newAfter: EntityID | undefined const newApplogs: ApplogForInsert[] = [] if (up) { // if (relationToReorder.kid === fullArray[0].kid) { if (index === 0) { DEBUG('[at top already]') } else { const nodeAboveUs = relationsOfParent[index - 1] // 'node' refers to the relation, not the block newAfter = nodeAboveUs.after ?? null // (i) explicit null needed incase its moving to the top newApplogs.push( { en: nodeAboveUs.en, at: 'relation/after', vl: thisRelation.block, ag }, ) const previouslyAfterUs = relationsOfParent.filter(r => r.after === thisRelation.block) for (const eachRelAfterUs of previouslyAfterUs) { newApplogs.push( { en: eachRelAfterUs.en, at: 'relation/after', vl: nodeAboveUs.block, ag }, ) } } } else if (down) { // if (relationToReorder.kid === relationsOfParent[relationsOfParent.length - 1].kid) { if (index === relationsOfParent.length - 1) { DEBUG('[at bottom already]') } else { const oldAfter = thisRelation.after ?? null // (i) explicit null needed incase its moving from the top const nodeAfterUs = relationsOfParent[index + 1] // 'node' refers to the relation, not the block newAfter = nodeAfterUs.block as string newApplogs.push( { en: thisRelation.en, at: 'relation/after', vl: newAfter, ag }, { en: nodeAfterUs.en, at: 'relation/after', vl: oldAfter, ag }, ) const previouslyAfterNext = relationsOfParent.filter(r => r.after === nodeAfterUs.block) for (const eachRelAfterNextNode of previouslyAfterNext) { newApplogs.push( { en: eachRelAfterNextNode.en, at: 'relation/after', vl: thisRelation.block, ag }, ) } } } else if (after) { WARN('[TODO actually move after]', after) } // FIX a sibling that is after a node with kids thinks it is after the first kid of the sibling instead of the sibling itself newApplogs.push( { en: thisRelation.en, at: 'relation/after', vl: newAfter ?? null, ag }, ) LOG('[reorderRelations] resulting applogs:', newApplogs, { thisRelation, whereTo, relationsOfParent }) return newApplogs }