From 79d8b5c812db5c28f477ace8db0ee4d9e18a84b7 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 17 Apr 2019 06:16:50 -0400
Subject: Started implementing new documents

---
 src/debug/Test.tsx | 39 +++++++++++++++++++++------------------
 1 file changed, 21 insertions(+), 18 deletions(-)

(limited to 'src/debug/Test.tsx')

diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 11f2b0c4e..ca093e5b2 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,29 +1,32 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
-import JsxParser from 'react-jsx-parser';
+import { serialize, deserialize, map } from 'serializr';
+import { URLField, Doc } from '../fields/NewDoc';
 
-class Hello extends React.Component<{ firstName: string, lastName: string }> {
-    render() {
-        return <div>Hello {this.props.firstName} {this.props.lastName}</div>;
+class Test extends React.Component {
+    onClick = () => {
+        const url = new URLField(new URL("http://google.com"));
+        const doc = new Doc("a");
+        const doc2 = new Doc("b");
+        doc.hello = 5;
+        doc.fields = "test";
+        doc.test = "hello doc";
+        doc.url = url;
+        doc.testDoc = doc2;
+
+        console.log(doc.hello);
+        console.log(doc.fields);
+        console.log(doc.test);
+        console.log(doc.url);
+        console.log(doc.testDoc);
     }
-}
 
-class Test extends React.Component {
     render() {
-        let jsx = "<Hello {...props}/>";
-        let bindings = {
-            props: {
-                firstName: "First",
-                lastName: "Last"
-            }
-        };
-        return <JsxParser jsx={jsx} bindings={bindings} components={{ Hello }}></JsxParser>;
+        return <button onClick={this.onClick}>Click me</button>;
     }
 }
 
-ReactDOM.render((
-    <div style={{ position: "absolute", width: "100%", height: "100%" }}>
-        <Test />
-    </div>),
+ReactDOM.render(
+    <Test />,
     document.getElementById('root')
 );
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 6afdd5e5136394c9dc739b5de390aa1b55c6360f Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 17 Apr 2019 13:51:40 -0400
Subject: Serialization is mostly working

---
 src/client/util/SerializationHelper.ts | 72 ++++++++++++++++++++++++++++++++++
 src/debug/Test.tsx                     | 10 ++---
 src/fields/NewDoc.ts                   | 45 ++++++++++++++-------
 3 files changed, 107 insertions(+), 20 deletions(-)
 create mode 100644 src/client/util/SerializationHelper.ts

(limited to 'src/debug/Test.tsx')

diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
new file mode 100644
index 000000000..656101c95
--- /dev/null
+++ b/src/client/util/SerializationHelper.ts
@@ -0,0 +1,72 @@
+import { PropSchema, serialize, deserialize, custom } from "serializr";
+import { Field } from "../../fields/NewDoc";
+
+export class SerializationHelper {
+
+    public static Serialize(obj: Field): any {
+        if (!obj) {
+            return null;
+        }
+
+        if (typeof obj !== 'object') {
+            return obj;
+        }
+
+        if (!(obj.constructor.name in reverseMap)) {
+            throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+        }
+
+        const json = serialize(obj);
+        json.__type = reverseMap[obj.constructor.name];
+        return json;
+    }
+
+    public static Deserialize(obj: any): any {
+        if (!obj) {
+            return null;
+        }
+
+        if (typeof obj !== 'object') {
+            return obj;
+        }
+
+        if (!obj.__type) {
+            throw Error("No property 'type' found in JSON.");
+        }
+
+        if (!(obj.__type in serializationTypes)) {
+            throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
+        }
+
+        return deserialize(serializationTypes[obj.__type], obj);
+    }
+}
+
+let serializationTypes: { [name: string]: any } = {};
+let reverseMap: { [ctor: string]: string } = {};
+
+export function Deserializable(name: string): Function;
+export function Deserializable(constructor: Function): void;
+export function Deserializable(constructor: Function | string): Function | void {
+    function addToMap(name: string, ctor: Function) {
+        if (!(name in serializationTypes)) {
+            serializationTypes[name] = constructor;
+            reverseMap[ctor.name] = name;
+        } else {
+            throw new Error(`Name ${name} has already been registered as deserializable`);
+        }
+    }
+    if (typeof constructor === "string") {
+        return (ctor: Function) => {
+            addToMap(constructor, ctor);
+        };
+    }
+    addToMap(constructor.name, constructor);
+}
+
+export function autoObject(): PropSchema {
+    return custom(
+        (s) => SerializationHelper.Serialize(s),
+        (s) => SerializationHelper.Deserialize(s)
+    );
+}
\ No newline at end of file
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index ca093e5b2..7e7b3a964 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { serialize, deserialize, map } from 'serializr';
 import { URLField, Doc } from '../fields/NewDoc';
+import { SerializationHelper } from '../client/util/SerializationHelper';
 
 class Test extends React.Component {
     onClick = () => {
@@ -14,11 +15,10 @@ class Test extends React.Component {
         doc.url = url;
         doc.testDoc = doc2;
 
-        console.log(doc.hello);
-        console.log(doc.fields);
-        console.log(doc.test);
-        console.log(doc.url);
-        console.log(doc.testDoc);
+        console.log("doc", doc);
+        const cereal = Doc.Serialize(doc);
+        console.log("cereal", cereal);
+        console.log("doc again", SerializationHelper.Deserialize(cereal));
     }
 
     render() {
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
index d0e518306..05dd14bc3 100644
--- a/src/fields/NewDoc.ts
+++ b/src/fields/NewDoc.ts
@@ -1,16 +1,15 @@
 import { observable, action } from "mobx";
 import { Server } from "../client/Server";
 import { UndoManager } from "../client/util/UndoManager";
-import { serialize, deserialize, serializable, primitive } from "serializr";
+import { serialize, deserialize, serializable, primitive, map, alias } from "serializr";
+import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
 
-const Type = Symbol("type");
-
-const Id = Symbol("id");
 export abstract class RefField {
-    readonly [Id]: string;
+    @serializable(alias("id", primitive()))
+    readonly __id: string;
 
     constructor(id: string) {
-        this[Id] = id;
+        this.__id = id;
     }
 }
 
@@ -31,6 +30,8 @@ function url() {
         }
     };
 }
+
+@Deserializable
 export class URLField extends ObjectField {
     @serializable(url())
     url: URL;
@@ -41,6 +42,7 @@ export class URLField extends ObjectField {
     }
 }
 
+@Deserializable
 export class ProxyField<T extends RefField> extends ObjectField {
     constructor();
     constructor(value: T);
@@ -48,7 +50,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         super();
         if (value) {
             this.cache = value;
-            this.fieldId = value[Id];
+            this.fieldId = value.__id;
         }
     }
 
@@ -94,11 +96,17 @@ export class ProxyField<T extends RefField> extends ObjectField {
 export type Field = number | string | boolean | ObjectField | RefField;
 
 const Self = Symbol("Self");
+
+@Deserializable
 export class Doc extends RefField {
 
     private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+        if (prop === "__id" || prop === "__fields") {
+            target[prop] = value;
+            return true;
+        }
         const curValue = target.__fields[prop];
-        if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
+        if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value.__id)) {
             // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
             // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
             return true;
@@ -130,16 +138,16 @@ export class Doc extends RefField {
     }
 
     private static getter(target: any, prop: string | symbol | number, receiver: any): any {
-        if (typeof prop !== "string") {
-            return undefined;
+        if (typeof prop === "symbol") {
+            return target[prop];
+        }
+        if (prop === "__id" || prop === "__fields") {
+            return target[prop];
         }
         return Doc.getField(target, prop, receiver);
     }
 
-    private static getField(target: any, prop: string, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
-        if (typeof prop === "symbol") {
-            return target.__fields[prop];
-        }
+    private static getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
         const field = target.__fields[prop];
         if (field instanceof ProxyField) {
             return field.value(callback);
@@ -162,6 +170,10 @@ export class Doc extends RefField {
         return new Promise(res => Doc.getField(self, key, ignoreProto, res));
     }
 
+    static Serialize(doc: Doc) {
+        return SerializationHelper.Serialize(doc[Self]);
+    }
+
     constructor(id: string) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -175,6 +187,7 @@ export class Doc extends RefField {
 
     [key: string]: Field | null | undefined;
 
+    @serializable(alias("fields", map(autoObject())))
     @observable
     private __fields: { [key: string]: Field | null | undefined } = {};
 
@@ -187,4 +200,6 @@ export class Doc extends RefField {
 
 export namespace Doc {
     export const Prototype = Symbol("Prototype");
-}
\ No newline at end of file
+}
+
+export const GetAsync = Doc.GetAsync;
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 9712a046868ee51a565a425d3216a2bb297c4eee Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 17 Apr 2019 19:45:59 -0400
Subject: Got saving to database working

---
 src/client/DocServer.ts                | 72 +++++++++++++++++++++++++++++++++
 src/client/util/SerializationHelper.ts | 73 +++++++++++++++++++++++++++++-----
 src/debug/Test.tsx                     |  6 +--
 src/fields/NewDoc.ts                   | 52 ++++++++++++++++--------
 src/server/Message.ts                  | 12 ++++++
 src/server/database.ts                 | 16 ++++----
 src/server/index.ts                    | 19 ++++++++-
 7 files changed, 212 insertions(+), 38 deletions(-)
 create mode 100644 src/client/DocServer.ts

(limited to 'src/debug/Test.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
new file mode 100644
index 000000000..9a3e122e8
--- /dev/null
+++ b/src/client/DocServer.ts
@@ -0,0 +1,72 @@
+import * as OpenSocket from 'socket.io-client';
+import { MessageStore, Types } from "./../server/Message";
+import { Opt, FieldWaiting, RefField, HandleUpdate } from '../fields/NewDoc';
+import { Utils } from '../Utils';
+import { SerializationHelper } from './util/SerializationHelper';
+
+export namespace DocServer {
+    const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+    const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+    const GUID: string = Utils.GenerateGuid();
+
+    export async function GetRefField(id: string): Promise<Opt<RefField>> {
+        let cached = _cache[id];
+        if (cached === undefined) {
+            const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => {
+                const field = fieldJson === undefined ? fieldJson : SerializationHelper.Deserialize(fieldJson);
+                if (field) {
+                    _cache[id] = field;
+                } else {
+                    delete _cache[id];
+                }
+                return field;
+            });
+            _cache[id] = prom;
+            return prom;
+        } else if (cached instanceof Promise) {
+            return cached;
+        } else {
+            return cached;
+        }
+    }
+
+    export function UpdateField(id: string, diff: any) {
+        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+    }
+
+    export function CreateField(initialState: any) {
+        if (!("id" in initialState)) {
+            throw new Error("Can't create a field on the server without an id");
+        }
+        Utils.Emit(_socket, MessageStore.CreateField, initialState);
+    }
+
+    function respondToUpdate(diff: any) {
+        const id = diff.id;
+        if (id === undefined) {
+            return;
+        }
+        const field = _cache[id];
+        const update = (f: Opt<RefField>) => {
+            if (f === undefined) {
+                return;
+            }
+            const handler = f[HandleUpdate];
+            if (handler) {
+                handler(diff);
+            }
+        };
+        if (field instanceof Promise) {
+            field.then(update);
+        } else {
+            update(field);
+        }
+    }
+
+    function connected(message: string) {
+        _socket.emit(MessageStore.Bar.Message, GUID);
+    }
+
+    Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
+    Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+}
\ No newline at end of file
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 656101c95..7273c3fe4 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,9 +1,13 @@
-import { PropSchema, serialize, deserialize, custom } from "serializr";
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
 import { Field } from "../../fields/NewDoc";
 
-export class SerializationHelper {
+export namespace SerializationHelper {
+    let serializing: number = 0;
+    export function IsSerializing() {
+        return serializing > 0;
+    }
 
-    public static Serialize(obj: Field): any {
+    export function Serialize(obj: Field): any {
         if (!obj) {
             return null;
         }
@@ -12,16 +16,18 @@ export class SerializationHelper {
             return obj;
         }
 
+        serializing += 1;
         if (!(obj.constructor.name in reverseMap)) {
             throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
         }
 
         const json = serialize(obj);
         json.__type = reverseMap[obj.constructor.name];
+        serializing -= 1;
         return json;
     }
 
-    public static Deserialize(obj: any): any {
+    export function Deserialize(obj: any): any {
         if (!obj) {
             return null;
         }
@@ -30,6 +36,7 @@ export class SerializationHelper {
             return obj;
         }
 
+        serializing += 1;
         if (!obj.__type) {
             throw Error("No property 'type' found in JSON.");
         }
@@ -38,32 +45,78 @@ export class SerializationHelper {
             throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
         }
 
-        return deserialize(serializationTypes[obj.__type], obj);
+        const value = deserialize(serializationTypes[obj.__type], obj);
+        serializing -= 1;
+        return value;
     }
 }
 
 let serializationTypes: { [name: string]: any } = {};
 let reverseMap: { [ctor: string]: string } = {};
 
-export function Deserializable(name: string): Function;
+export interface DeserializableOpts {
+    (constructor: Function): void;
+    withFields(fields: string[]): Function;
+}
+
+export function Deserializable(name: string): DeserializableOpts;
 export function Deserializable(constructor: Function): void;
-export function Deserializable(constructor: Function | string): Function | void {
+export function Deserializable(constructor: Function | string): DeserializableOpts | void {
     function addToMap(name: string, ctor: Function) {
         if (!(name in serializationTypes)) {
-            serializationTypes[name] = constructor;
+            serializationTypes[name] = ctor;
             reverseMap[ctor.name] = name;
         } else {
             throw new Error(`Name ${name} has already been registered as deserializable`);
         }
     }
     if (typeof constructor === "string") {
-        return (ctor: Function) => {
+        return Object.assign((ctor: Function) => {
             addToMap(constructor, ctor);
-        };
+        }, { withFields: Deserializable.withFields });
     }
     addToMap(constructor.name, constructor);
 }
 
+export namespace Deserializable {
+    export function withFields(fields: string[]) {
+        return function (constructor: { new(...fields: any[]): any }) {
+            Deserializable(constructor);
+            let schema = getDefaultModelSchema(constructor);
+            if (schema) {
+                schema.factory = context => {
+                    const args = fields.map(key => context.json[key]);
+                    return new constructor(...args);
+                };
+                // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
+                // fields.forEach(field => {
+                //     if (field in schema.props) {
+                //         let propSchema = schema.props[field];
+                //         if (propSchema === false) {
+                //             return;
+                //         } else if (propSchema === true) {
+                //             propSchema = primitive();
+                //         }
+                //         schema.props[field] = custom(propSchema.serializer,
+                //             () => {
+                //                 return SKIP;
+                //             });
+                //     }
+                // });
+            } else {
+                schema = {
+                    props: {},
+                    factory: context => {
+                        const args = fields.map(key => context.json[key]);
+                        return new constructor(...args);
+                    }
+                };
+                setDefaultModelSchema(constructor, schema);
+            }
+        };
+    }
+}
+
 export function autoObject(): PropSchema {
     return custom(
         (s) => SerializationHelper.Serialize(s),
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 7e7b3a964..660115453 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -7,8 +7,8 @@ import { SerializationHelper } from '../client/util/SerializationHelper';
 class Test extends React.Component {
     onClick = () => {
         const url = new URLField(new URL("http://google.com"));
-        const doc = new Doc("a");
-        const doc2 = new Doc("b");
+        const doc = new Doc();
+        const doc2 = new Doc();
         doc.hello = 5;
         doc.fields = "test";
         doc.test = "hello doc";
@@ -16,7 +16,7 @@ class Test extends React.Component {
         doc.testDoc = doc2;
 
         console.log("doc", doc);
-        const cereal = Doc.Serialize(doc);
+        const cereal = SerializationHelper.Serialize(doc);
         console.log("cereal", cereal);
         console.log("doc again", SerializationHelper.Deserialize(cereal));
     }
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
index 05dd14bc3..46f2e20e9 100644
--- a/src/fields/NewDoc.ts
+++ b/src/fields/NewDoc.ts
@@ -1,16 +1,24 @@
 import { observable, action } from "mobx";
 import { Server } from "../client/Server";
 import { UndoManager } from "../client/util/UndoManager";
-import { serialize, deserialize, serializable, primitive, map, alias } from "serializr";
+import { serializable, primitive, map, alias } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+import { Utils } from "../Utils";
+import { DocServer } from "../client/DocServer";
 
+export const HandleUpdate = Symbol("HandleUpdate");
+const Id = Symbol("Id");
 export abstract class RefField {
     @serializable(alias("id", primitive()))
-    readonly __id: string;
+    private __id: string;
+    readonly [Id]: string;
 
-    constructor(id: string) {
-        this.__id = id;
+    constructor(id?: string) {
+        this.__id = id || Utils.GenerateGuid();
+        this[Id] = this.__id;
     }
+
+    protected [HandleUpdate]?(diff: any): void;
 }
 
 const Update = Symbol("Update");
@@ -31,10 +39,10 @@ function url() {
     };
 }
 
-@Deserializable
+@Deserializable("url")
 export class URLField extends ObjectField {
     @serializable(url())
-    url: URL;
+    readonly url: URL;
 
     constructor(url: URL) {
         super();
@@ -42,7 +50,7 @@ export class URLField extends ObjectField {
     }
 }
 
-@Deserializable
+@Deserializable("proxy")
 export class ProxyField<T extends RefField> extends ObjectField {
     constructor();
     constructor(value: T);
@@ -50,7 +58,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         super();
         if (value) {
             this.cache = value;
-            this.fieldId = value.__id;
+            this.fieldId = value[Id];
         }
     }
 
@@ -94,19 +102,26 @@ export class ProxyField<T extends RefField> extends ObjectField {
 }
 
 export type Field = number | string | boolean | ObjectField | RefField;
+export type Opt<T> = T | undefined;
+export type FieldWaiting = null;
+export const FieldWaiting: FieldWaiting = null;
 
 const Self = Symbol("Self");
 
-@Deserializable
+@Deserializable("doc").withFields(["id"])
 export class Doc extends RefField {
 
     private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
-        if (prop === "__id" || prop === "__fields") {
+        if (SerializationHelper.IsSerializing()) {
+            target[prop] = value;
+            return true;
+        }
+        if (typeof prop === "symbol") {
             target[prop] = value;
             return true;
         }
         const curValue = target.__fields[prop];
-        if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value.__id)) {
+        if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
             // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
             // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
             return true;
@@ -120,7 +135,7 @@ export class Doc extends RefField {
             }
             value[Parent] = target;
             value[Update] = (diff?: any) => {
-                if (!diff) diff = serialize(value);
+                if (!diff) diff = SerializationHelper.Serialize(value);
                 target[Update]({ [prop]: diff });
             };
         }
@@ -129,7 +144,7 @@ export class Doc extends RefField {
             delete curValue[Update];
         }
         target.__fields[prop] = value;
-        target[Update]({ [prop]: typeof value === "object" ? serialize(value) : value });
+        target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) });
         UndoManager.AddEvent({
             redo: () => receiver[prop] = value,
             undo: () => receiver[prop] = curValue
@@ -141,7 +156,7 @@ export class Doc extends RefField {
         if (typeof prop === "symbol") {
             return target[prop];
         }
-        if (prop === "__id" || prop === "__fields") {
+        if (SerializationHelper.IsSerializing()) {
             return target[prop];
         }
         return Doc.getField(target, prop, receiver);
@@ -174,7 +189,7 @@ export class Doc extends RefField {
         return SerializationHelper.Serialize(doc[Self]);
     }
 
-    constructor(id: string) {
+    constructor(id?: string, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
             set: Doc.setter,
@@ -182,6 +197,9 @@ export class Doc extends RefField {
             deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); },
             defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
         });
+        if (!id || forceSave) {
+            DocServer.CreateField(SerializationHelper.Serialize(doc));
+        }
         return doc;
     }
 
@@ -191,8 +209,8 @@ export class Doc extends RefField {
     @observable
     private __fields: { [key: string]: Field | null | undefined } = {};
 
-    private [Update] = (diff?: any) => {
-        console.log(JSON.stringify(diff || this));
+    private [Update] = (diff: any) => {
+        DocServer.UpdateField(this[Id], diff);
     }
 
     private [Self] = this;
diff --git a/src/server/Message.ts b/src/server/Message.ts
index bbe4ffcad..843a923d1 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -24,6 +24,14 @@ export interface Transferable {
     readonly data?: any;
 }
 
+export interface Reference {
+    readonly id: string;
+}
+
+export interface Diff extends Reference {
+    readonly diff: any;
+}
+
 export namespace MessageStore {
     export const Foo = new Message<string>("Foo");
     export const Bar = new Message<string>("Bar");
@@ -32,4 +40,8 @@ export namespace MessageStore {
     export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back
     export const GetDocument = new Message<string>("Get Document");
     export const DeleteAll = new Message<any>("Delete All");
+
+    export const GetRefField = new Message<string>("Get Ref Field");
+    export const UpdateField = new Message<Diff>("Update Ref Field");
+    export const CreateField = new Message<Reference>("Create Ref Field");
 }
diff --git a/src/server/database.ts b/src/server/database.ts
index 5457e4dd5..a61b4d823 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -13,14 +13,14 @@ export class Database {
         this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
     }
 
-    public update(id: string, value: any, callback: () => void) {
+    public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
         if (this.db) {
-            let collection = this.db.collection('documents');
+            let collection = this.db.collection(collectionName);
             const prom = this.currentWrites[id];
             let newProm: Promise<void>;
             const run = (): Promise<void> => {
                 return new Promise<void>(resolve => {
-                    collection.updateOne({ _id: id }, { $set: value }, { upsert: true }
+                    collection.updateOne({ _id: id }, { $set: value }, { upsert }
                         , (err, res) => {
                             if (err) {
                                 console.log(err.message);
@@ -51,10 +51,12 @@ export class Database {
             this.db && this.db.collection(collectionName).deleteMany({}, res));
     }
 
-    public insert(kvpairs: any, collectionName = Database.DocumentsCollection) {
-        this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) =>
-            err // &&  console.log(err)
-        );
+    public insert(value: any, collectionName = Database.DocumentsCollection) {
+        if ("id" in value) {
+            value._id = value.id;
+            delete value.id;
+        }
+        this.db && this.db.collection(collectionName).insertOne(value);
     }
 
     public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
diff --git a/src/server/index.ts b/src/server/index.ts
index 70a7d266c..d6d5f0e55 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -22,7 +22,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
 import { DashUserModel } from './authentication/models/user_model';
 import { Client } from './Client';
 import { Database } from './database';
-import { MessageStore, Transferable } from "./Message";
+import { MessageStore, Transferable, Diff } from "./Message";
 import { RouteStore } from './RouteStore';
 const app = express();
 const config = require('../../webpack.config');
@@ -232,6 +232,10 @@ server.on("connection", function (socket: Socket) {
     Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
     Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
     Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+
+    Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+    Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+    Utils.AddServerHandler(socket, MessageStore.GetRefField, GetRefField);
 });
 
 function deleteFields() {
@@ -262,5 +266,18 @@ function setField(socket: Socket, newValue: Transferable) {
         socket.broadcast.emit(MessageStore.SetField.Message, newValue));
 }
 
+function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+    Database.Instance.getDocument(id, callback, "newDocuments");
+}
+
+function UpdateField(socket: Socket, diff: Diff) {
+    Database.Instance.update(diff.id, diff.diff,
+        () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+}
+
+function CreateField(newValue: any) {
+    Database.Instance.insert(newValue, "newDocuments");
+}
+
 server.listen(serverPort);
 console.log(`listening on port ${serverPort}`);
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 8eebfed7906e1e2088d528e3af36af21094c38a9 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Thu, 18 Apr 2019 19:17:48 -0400
Subject: Implemented document schemas

---
 src/debug/Test.tsx   | 50 ++++++++++++++++++++++++++---
 src/fields/NewDoc.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 129 insertions(+), 11 deletions(-)

(limited to 'src/debug/Test.tsx')

diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 660115453..b46eb4477 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,9 +1,35 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { serialize, deserialize, map } from 'serializr';
-import { URLField, Doc } from '../fields/NewDoc';
+import { URLField, Doc, createSchema, makeInterface, makeStrictInterface } from '../fields/NewDoc';
 import { SerializationHelper } from '../client/util/SerializationHelper';
 
+const schema1 = createSchema({
+    hello: "number",
+    test: "string",
+    fields: "boolean",
+    url: URLField,
+    testDoc: Doc
+});
+
+const TestDoc = makeInterface(schema1);
+type TestDoc = makeInterface<typeof schema1>;
+
+const schema2 = createSchema({
+    hello: URLField,
+    test: "boolean",
+    fields: "string",
+    url: "number",
+    testDoc: URLField
+});
+
+const Test2Doc = makeStrictInterface(schema2);
+type Test2Doc = makeStrictInterface<typeof schema2>;
+
+const assert = (bool: boolean) => {
+    if (!bool) throw new Error();
+};
+
 class Test extends React.Component {
     onClick = () => {
         const url = new URLField(new URL("http://google.com"));
@@ -15,10 +41,24 @@ class Test extends React.Component {
         doc.url = url;
         doc.testDoc = doc2;
 
-        console.log("doc", doc);
-        const cereal = SerializationHelper.Serialize(doc);
-        console.log("cereal", cereal);
-        console.log("doc again", SerializationHelper.Deserialize(cereal));
+
+        const test1: TestDoc = TestDoc(doc);
+        const test2: Test2Doc = Test2Doc(doc);
+        assert(test1.hello === 5);
+        assert(test1.fields === undefined);
+        assert(test1.test === "hello doc");
+        assert(test1.url === url);
+        assert(test1.testDoc === doc2);
+        test1.myField = 20;
+        assert(test1.myField === 20);
+
+        assert(test2.hello === undefined);
+        assert(test2.fields === "test");
+        assert(test2.test === undefined);
+        assert(test2.url === undefined);
+        assert(test2.testDoc === undefined);
+        test2.url = 35;
+        assert(test2.url === 35);
     }
 
     render() {
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
index 46f2e20e9..150b8dae8 100644
--- a/src/fields/NewDoc.ts
+++ b/src/fields/NewDoc.ts
@@ -1,5 +1,4 @@
 import { observable, action } from "mobx";
-import { Server } from "../client/Server";
 import { UndoManager } from "../client/util/UndoManager";
 import { serializable, primitive, map, alias } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
@@ -185,10 +184,6 @@ export class Doc extends RefField {
         return new Promise(res => Doc.getField(self, key, ignoreProto, res));
     }
 
-    static Serialize(doc: Doc) {
-        return SerializationHelper.Serialize(doc[Self]);
-    }
-
     constructor(id?: string, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -220,4 +215,87 @@ export namespace Doc {
     export const Prototype = Symbol("Prototype");
 }
 
-export const GetAsync = Doc.GetAsync;
\ No newline at end of file
+export const GetAsync = Doc.GetAsync;
+
+interface IDoc {
+    [key: string]: Field | null | undefined;
+}
+
+interface ImageDocument extends IDoc {
+    data: URLField;
+    test: number;
+}
+
+export type ToType<T> =
+    T extends "string" ? string :
+    T extends "number" ? number :
+    T extends "boolean" ? boolean :
+    T extends { new(...args: any[]): infer R } ? R : undefined;
+
+export type ToInterface<T> = {
+    [P in keyof T]: ToType<T[P]>;
+};
+
+interface Interface {
+    [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string";
+}
+
+type FieldCtor<T extends Field> = { new(): T } | "number" | "string" | "boolean";
+
+function Cast<T extends Field>(field: Field | undefined, ctor: FieldCtor<T>): T | undefined {
+    if (field !== undefined) {
+        if (typeof ctor === "string") {
+            if (typeof field === ctor) {
+                return field as T;
+            }
+        } else if (field instanceof ctor) {
+            return field;
+        }
+    }
+    return undefined;
+}
+
+export type makeInterface<T extends Interface> = Partial<ToInterface<T>> & Doc;
+export function makeInterface<T extends Interface>(schema: T): (doc: Doc) => makeInterface<T> {
+    return function (doc: any) {
+        return new Proxy(doc, {
+            get(target, prop) {
+                const field = target[prop];
+                if (prop in schema) {
+                    return Cast(field, (schema as any)[prop]);
+                }
+                return field;
+            }
+        });
+    };
+}
+
+export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
+export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) => makeStrictInterface<T> {
+    const proto = {};
+    for (const key in schema) {
+        const type = schema[key];
+        Object.defineProperty(proto, key, {
+            get() {
+                return Cast(this.__doc[key], type);
+            },
+            set(value) {
+                value = Cast(value, type);
+                if (value !== undefined) {
+                    this.__doc[key] = value;
+                    return;
+                }
+                throw new TypeError("Expected type " + type);
+            }
+        });
+    }
+    return function (doc: any) {
+        const obj = Object.create(proto);
+        obj.__doc = doc;
+        return obj;
+    };
+}
+
+export function createSchema<T extends Interface>(schema: T): T {
+    return schema;
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From be5d2d30bdd98dfc32c28a84ad606eb2b4599932 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 19 Apr 2019 02:40:46 -0400
Subject: Kind of got list typing working for schemas

---
 src/client/views/nodes/ImageBox.tsx | 10 ++++-----
 src/debug/Test.tsx                  | 13 ++++++++---
 src/fields/NewDoc.ts                | 43 +++++++++++++++++++++++++++++--------
 3 files changed, 49 insertions(+), 17 deletions(-)

(limited to 'src/debug/Test.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe0b07bc0..71b431b84 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,7 +38,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
     onLoad = (target: any) => {
         var h = this._imgRef.current!.naturalHeight;
         var w = this._imgRef.current!.naturalWidth;
-        if (this._photoIndex == 0) this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w);
+        if (this._photoIndex === 0) this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w);
     }
 
 
@@ -53,7 +53,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
     onDrop = (e: React.DragEvent) => {
         e.stopPropagation();
         e.preventDefault();
-        console.log("IMPLEMENT ME PLEASE")
+        console.log("IMPLEMENT ME PLEASE");
     }
 
 
@@ -145,9 +145,9 @@ export class ImageBox extends React.Component<FieldViewProps> {
         let left = (nativeWidth - paths.length * dist) / 2;
         return paths.map((p, i) =>
             <div className="imageBox-placer" key={i} >
-                <div className="imageBox-dot" style={{ background: (i == this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+                <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
             </div>
-        )
+        );
     }
 
     render() {
@@ -159,7 +159,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
         let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1);
         return (
             <div className="imageBox-cont" onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
-                <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex == 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} />
+                <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} />
                 {paths.length > 1 ? this.dots(paths) : (null)}
                 {this.lightbox(paths)}
             </div>);
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index b46eb4477..033615be6 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { serialize, deserialize, map } from 'serializr';
-import { URLField, Doc, createSchema, makeInterface, makeStrictInterface } from '../fields/NewDoc';
+import { URLField, Doc, createSchema, makeInterface, makeStrictInterface, List, ListSpec } from '../fields/NewDoc';
 import { SerializationHelper } from '../client/util/SerializationHelper';
 
 const schema1 = createSchema({
@@ -18,7 +18,7 @@ type TestDoc = makeInterface<typeof schema1>;
 const schema2 = createSchema({
     hello: URLField,
     test: "boolean",
-    fields: "string",
+    fields: { List: "number" } as ListSpec<number>,
     url: "number",
     testDoc: URLField
 });
@@ -26,6 +26,13 @@ const schema2 = createSchema({
 const Test2Doc = makeStrictInterface(schema2);
 type Test2Doc = makeStrictInterface<typeof schema2>;
 
+const schema3 = createSchema({
+    test: "boolean",
+});
+
+const Test3Doc = makeStrictInterface(schema3);
+type Test3Doc = makeStrictInterface<typeof schema3>;
+
 const assert = (bool: boolean) => {
     if (!bool) throw new Error();
 };
@@ -53,7 +60,7 @@ class Test extends React.Component {
         assert(test1.myField === 20);
 
         assert(test2.hello === undefined);
-        assert(test2.fields === "test");
+        // assert(test2.fields === "test");
         assert(test2.test === undefined);
         assert(test2.url === undefined);
         assert(test2.testDoc === undefined);
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
index 150b8dae8..7be0d5146 100644
--- a/src/fields/NewDoc.ts
+++ b/src/fields/NewDoc.ts
@@ -107,6 +107,10 @@ export const FieldWaiting: FieldWaiting = null;
 
 const Self = Symbol("Self");
 
+export class List<T extends Field> extends ObjectField {
+    [index: number]: T;
+}
+
 @Deserializable("doc").withFields(["id"])
 export class Doc extends RefField {
 
@@ -230,26 +234,47 @@ export type ToType<T> =
     T extends "string" ? string :
     T extends "number" ? number :
     T extends "boolean" ? boolean :
-    T extends { new(...args: any[]): infer R } ? R : undefined;
+    T extends ListSpec<infer U> ? List<U> :
+    T extends { new(...args: any[]): infer R } ? R : never;
+
+export type ToConstructor<T> =
+    T extends string ? "string" :
+    T extends number ? "number" :
+    T extends boolean ? "boolean" : { new(...args: any[]): T };
 
 export type ToInterface<T> = {
     [P in keyof T]: ToType<T[P]>;
 };
 
+// type ListSpec<T extends Field[]> = { List: FieldCtor<Head<T>> | ListSpec<Tail<T>> };
+export type ListSpec<T> = { List: FieldCtor<T> };
+
+// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
+
+type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
+type Tail<T extends any[]> =
+    ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
+type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+
 interface Interface {
-    [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string";
+    [key: string]: ToConstructor<Field> | ListSpec<Field>;
+    // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
 }
 
-type FieldCtor<T extends Field> = { new(): T } | "number" | "string" | "boolean";
+type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<Field>;
 
-function Cast<T extends Field>(field: Field | undefined, ctor: FieldCtor<T>): T | undefined {
+function Cast<T extends Field>(field: Field | undefined, ctor: FieldCtor<T>): ToType<typeof ctor> | undefined {
     if (field !== undefined) {
         if (typeof ctor === "string") {
             if (typeof field === ctor) {
-                return field as T;
+                return field as ToType<typeof ctor>;
+            }
+        } else if (typeof ctor === "object") {
+            if (field instanceof List) {
+                return field as ToType<typeof ctor>;
             }
-        } else if (field instanceof ctor) {
-            return field;
+        } else if (field instanceof (ctor as any)) {
+            return field as ToType<typeof ctor>;
         }
     }
     return undefined;
@@ -277,10 +302,10 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
         const type = schema[key];
         Object.defineProperty(proto, key, {
             get() {
-                return Cast(this.__doc[key], type);
+                return Cast(this.__doc[key], type as any);
             },
             set(value) {
-                value = Cast(value, type);
+                value = Cast(value, type as any);
                 if (value !== undefined) {
                     this.__doc[key] = value;
                     return;
-- 
cgit v1.2.3-70-g09d2


From ecae4ae106be3e07471208cb93ec0965548d2d12 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 19 Apr 2019 05:40:10 -0400
Subject: Added decent  amount of list support

---
 src/debug/Test.tsx   |   8 +++
 src/fields/NewDoc.ts | 190 ++++++++++++++++++++++++++++-----------------------
 2 files changed, 111 insertions(+), 87 deletions(-)

(limited to 'src/debug/Test.tsx')

diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 033615be6..6a677f80f 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -66,6 +66,14 @@ class Test extends React.Component {
         assert(test2.testDoc === undefined);
         test2.url = 35;
         assert(test2.url === 35);
+        const l = new List<number>();
+        //TODO push, and other array functions don't go through the proxy
+        l.push(1);
+        //TODO currently length, and any other string fields will get serialized
+        l.length = 3;
+        l[2] = 5;
+        console.log(l.slice());
+        console.log(SerializationHelper.Serialize(l));
     }
 
     render() {
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
index 7be0d5146..c22df4b70 100644
--- a/src/fields/NewDoc.ts
+++ b/src/fields/NewDoc.ts
@@ -1,6 +1,6 @@
 import { observable, action } from "mobx";
 import { UndoManager } from "../client/util/UndoManager";
-import { serializable, primitive, map, alias } from "serializr";
+import { serializable, primitive, map, alias, list } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
 import { Utils } from "../Utils";
 import { DocServer } from "../client/DocServer";
@@ -21,9 +21,10 @@ export abstract class RefField {
 }
 
 const Update = Symbol("Update");
+const OnUpdate = Symbol("OnUpdate");
 const Parent = Symbol("Parent");
 export class ObjectField {
-    protected [Update]?: (diff?: any) => void;
+    protected [OnUpdate]?: (diff?: any) => void;
     private [Parent]?: Doc;
 }
 
@@ -107,92 +108,112 @@ export const FieldWaiting: FieldWaiting = null;
 
 const Self = Symbol("Self");
 
-export class List<T extends Field> extends ObjectField {
-    [index: number]: T;
+function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+    if (SerializationHelper.IsSerializing()) {
+        target[prop] = value;
+        return true;
+    }
+    if (typeof prop === "symbol") {
+        target[prop] = value;
+        return true;
+    }
+    const curValue = target.__fields[prop];
+    if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
+        // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
+        // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
+        return true;
+    }
+    if (value instanceof RefField) {
+        value = new ProxyField(value);
+    }
+    if (value instanceof ObjectField) {
+        if (value[Parent] && value[Parent] !== target) {
+            throw new Error("Can't put the same object in multiple documents at the same time");
+        }
+        value[Parent] = target;
+        value[OnUpdate] = (diff?: any) => {
+            if (!diff) diff = SerializationHelper.Serialize(value);
+            target[Update]({ [prop]: diff });
+        };
+    }
+    if (curValue instanceof ObjectField) {
+        delete curValue[Parent];
+        delete curValue[OnUpdate];
+    }
+    target.__fields[prop] = value;
+    target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) });
+    UndoManager.AddEvent({
+        redo: () => receiver[prop] = value,
+        undo: () => receiver[prop] = curValue
+    });
+    return true;
 }
 
-@Deserializable("doc").withFields(["id"])
-export class Doc extends RefField {
-
-    private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
-        if (SerializationHelper.IsSerializing()) {
-            target[prop] = value;
-            return true;
-        }
-        if (typeof prop === "symbol") {
-            target[prop] = value;
-            return true;
-        }
-        const curValue = target.__fields[prop];
-        if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
-            // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
-            // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
-            return true;
-        }
-        if (value instanceof RefField) {
-            value = new ProxyField(value);
-        }
-        if (value instanceof ObjectField) {
-            if (value[Parent] && value[Parent] !== target) {
-                throw new Error("Can't put the same object in multiple documents at the same time");
-            }
-            value[Parent] = target;
-            value[Update] = (diff?: any) => {
-                if (!diff) diff = SerializationHelper.Serialize(value);
-                target[Update]({ [prop]: diff });
-            };
-        }
-        if (curValue instanceof ObjectField) {
-            delete curValue[Parent];
-            delete curValue[Update];
-        }
-        target.__fields[prop] = value;
-        target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) });
-        UndoManager.AddEvent({
-            redo: () => receiver[prop] = value,
-            undo: () => receiver[prop] = curValue
-        });
-        return true;
+function getter(target: any, prop: string | symbol | number, receiver: any): any {
+    if (typeof prop === "symbol") {
+        return target.__fields[prop] || target[prop];
+    }
+    if (SerializationHelper.IsSerializing()) {
+        return target[prop];
     }
+    return getField(target, prop, receiver);
+}
 
-    private static getter(target: any, prop: string | symbol | number, receiver: any): any {
-        if (typeof prop === "symbol") {
-            return target[prop];
-        }
-        if (SerializationHelper.IsSerializing()) {
-            return target[prop];
+function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+    const field = target.__fields[prop];
+    if (field instanceof ProxyField) {
+        return field.value(callback);
+    }
+    if (field === undefined && !ignoreProto) {
+        const proto = getField(target, "prototype", true);
+        if (proto instanceof Doc) {
+            let field = proto[prop];
+            callback && callback(field === null ? undefined : field);
+            return field;
         }
-        return Doc.getField(target, prop, receiver);
     }
+    callback && callback(field);
+    return field;
 
-    private static getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
-        const field = target.__fields[prop];
-        if (field instanceof ProxyField) {
-            return field.value(callback);
-        }
-        if (field === undefined && !ignoreProto) {
-            const proto = Doc.getField(target, "prototype", true);
-            if (proto instanceof Doc) {
-                let field = proto[prop];
-                callback && callback(field === null ? undefined : field);
-                return field;
-            }
-        }
-        callback && callback(field);
-        return field;
+}
 
+@Deserializable("list")
+class ListImpl<T extends Field> extends ObjectField {
+    constructor() {
+        super();
+        const list = new Proxy<this>(this, {
+            set: function (a, b, c, d) { return setter(a, b, c, d); },
+            get: getter,
+            deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); },
+            defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+        });
+        return list;
     }
 
-    static GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
-        const self = doc[Self];
-        return new Promise(res => Doc.getField(self, key, ignoreProto, res));
+    [key: number]: T | null | undefined;
+
+    @serializable(alias("fields", list(autoObject())))
+    @observable
+    private __fields: (T | null | undefined)[] = [];
+
+    private [Update] = (diff: any) => {
+        console.log(diff);
+        const update = this[OnUpdate];
+        update && update(diff);
     }
 
+    private [Self] = this;
+}
+export type List<T extends Field> = ListImpl<T> & T[];
+export const List: { new <T extends Field>(): List<T> } = ListImpl as any;
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
     constructor(id?: string, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
-            set: Doc.setter,
-            get: Doc.getter,
+            set: setter,
+            get: getter,
             deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); },
             defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
         });
@@ -216,20 +237,15 @@ export class Doc extends RefField {
 }
 
 export namespace Doc {
+    export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
+        const self = doc[Self];
+        return new Promise(res => getField(self, key, ignoreProto, res));
+    }
     export const Prototype = Symbol("Prototype");
 }
 
 export const GetAsync = Doc.GetAsync;
 
-interface IDoc {
-    [key: string]: Field | null | undefined;
-}
-
-interface ImageDocument extends IDoc {
-    data: URLField;
-    test: number;
-}
-
 export type ToType<T> =
     T extends "string" ? string :
     T extends "number" ? number :
@@ -261,20 +277,20 @@ interface Interface {
     // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
 }
 
-type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<Field>;
+type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<T>;
 
-function Cast<T extends Field>(field: Field | undefined, ctor: FieldCtor<T>): ToType<typeof ctor> | undefined {
+function Cast<T extends FieldCtor<Field>>(field: Field | undefined, ctor: T): ToType<T> | undefined {
     if (field !== undefined) {
         if (typeof ctor === "string") {
             if (typeof field === ctor) {
-                return field as ToType<typeof ctor>;
+                return field as ToType<T>;
             }
         } else if (typeof ctor === "object") {
             if (field instanceof List) {
-                return field as ToType<typeof ctor>;
+                return field as ToType<T>;
             }
         } else if (field instanceof (ctor as any)) {
-            return field as ToType<typeof ctor>;
+            return field as ToType<T>;
         }
     }
     return undefined;
-- 
cgit v1.2.3-70-g09d2

