import { action, makeObservable, observable, observe } from 'mobx'; import { computedFn } from 'mobx-utils'; import * as rp from 'request-promise'; import { ClientUtils } from '../../ClientUtils'; import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from '../../fields/Doc'; import { DirectLinks, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { DocumentType } from '../documents/DocumentTypes'; import { ScriptingGlobals } from './ScriptingGlobals'; /* * link doc: * - link_anchor_1: doc * - link_anchor_2: doc * * group doc: * - type: string representing the group type/name/category * - metadata: doc representing the metadata kvps * * metadata doc: * - user defined kvps */ export class LinkManager { @observable static _instance: LinkManager; @observable.shallow userLinkDBs: Doc[] = []; @observable public currentLink: Opt = undefined; @observable public currentLinkAnchor: Opt = undefined; public static get Instance() { return LinkManager._instance; } public static Links(doc: Doc | undefined) { return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : []; } public addLinkDB = async (linkDb: any) => { await Promise.all( ((await DocListCastAsync(linkDb.data)) ?? []).map(link => // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager [PromiseValue(link?.link_anchor_1), PromiseValue(link?.link_anchor_2)] ) ); this.userLinkDBs.push(linkDb); }; public static AutoKeywords = 'keywords:Usages'; constructor() { makeObservable(this); LinkManager._instance = this; Doc.AddLink = this.addLink; this.createlink_relationshipLists(); // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto // Then add the link to the anchor protos. const addLinkToDoc = (lprom: Doc) => PromiseValue(lprom).then((link: Opt) => PromiseValue(link?.proto as Doc).then((lproto: Opt) => Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt[]) => Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt[]) => Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( link && action(lAnchProtoProtos => { Doc.AddDocToList(Doc.UserDoc(), 'links', link); lAnchs[0]?.[DocData][DirectLinks].add(link); lAnchs[1]?.[DocData][DirectLinks].add(link); }) ) ) ) ) ); const remLinkFromDoc = (lprom: Doc) => PromiseValue(lprom).then((link: Opt) => PromiseValue(link?.proto as Doc).then((lproto: Opt) => Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt[]) => Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt[]) => Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( action(lAnchProtoProtos => { link && lAnchs[0] && lAnchs[0][DocData][DirectLinks].delete(link); link && lAnchs[1] && lAnchs[1][DocData][DirectLinks].delete(link); }) ) ) ) ) ); const watchUserLinkDB = (userLinkDBDoc: Doc) => { const toRealField = (field: FieldType) => (field instanceof ProxyField ? field.value : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields if (userLinkDBDoc.data) { observe( userLinkDBDoc.data, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) switch (change.type as any) { case 'splice': (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); break; case 'update': //let oldValue = change.oldValue; } }, true ); observe( userLinkDBDoc, 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) change => { switch (change.type as any) { case 'update': Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => { const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length); const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length); const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); added?.forEach((link: any) => addLinkToDoc(toRealField(link))); removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); }); } }, true ); } }; observe( this.userLinkDBs, change => { switch (change.type as any) { case 'splice': (change as any).added.forEach(watchUserLinkDB); break; case 'update': //let oldValue = change.oldValue; } }, true ); FieldLoader.ServerLoadStatus.message = 'links'; this.addLinkDB(Doc.LinkDBDoc()); } public createlink_relationshipLists = () => { //create new lists for link relations and their associated colors if the lists don't already exist !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List()); !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List()); !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List()); }; public addLink(linkDoc: Doc, checkExists = false) { Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); setTimeout(UPDATE_SERVER_CACHE, 100); } } public deleteLink(linkDoc: Doc) { const ret = Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc); linkDoc[DocData].link_anchor_1 = linkDoc[DocData].link_anchor_2 = undefined; return ret; } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(LinkManager.Instance.deleteLink); } public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor?: Doc): Doc[] { return anchor ? Array.from(anchor[DocData][DirectLinks]) : []; } // finds all links that contain the given anchor computedRelatedLinks = (anchor: Doc, processed: Doc[]): Doc[] => { if (Doc.IsSystem(anchor)) return []; if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) { console.log('WAITING FOR DOC/PROTO IN LINKMANAGER'); return []; } const dirLinks = Array.from(anchor[DocData][DirectLinks]).filter(l => Doc.GetProto(anchor) === anchor[DocData] || ['1', '2'].includes(LinkManager.anchorIndex(l, anchor) as any)); const anchorRoot = DocCast(anchor.rootDocument, anchor); // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout) const annos = DocListCast(anchorRoot[Doc.LayoutFieldKey(anchor) + '_annotations']); return Array.from( annos.reduce((set, anno) => { if (!processed.includes(anno)) { processed.push(anno); this.computedRelatedLinks(anno, processed).forEach(link => set.add(link)); } return set; }, new Set(dirLinks)) ); }; relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { return this.computedRelatedLinks(anchor, [anchor]); }, true); // returns map of group type to anchor's links in that group type public getRelatedGroupedLinks(anchor: Doc): Map> { const anchorGroups = new Map>(); this.relatedLinker(anchor).forEach(link => { if (link.link_relationship && link.link_relationship !== '-ungrouped-') { const relation = StrCast(link.link_relationship); const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; const group = anchorGroups.get(anchorRelation); anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { // if link is in no groups then put it in default group const group = anchorGroups.get('*'); anchorGroups.set('*', group ? [...group, link] : [link]); } }); return anchorGroups; } // finds the opposite anchor of a given anchor in a link public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { const id = LinkManager.anchorIndex(linkDoc, anchor); const a1 = DocCast(linkDoc.link_anchor_1); const a2 = DocCast(linkDoc.link_anchor_2); return id === '1' ? a2 : id === '2' ? a1 : id === '0' ? linkDoc : undefined; // if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return a2; // if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return a1; // if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } public static anchorIndex(linkDoc: Doc, anchor: Doc) { const a1 = DocCast(linkDoc.link_anchor_1); const a2 = DocCast(linkDoc.link_anchor_2); if (linkDoc.link_matchEmbeddings) { return [a2, a2?.annotationOn].includes(anchor) ? '2' : '1'; } if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1'; if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2'; if (Doc.AreProtosEqual(anchor, linkDoc)) return '0'; // const a1 = DocCast(linkDoc.link_anchor_1); // const a2 = DocCast(linkDoc.link_anchor_2); // if (linkDoc.link_matchEmbeddings) { // return [a2, a2.annotationOn].includes(anchor) ? '2' : '1'; // } // if (Doc.AreProtosEqual(a2, anchor) || Doc.AreProtosEqual(a2.annotationOn as Doc, anchor)) return '2'; // return Doc.AreProtosEqual(a1, anchor) || Doc.AreProtosEqual(a1.annotationOn as Doc, anchor) ? '1' : '2'; } } let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server) export function UPDATE_SERVER_CACHE() { const prototypes = Object.values(DocumentType) .filter(type => type !== DocumentType.NONE) .map(type => DocServer._cache[type + 'Proto']) .filter(doc => doc instanceof Doc) .map(doc => doc as Doc); const references = new Set(prototypes); Doc.FindReferences(Doc.UserDoc(), references, undefined); DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => { if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) { Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link); Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link); } }); LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined)); const filtered = Array.from(references); const newCacheUpdate = filtered.map(doc => doc[Id]).join(';'); if (newCacheUpdate === cacheDocumentIds) return; cacheDocumentIds = newCacheUpdate; // print out cached docs Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = '); const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc)); const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); rp.post(ClientUtils.prepend('/setCacheDocumentIds'), { body: { cacheDocumentIds, }, json: true, }); } ScriptingGlobals.add( function links(doc: any) { return new List(LinkManager.Links(doc)); }, 'returns all the links to the document or its annotations', '(doc: any)' );