From 147cd8618023884b9eb60a79d5efe53abefe9c47 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 24 Mar 2021 18:50:27 -0400 Subject: redid how LinkManager stores links on documents by putting them on the Doc itself instead of as a computedFn. This has a signifcant effect on efficiency since adding a link to one document will no longer invalidate every other view that references *any* document's links --- src/client/util/LinkManager.ts | 103 +++++++++++++++++-------- src/client/views/DocumentButtonBar.tsx | 4 +- src/client/views/Main.tsx | 2 + src/client/views/nodes/DocumentLinksButton.tsx | 22 +++--- src/client/views/nodes/DocumentView.tsx | 3 +- src/client/views/nodes/ScreenshotBox.tsx | 2 + src/client/views/nodes/VideoBox.tsx | 10 +-- src/fields/Doc.ts | 2 + src/fields/util.ts | 3 +- 9 files changed, 92 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index bf973c3d6..62338e691 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,21 +1,21 @@ import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { BoolCast, Cast, StrCast } from "../../fields/Types"; +import { Doc, DocListCast, Opt, DirectLinksSym, Field } from "../../fields/Doc"; +import { BoolCast, Cast, StrCast, PromiseValue } from "../../fields/Types"; import { LightboxView } from "../views/LightboxView"; import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; +import { observe, observable, reaction } from "mobx"; +import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; +import { ProxyField } from "../../fields/Proxy"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* * link doc: - * - anchor1: doc - * - anchor1page: number - * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in + * - anchor1: doc * - anchor2: doc - * - anchor2page: number - * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in * * group doc: * - type: string representing the group type/name/category @@ -26,38 +26,80 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo */ export class LinkManager { - private static _instance: LinkManager; + @observable static _instance: LinkManager; + @observable static userDocs: Doc[] = []; public static currentLink: Opt; - public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } + public static get Instance() { return LinkManager._instance; } + constructor() { + LinkManager._instance = this; + setTimeout(() => { + LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase as Doc)]; + const addLinkToDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + } + } + const remLinkFromDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + } + } + const watchUserLinks = (userLinks: List) => { + const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + observe(userLinks, change => { + switch (change.type) { + 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(LinkManager.userDocs, change => { + switch (change.type) { + case "splice": (change as any).added.forEach(watchUserLinks); break; + case "update": let oldValue = change.oldValue; + } + }, true); + }); + } - public addLink(linkDoc: Doc) { return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); } + public addLink(linkDoc: Doc) { + return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + } public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { return this.directLinker(anchor); } // finds all links that contain the given anchor - public getAllLinks(): Doc[] { return this.allLinks(); } - - allLinks = computedFn(function allLinks(this: any): Doc[] { - const linkData = Doc.LinkDBDoc().data; - const lset = new Set(DocListCast(linkData)); - SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); - return Array.from(lset); - }, true); + public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor + public getAllLinks(): Doc[] { return []; }//this.allLinks(); } - directLinker = computedFn(function directLinker(this: any, anchor: Doc): Doc[] { - return LinkManager.Instance.allLinks().filter(link => { - const a1 = Cast(link?.anchor1, Doc, null); - const a2 = Cast(link?.anchor2, Doc, null); - return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); - }); - }, true); + // allLinks = computedFn(function allLinks(this: any): Doc[] { + // const linkData = Doc.LinkDBDoc().data; + // const lset = new Set(DocListCast(linkData)); + // SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); + // LinkManager.Instance.allLinks().filter(link => { + // const a1 = Cast(link?.anchor1, Doc, null); + // const a2 = Cast(link?.anchor2, Doc, null); + // return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); + // }); + // return Array.from(lset); + // }, true); relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], - LinkManager.Instance.directLinker(anchor).slice()); + Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());// LinkManager.Instance.directLinker(anchor).slice()); }, true); // returns map of group type to anchor's links in that group type @@ -76,13 +118,6 @@ export class LinkManager { return anchorGroups; } - // checks if a link with the given anchors exists - public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { - return -1 !== LinkManager.Instance.allLinks().findIndex(linkDoc => - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1))); - } - // finds the opposite anchor of a given anchor in a link //TODO This should probably return undefined if there isn't an opposite anchor //TODO This should also await the return value of the anchor so we don't filter out promises diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index e248ef39a..a5d80cd22 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -352,10 +352,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return
- +
{DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-fullMenu"] ?
- +
: (null)} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) :
{this.templateButton} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 92f6ae028..60327f1bf 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,6 +7,7 @@ import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Networking } from "../Network"; import { CollectionView } from "./collections/CollectionView"; +import { LinkManager } from "../util/LinkManager"; AssignAllExtensions(); @@ -31,5 +32,6 @@ AssignAllExtensions(); d.setTime(d.getTime() + (100 * 24 * 60 * 60 * 1000)); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; + new LinkManager(); ReactDOM.render(, document.getElementById('root')); })(); \ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 57d1a41b6..a6d07374a 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -31,7 +31,6 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; - links: Doc[]; } @observer export class DocumentLinksButton extends React.Component { @@ -225,7 +224,6 @@ export class DocumentLinksButton extends React.Component(this.props.links)).forEach(link => { - if (!DocUtils.FilterDocs([link], this.props.View.props.docFilters(), []).length) { - if (DocUtils.FilterDocs([link.anchor2 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - if (DocUtils.FilterDocs([link.anchor1 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - } else results.push(link); + const filters = this.props.View.props.docFilters(); + Array.from(new Set(this.props.View.allLinks)).forEach(link => { + if (DocUtils.FilterDocs([link], filters, []).length || + DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || + DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + results.push(link); + } }); return results; } @@ -296,12 +292,12 @@ export class DocumentLinksButton extends React.Component
{title}
}> + {title}
}> {this.linkButtonInner} : !DocumentLinksButton.LinkEditorDocView && !this.props.InMenu ? -
{title}
}> + {title}
}> {this.linkButtonInner} : this.linkButtonInner; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index aff0efdc7..df769a407 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -795,7 +795,7 @@ export class DocumentViewInternal extends DocComponent {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} {this.hideLinkButton ? (null) : - } + } {audioView} ; @@ -814,7 +814,6 @@ export class DocumentViewInternal extends DocComponent !d.hidden); return filtered.map((link, i) => diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 999ccf5f6..ec97a11e4 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -23,6 +23,7 @@ import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; +import { TraceMobx } from "../../../fields/util"; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has } @@ -133,6 +134,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent [this.content]; render() { + TraceMobx(); return
; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 953d96ffa..d0ccce9cf 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -85,6 +85,7 @@ export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); +export const DirectLinksSym = Symbol("DirectLinks"); export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); @@ -185,6 +186,7 @@ export class Doc extends RefField { @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; @observable public [AclSym]: { [key: string]: symbol }; + @observable public [DirectLinksSym]: Set = new Set(); private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; diff --git a/src/fields/util.ts b/src/fields/util.ts index 631cb7160..6c9c9d45c 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -19,12 +19,11 @@ function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -const tracing = false; +const tracing = true; export function TraceMobx() { tracing && trace(); } - export interface GetterResult { value: FieldResult; shouldReturn?: boolean; -- cgit v1.2.3-70-g09d2