aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts72
-rw-r--r--src/client/util/SerializationHelper.ts73
-rw-r--r--src/debug/Test.tsx6
-rw-r--r--src/fields/NewDoc.ts52
-rw-r--r--src/server/Message.ts12
-rw-r--r--src/server/database.ts16
-rw-r--r--src/server/index.ts19
7 files changed, 212 insertions, 38 deletions
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