diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2020-05-07 01:35:11 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2020-05-07 01:35:11 -0400 |
commit | 51a1f88b4e975946eb5ac8cef75a122e197cd872 (patch) | |
tree | 9d56101ca7caf1f3625843d380f4493a16cde1bf | |
parent | 7e2b162446a7f384ef2781fd786a97c21d1a172b (diff) |
added batch requesting of list items. fixed some performance issues with tree views.
-rw-r--r-- | src/client/DocServer.ts | 64 | ||||
-rw-r--r-- | src/client/util/Scripting.ts | 2 | ||||
-rw-r--r-- | src/client/views/DocumentButtonBar.tsx | 32 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackingView.tsx | 10 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.scss | 1 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 18 | ||||
-rw-r--r-- | src/client/views/collections/ParentDocumentSelector.tsx | 4 | ||||
-rw-r--r-- | src/new_fields/List.ts | 76 | ||||
-rw-r--r-- | src/new_fields/Proxy.ts | 18 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 13 | ||||
-rw-r--r-- | src/server/database.ts | 2 |
13 files changed, 161 insertions, 89 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 0c9d5f75c..bf9fd232c 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -7,6 +7,7 @@ import { RefField } from '../new_fields/RefField'; import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; import GestureOverlay from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; +import { runInAction } from 'mobx'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -109,6 +110,7 @@ export namespace DocServer { GUID = identifier; _socket = OpenSocket(`${protocol}//${hostname}:${port}`); + _GetCachedRefField = _GetCachedRefFieldImpl; _GetRefField = _GetRefFieldImpl; _GetRefFields = _GetRefFieldsImpl; _CreateField = _CreateFieldImpl; @@ -243,12 +245,22 @@ export namespace DocServer { return Promise.resolve(cached); } }; + const _GetCachedRefFieldImpl = (id: string): Opt<RefField> => { + const cached = _cache[id]; + if (cached !== undefined && !(cached instanceof Promise)) { + return cached; + } + } let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc; + let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc; export function GetRefField(id: string): Promise<Opt<RefField>> { return _GetRefField(id); } + export function GetCachedRefField(id: string): Opt<RefField> { + return _GetCachedRefField(id); + } export async function getYoutubeChannels() { const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels }); @@ -308,32 +320,34 @@ export namespace DocServer { const deserializeFields = getSerializedFields.then(async fields => { const fieldMap: { [id: string]: RefField } = {}; const proms: Promise<void>[] = []; - for (const field of fields) { - if (field !== undefined && field !== null) { - // deserialize - const prom = SerializationHelper.Deserialize(field).then(deserialized => { - fieldMap[field.id] = deserialized; - - //overwrite or delete any promises (that we inserted as flags - // to indicate that the field was in the process of being fetched). Now everything - // should be an actual value within or entirely absent from the cache. - if (deserialized !== undefined) { - _cache[field.id] = deserialized; - } else { - delete _cache[field.id]; - } - return deserialized; - }); - // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) - // we set the value at the field's id to a promise that will resolve to the field. - // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). - // The mapping in the .then call ensures that when other callers await these promises, they'll - // get the resolved field - _cache[field.id] = prom; - // adds to a list of promises that will be awaited asynchronously - proms.push(prom); + runInAction(() => { + for (const field of fields) { + if (field !== undefined && field !== null) { + // deserialize + const prom = SerializationHelper.Deserialize(field).then(deserialized => { + fieldMap[field.id] = deserialized; + + //overwrite or delete any promises (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. + if (deserialized !== undefined) { + _cache[field.id] = deserialized; + } else { + delete _cache[field.id]; + } + return deserialized; + }); + // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) + // we set the value at the field's id to a promise that will resolve to the field. + // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). + // The mapping in the .then call ensures that when other callers await these promises, they'll + // get the resolved field + _cache[field.id] = prom; + // adds to a list of promises that will be awaited asynchronously + proms.push(prom); + } } - } + }); await Promise.all(proms); return fieldMap; }); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 12628273b..8b7b9c9c7 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -136,7 +136,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an if (batch) { batch.end(); } - onError && onError(error); + onError?.(error); return { success: false, error, result: errorVal }; } }; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index cf57094b2..d58eb5875 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -50,11 +50,9 @@ enum UtilityButtonState { } @observer -export class DocumentButtonBar extends React.Component<{ views: (DocumentView | undefined)[], stack?: any }, {}> { +export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[], stack?: any }, {}> { private _linkButton = React.createRef<HTMLDivElement>(); private _dragRef = React.createRef<HTMLDivElement>(); - private _downX = 0; - private _downY = 0; private _pullAnimating = false; private _pushAnimating = false; private _pullColorAnimating = false; @@ -71,7 +69,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | public static hasPushedHack = false; public static hasPulledHack = false; - constructor(props: { views: (DocumentView | undefined)[] }) { + constructor(props: { views: () => (DocumentView | undefined)[] }) { super(props); runInAction(() => DocumentButtonBar.Instance = this); } @@ -113,7 +111,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | this._pullColorAnimating = false; }); - get view0() { return this.props.views?.[0]; } + get view0() { return this.props.views()?.[0]; } @action onLinkButtonMoved = (e: PointerEvent) => { @@ -265,7 +263,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | const view0 = this.view0; return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<MetadataEntryMenu docs={() => this.props.views.filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> + content={<MetadataEntryMenu docs={() => this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} </div> @@ -289,7 +287,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | } onAliasButtonMoved = () => { if (this._dragRef.current) { - const dragDocView = this.props.views[0]!; + const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); dragData.embedDoc = true; @@ -308,16 +306,18 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | get templateButton() { const view0 = this.view0; const templates: Map<Template, boolean> = new Map(); + const views = this.props.views(); Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); - return !view0 ? (null) : <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={!this._aliasDown ? (null) : <TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} - </div> - </Flyout> - </div>; + templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); + return !view0 ? (null) : + <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + </div> + </Flyout> + </div>; } render() { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index b89806656..9b6105811 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -499,7 +499,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div > <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> - <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> + <DocumentButtonBar views={SelectionManager.SelectedDocuments} /> </div> </div > ); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6cd5d1b1b..6f5989052 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -517,14 +517,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); } }; - let rendered = false; + tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), (views) => { - !rendered && ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > - <DockingViewButtonSelector views={views} Stack={stack} /> + ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > + <DockingViewButtonSelector views={() => views} Stack={stack} /> </span>, gearSpan); - rendered = true; + tab.buttonDisposer?.(); }); tab.reactComponents = [gearSpan]; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4cc2e1a5f..c199988c0 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -57,6 +57,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } + constructor(props: any) { + super(props); + + if (this.sectionHeaders === undefined) { + this.props.Document.sectionHeaders = new List<SchemaHeaderField>(); + } + } + children(docs: Doc[], columns?: number) { TraceMobx(); this._docXfs.length = 0; @@ -85,7 +93,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders); + const sectionHeaders = Array.from(this.sectionHeaders); const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index a00bb6bfb..2aac81146 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -81,7 +81,6 @@ position: relative; text-overflow: ellipsis; white-space: pre-wrap; - overflow: hidden; min-width: 10px; // width:100%;//width: max-content; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 297c11e35..4fb54cc07 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -34,6 +34,7 @@ import "./CollectionTreeView.scss"; import { CollectionViewType } from './CollectionView'; import React = require("react"); import { makeTemplate } from '../../util/DropConverter'; +import { TraceMobx } from '../../../new_fields/util'; export interface TreeViewProps { @@ -100,7 +101,7 @@ class TreeView extends React.Component<TreeViewProps> { get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @@ -326,6 +327,7 @@ class TreeView extends React.Component<TreeViewProps> { rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; @computed get renderContent() { + TraceMobx(); const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { const remDoc = (doc: Doc) => this.remove(doc, expandKey); @@ -419,13 +421,15 @@ class TreeView extends React.Component<TreeViewProps> { return [{ script: focusScript!, label: "Focus" }]; } _docRef = React.createRef<DocumentView>(); + _editTitleScript: ScriptField | undefined; /** * Renders the EditableView title element for placement into the tree. */ @computed get renderTitle() { + TraceMobx(); const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); - const editTitle = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)"); + //!this._editTitleScript && (this._editTitleScript = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)")); const headerElements = ( <> @@ -449,7 +453,6 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} onPointerDown={onItemDown} style={{ - background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", fontWeight: this.props.document.searchMatch ? "bold" : undefined, textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, @@ -461,12 +464,12 @@ class TreeView extends React.Component<TreeViewProps> { ref={this._docRef} Document={this.props.document} DataDoc={undefined} - LibraryPath={this.props.libraryPath || []} + LibraryPath={this.props.libraryPath || emptyPath} addDocument={undefined} addDocTab={this.props.addDocTab} rootSelected={returnTrue} pinToPres={emptyFunction} - onClick={this.props.onChildClick || editTitle} + onClick={this.props.onChildClick || this._editTitleScript} dropAction={this.props.dropAction} moveDocument={this.move} removeDocument={this.removeDoc} @@ -478,7 +481,7 @@ class TreeView extends React.Component<TreeViewProps> { NativeWidth={returnZero} contextMenuItems={this.contextMenuItems} renderDepth={1} - focus={emptyFunction} + focus={returnTrue} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -493,8 +496,9 @@ class TreeView extends React.Component<TreeViewProps> { } render() { + TraceMobx(); const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; - setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); + //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); return <div className="treeViewItem-container" ref={this.createTreeDropTarget}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onClick={e => { diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 10c6ead1a..6e4f801c0 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -94,7 +94,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> { } @observer -export class DockingViewButtonSelector extends React.Component<{ views: DocumentView[], Stack: any }> { +export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> { customStylesheet(styles: any) { return { ...styles, @@ -120,7 +120,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document if (getComputedStyle(this._ref.current!).width !== "100%") { e.stopPropagation(); e.preventDefault(); } - this.props.views[0]?.select(false); + this.props.views()[0]?.select(false); }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={"cog"} size={"sm"} /> diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index a43f11e82..fdabea365 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -2,12 +2,13 @@ import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/ import { Field } from "./Doc"; import { setter, getter, deleteProperty, updateFunction } from "./util"; import { serializable, alias, list } from "serializr"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { ObjectField } from "./ObjectField"; import { RefField } from "./RefField"; import { ProxyField } from "./Proxy"; -import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy } from "./FieldSymbols"; +import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy, Id } from "./FieldSymbols"; import { Scripting } from "../client/util/Scripting"; +import { DocServer } from "../client/DocServer"; const listHandlers: any = { /// Mutator methods @@ -54,11 +55,13 @@ const listHandlers: any = { return res; }, sort(cmpFunc: any) { + this[Self].__realFields(); // coerce retrieving entire array const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); this[Update](); return res; }, splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { + this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; for (let i = 0; i < items.length; i++) { @@ -94,102 +97,102 @@ const listHandlers: any = { }, /// Accessor methods concat: action(function (this: any, ...items: any[]) { + this[Self].__realFields(); return this[Self].__fields.map(toRealField).concat(...items); }), includes(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).includes(valueToFind, fromIndex); + return this[Self].__realFields().includes(valueToFind, fromIndex); } else { - return fields.includes(valueToFind, fromIndex); + return this[Self].__fields.includes(valueToFind, fromIndex); } }, indexOf(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).indexOf(valueToFind, fromIndex); + return this[Self].__realFields().indexOf(valueToFind, fromIndex); } else { - return fields.indexOf(valueToFind, fromIndex); + return this[Self].__fields.indexOf(valueToFind, fromIndex); } }, join(separator: any) { + this[Self].__realFields(); return this[Self].__fields.map(toRealField).join(separator); }, lastIndexOf(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex); + return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); } else { - return fields.lastIndexOf(valueToFind, fromIndex); + return this[Self].__fields.lastIndexOf(valueToFind, fromIndex); } }, slice(begin: number, end: number) { + this[Self].__realFields(); return this[Self].__fields.slice(begin, end).map(toRealField); }, /// Iteration methods entries() { - return this[Self].__fields.map(toRealField).entries(); + return this[Self].__realFields().entries(); }, every(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).every(callback, thisArg); + return this[Self].__realFields().every(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, filter(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).filter(callback, thisArg); + return this[Self].__realFields().filter(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, find(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).find(callback, thisArg); + return this[Self].__realFields().find(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, findIndex(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).findIndex(callback, thisArg); + return this[Self].__realFields().findIndex(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, forEach(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).forEach(callback, thisArg); + return this[Self].__realFields().forEach(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, map(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).map(callback, thisArg); + return this[Self].__realFields().map(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, reduce(callback: any, initialValue: any) { - return this[Self].__fields.map(toRealField).reduce(callback, initialValue); + return this[Self].__realFields().reduce(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, reduceRight(callback: any, initialValue: any) { - return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue); + return this[Self].__realFields().reduceRight(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, some(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).some(callback, thisArg); + return this[Self].__realFields().some(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, values() { - return this[Self].__fields.map(toRealField).values(); + return this[Self].__realFields().values(); }, [Symbol.iterator]() { - return this[Self].__fields.map(toRealField).values(); + return this[Self].__realFields().values(); } }; @@ -254,6 +257,31 @@ class ListImpl<T extends Field> extends ObjectField { [key: number]: T | (T extends RefField ? Promise<T> : never); + // this requests all ProxyFields at the same time to avoid the overhead + // of separate network requests and separate updates to the React dom. + private __realFields() { + const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()); + const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : ""); + // if we find any ProxyFields that don't have a current value, then + // start the server request for all of them + if (promised.length) { + const promise = DocServer.GetRefFields(promised); + // as soon as we get the fields from the server, set all the list values in one + // action to generate one React dom update. + promise.then(fields => runInAction(() => { + waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]])); + })); + // we also have to mark all lists items with this promise so that any calls to them + // will await the batch request. + // This counts on the handler for 'promise' in the call above being invoked before the + // handler for 'promise' in the lines below. + waiting.map((w, i) => { + w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]])); + }); + } + return this.__fields.map(toRealField); + } + @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize }))) private get __fields() { return this.___fields; @@ -283,7 +311,7 @@ class ListImpl<T extends Field> extends ObjectField { // console.log(diff); const update = this[OnUpdate]; // update && update(diff); - update && update(); + update?.(); } private [Self] = this; diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index d50c0f14e..555faaad0 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -1,7 +1,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { FieldWaiting } from "./Doc"; import { primitive, serializable } from "serializr"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { DocServer } from "../client/DocServer"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; @@ -61,6 +61,11 @@ export class ProxyField<T extends RefField> extends ObjectField { return undefined; } if (!this.promise) { + const cached = DocServer.GetCachedRefField(this.fieldId); + if (cached !== undefined) { + runInAction(() => this.cache = cached as any); + return cached as any; + } this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => { this.promise = undefined; this.cache = field; @@ -70,6 +75,17 @@ export class ProxyField<T extends RefField> extends ObjectField { } return this.promise as any; } + promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; } + setPromise(promise: any) { + this.promise = promise; + } + @action + setValue(field: any) { + this.promise = undefined; + this.cache = field; + if (field === undefined) this.failed = true; + return field; + } } export namespace ProxyField { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index e7b448358..6e06fe931 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -21,8 +21,7 @@ import { FormattedTextBox } from "../../../client/views/nodes/formattedText/Form import { MainView } from "../../../client/views/MainView"; import { DocumentType } from "../../../client/documents/DocumentTypes"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { faBoxOpen } from "@fortawesome/free-solid-svg-icons"; -import { CollectionMulticolumnView, DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView"; +import { DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView"; export class CurrentUserUtils { private static curr_id: string; @@ -282,8 +281,12 @@ export class CurrentUserUtils { doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); - DocListCastAsync(templateIconsDoc).then(list => templateIconsDoc.data = new List<Doc>([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc])); + const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; + DocListCastAsync(templateIconsDoc.data).then(async curIcons => { + await Promise.all(curIcons!); + requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); + }); } return doc["template-icons"] as Doc; } @@ -501,7 +504,7 @@ export class CurrentUserUtils { static setupDocumentCollection(doc: Doc) { if (doc.myDocuments === undefined) { doc.myDocuments = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, })); } return doc.myDocuments as Doc; diff --git a/src/server/database.ts b/src/server/database.ts index cfd707825..580f7f919 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -291,7 +291,7 @@ export namespace Database { } } - export const Instance: IDatabase = getDatabase(); + export const Instance = getDatabase(); export namespace Auxiliary { |