From 79d8b5c812db5c28f477ace8db0ee4d9e18a84b7 Mon Sep 17 00:00:00 2001 From: Tyler Schicke 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
Hello {this.props.firstName} {this.props.lastName}
; +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 = ""; - let bindings = { - props: { - firstName: "First", - lastName: "Last" - } - }; - return ; + return ; } } -ReactDOM.render(( -
- -
), +ReactDOM.render( + , 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 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 extends ObjectField { constructor(); constructor(value: T); @@ -48,7 +50,7 @@ export class ProxyField extends ObjectField { super(); if (value) { this.cache = value; - this.fieldId = value[Id]; + this.fieldId = value.__id; } } @@ -94,11 +96,17 @@ export class ProxyField 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, { @@ -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 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> } = {}; + const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); + const GUID: string = Utils.GenerateGuid(); + + export async function GetRefField(id: string): Promise> { + 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) => { + 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 extends ObjectField { constructor(); constructor(value: T); @@ -50,7 +58,7 @@ export class ProxyField extends ObjectField { super(); if (value) { this.cache = value; - this.fieldId = value.__id; + this.fieldId = value[Id]; } } @@ -94,19 +102,26 @@ export class ProxyField extends ObjectField { } export type Field = number | string | boolean | ObjectField | RefField; +export type Opt = 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, { 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("Foo"); export const Bar = new Message("Bar"); @@ -32,4 +40,8 @@ export namespace MessageStore { export const GetFields = new Message("Get Fields"); // send string[] of 'id' get Transferable[] back export const GetDocument = new Message("Get Document"); export const DeleteAll = new Message("Delete All"); + + export const GetRefField = new Message("Get Ref Field"); + export const UpdateField = new Message("Update Ref Field"); + export const CreateField = new Message("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; const run = (): Promise => { return new Promise(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 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; + +const schema2 = createSchema({ + hello: URLField, + test: "boolean", + fields: "string", + url: "number", + testDoc: URLField +}); + +const Test2Doc = makeStrictInterface(schema2); +type Test2Doc = makeStrictInterface; + +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, { @@ -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 extends "string" ? string : + T extends "number" ? number : + T extends "boolean" ? boolean : + T extends { new(...args: any[]): infer R } ? R : undefined; + +export type ToInterface = { + [P in keyof T]: ToType; +}; + +interface Interface { + [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string"; +} + +type FieldCtor = { new(): T } | "number" | "string" | "boolean"; + +function Cast(field: Field | undefined, ctor: FieldCtor): 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 = Partial> & Doc; +export function makeInterface(schema: T): (doc: Doc) => makeInterface { + 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 = Partial>; +export function makeStrictInterface(schema: T): (doc: Doc) => makeStrictInterface { + 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(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 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 { 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 { 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 { let left = (nativeWidth - paths.length * dist) / 2; return paths.map((p, i) =>
-
{ e.stopPropagation(); this.onDotDown(i); }} /> +
{ e.stopPropagation(); this.onDotDown(i); }} />
- ) + ); } render() { @@ -159,7 +159,7 @@ export class ImageBox extends React.Component { let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); return (
- Image not found + Image not found {paths.length > 1 ? this.dots(paths) : (null)} {this.lightbox(paths)}
); 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; const schema2 = createSchema({ hello: URLField, test: "boolean", - fields: "string", + fields: { List: "number" } as ListSpec, url: "number", testDoc: URLField }); @@ -26,6 +26,13 @@ const schema2 = createSchema({ const Test2Doc = makeStrictInterface(schema2); type Test2Doc = makeStrictInterface; +const schema3 = createSchema({ + test: "boolean", +}); + +const Test3Doc = makeStrictInterface(schema3); +type Test3Doc = makeStrictInterface; + 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 extends ObjectField { + [index: number]: T; +} + @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { @@ -230,26 +234,47 @@ export type ToType = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : - T extends { new(...args: any[]): infer R } ? R : undefined; + T extends ListSpec ? List : + T extends { new(...args: any[]): infer R } ? R : never; + +export type ToConstructor = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : { new(...args: any[]): T }; export type ToInterface = { [P in keyof T]: ToType; }; +// type ListSpec = { List: FieldCtor> | ListSpec> }; +export type ListSpec = { List: FieldCtor }; + +// type ListType = { 0: List>>, 1: ToType> }[HasTail extends true ? 0 : 1]; + +type Head = T extends [any, ...any[]] ? T[0] : never; +type Tail = + ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; +type HasTail = T extends ([] | [any]) ? false : true; + interface Interface { - [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string"; + [key: string]: ToConstructor | ListSpec; + // [key: string]: ToConstructor | ListSpec; } -type FieldCtor = { new(): T } | "number" | "string" | "boolean"; +type FieldCtor = ToConstructor | ListSpec; -function Cast(field: Field | undefined, ctor: FieldCtor): T | undefined { +function Cast(field: Field | undefined, ctor: FieldCtor): ToType | undefined { if (field !== undefined) { if (typeof ctor === "string") { if (typeof field === ctor) { - return field as T; + return field as ToType; + } + } else if (typeof ctor === "object") { + if (field instanceof List) { + return field as ToType; } - } else if (field instanceof ctor) { - return field; + } else if (field instanceof (ctor as any)) { + return field as ToType; } } return undefined; @@ -277,10 +302,10 @@ export function makeStrictInterface(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 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(); + //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 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 extends ObjectField { + constructor() { + super(); + const list = new Proxy(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 { - 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 = ListImpl & T[]; +export const List: { new (): List } = ListImpl as any; + +@Deserializable("doc").withFields(["id"]) +export class Doc extends RefField { constructor(id?: string, forceSave?: boolean) { super(id); const doc = new Proxy(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 { + 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 extends "string" ? string : T extends "number" ? number : @@ -261,20 +277,20 @@ interface Interface { // [key: string]: ToConstructor | ListSpec; } -type FieldCtor = ToConstructor | ListSpec; +type FieldCtor = ToConstructor | ListSpec; -function Cast(field: Field | undefined, ctor: FieldCtor): ToType | undefined { +function Cast>(field: Field | undefined, ctor: T): ToType | undefined { if (field !== undefined) { if (typeof ctor === "string") { if (typeof field === ctor) { - return field as ToType; + return field as ToType; } } else if (typeof ctor === "object") { if (field instanceof List) { - return field as ToType; + return field as ToType; } } else if (field instanceof (ctor as any)) { - return field as ToType; + return field as ToType; } } return undefined; -- cgit v1.2.3-70-g09d2