diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/DocServer.ts | 80 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 | ||||
-rw-r--r-- | src/fields/Doc.ts | 48 | ||||
-rw-r--r-- | src/fields/Schema.ts | 2 | ||||
-rw-r--r-- | src/fields/util.ts | 10 |
6 files changed, 95 insertions, 52 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index bac324c77..8ded43468 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,13 +1,14 @@ import * as io from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc, fetchProto } from '../fields/Doc'; +import { Opt, Doc, fetchProto, FieldsSym } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; -import { Id, HandleUpdate } from '../fields/FieldSymbols'; +import { Id, HandleUpdate, Parent } from '../fields/FieldSymbols'; import GestureOverlay from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; +import { ObjectField } from '../fields/ObjectField'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -207,12 +208,12 @@ export namespace DocServer { * the server if the document has not been cached. * @param id the id of the requested document */ - const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => { + const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => { // an initial pass through the cache to determine whether the document needs to be fetched, // is already in the process of being fetched or already exists in the // cache const cached = _cache[id]; - if (cached === undefined) { + if (cached === undefined || force) { // NOT CACHED => we'll have to send a request to the server // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) @@ -226,7 +227,16 @@ export namespace DocServer { const deserializeField = getSerializedField.then(async fieldJson => { // deserialize const field = await SerializationHelper.Deserialize(fieldJson); - if (field !== undefined) { + if (force && field instanceof Doc && cached instanceof Doc) { + Array.from(Object.keys(field)).forEach(key => { + const fieldval = field[key]; + if (fieldval instanceof ObjectField) { + fieldval[Parent] = undefined; + } + cached[key] = field[key]; + }); + } + else if (field !== undefined) { _cache[id] = field; } else { delete _cache[id]; @@ -238,8 +248,8 @@ export namespace DocServer { }); // here, indicate that the document associated with this id is currently // being retrieved and cached - _cache[id] = deserializeField; - return deserializeField; + !force && (_cache[id] = deserializeField); + return force ? cached as any : deserializeField; } else if (cached instanceof Promise) { // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s), // and requested the document I'm looking for. Shouldn't fetch again, just @@ -260,11 +270,11 @@ export namespace DocServer { } }; - let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc; + let _GetRefField: (id: string, force: boolean) => Promise<Opt<RefField>> = errorFunc; let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc; - export function GetRefField(id: string): Promise<Opt<RefField>> { - return _GetRefField(id); + export function GetRefField(id: string, force = false): Promise<Opt<RefField>> { + return _GetRefField(id, force); } export function GetCachedRefField(id: string): Opt<RefField> { return _GetCachedRefField(id); @@ -330,29 +340,35 @@ export namespace DocServer { const proms: Promise<void>[] = []; runInAction(() => { for (const field of fields) { - if (field !== undefined && field !== null) { + if (field !== undefined && field !== null && !_cache[field.id]) { // 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); + const cached = _cache[field.id]; + if (!cached) { + 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); + } else if (cached instanceof Promise) { + proms.push(cached as any); + } } } }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 979b21321..38c1edde2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -944,7 +944,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { - ...this.props, + pinToPres: this.props.pinToPres, + whenActiveChanged: this.props.whenActiveChanged, NativeHeight: returnZero, NativeWidth: returnZero, fitToBox: false, @@ -1145,7 +1146,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action componentDidMount() { super.componentDidMount?.(); - this._layoutComputeReaction = reaction(() => this.doLayoutComputation, + this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation }, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2ac53ce6a..53234d819 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -690,7 +690,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ); this._disposers.editorState = reaction( () => { - if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { + if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index c9231753f..9020dcc84 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -104,7 +104,7 @@ export const AclAddonly = Symbol("AclAddonly"); export const AclEdit = Symbol("AclEdit"); export const AclAdmin = Symbol("AclAdmin"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); -const CachedUpdates = Symbol("Cached updates"); +export const CachedUpdates = Symbol("Cached updates"); const AclMap = new Map<string, symbol>([ [SharingPermissions.None, AclPrivate], @@ -115,15 +115,15 @@ const AclMap = new Map<string, symbol>([ ]); export function fetchProto(doc: Doc) { - // if (doc.author !== Doc.CurrentUserEmail) { - untracked(() => { - const permissions: { [key: string]: symbol } = {}; + const permissions: { [key: string]: symbol } = {}; - Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); + Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); - if (Object.keys(permissions).length) doc[AclSym] = permissions; - }); - // } + if (Object.keys(permissions).length) doc[AclSym] = permissions; + + if (GetEffectiveAcl(doc) === AclPrivate) { + runInAction(() => doc[FieldsSym](true)); + } if (doc.proto instanceof Promise) { doc.proto.then(fetchProto); @@ -143,7 +143,7 @@ export class Doc extends RefField { has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields, ownKeys: target => { const obj = {} as any; - if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields); + if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); return Object.keys(obj); }, @@ -151,11 +151,11 @@ export class Doc extends RefField { if (prop.toString() === "__LAYOUT__") { return Reflect.getOwnPropertyDescriptor(target, prop); } - if (prop in target.__fields) { + if (prop in target.__fieldKeys) { return { configurable: true,//TODO Should configurable be true? enumerable: true, - value: target.__fields[prop] + value: 0//() => target.__fields[prop]) }; } return Reflect.getOwnPropertyDescriptor(target, prop); @@ -179,15 +179,23 @@ export class Doc extends RefField { this.___fields = value; for (const key in value) { const field = value[key]; + field && (this.__fieldKeys[key] = true); if (!(field instanceof ObjectField)) continue; field[Parent] = this[Self]; field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); } } + private get __fieldKeys() { return this.___fieldKeys; } + private set __fieldKeys(value) { + this.___fieldKeys = value; + } @observable private ___fields: any = {}; + @observable + private ___fieldKeys: any = {}; + private [UpdatingFromServer]: boolean = false; private [Update] = (diff: any) => { @@ -196,7 +204,14 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym] = () => this.___fields; + public [FieldsSym] = (clear?: boolean) => { + if (clear) { + this.___fields = {}; + this.___fieldKeys = {}; + } + return this.___fields; + } + @observable public [AclSym]: { [key: string]: symbol }; public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); @@ -237,11 +252,18 @@ export class Doc extends RefField { const fKey = key.substring(7); const fn = async () => { const value = await SerializationHelper.Deserialize(set[key]); + const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; + if (fKey.startsWith("ACL")) { + fetchProto(this); + } this[UpdatingFromServer] = false; + if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { + DocServer.GetRefField(this[Id], true); + } }; - if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { + if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; await fn(); } else { diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 98ef3e087..c6a8d6ae9 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -31,7 +31,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu } const proto = new Proxy({}, { get(target: any, prop, receiver) { - const field = receiver.doc[prop]; + const field = receiver.doc?.[prop]; if (prop in schema) { const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec diff --git a/src/fields/util.ts b/src/fields/util.ts index 2b970d342..e0b1843a1 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,11 +1,11 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, fetchProto, DataSym, DocListCast, AclAdmin } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action, trace } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; @@ -77,8 +77,10 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (writeToDoc) { if (value === undefined) { + delete target.__fieldKeys[prop]; delete target.__fields[prop]; } else { + target.__fieldKeys[prop] = true; target.__fields[prop] = value; } //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; @@ -137,7 +139,7 @@ export enum SharingPermissions { } export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { - if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclEdit; + if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { @@ -241,6 +243,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; + + if (in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined; if (prop === LayoutSym) { |