aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts2
-rw-r--r--src/client/util/SerializationHelper.ts2
-rw-r--r--src/debug/Test.tsx8
-rw-r--r--src/fields/HtmlField.ts25
-rw-r--r--src/fields/ListField.ts196
-rw-r--r--src/fields/NewDoc.ts358
-rw-r--r--src/fields/TupleField.ts59
-rw-r--r--src/new_fields/Doc.ts90
-rw-r--r--src/new_fields/HtmlField.ts14
-rw-r--r--src/new_fields/List.ts35
-rw-r--r--src/new_fields/Proxy.ts55
-rw-r--r--src/new_fields/Schema.ts47
-rw-r--r--src/new_fields/Types.ts58
-rw-r--r--src/new_fields/URLField.ts25
-rw-r--r--src/new_fields/util.ts73
15 files changed, 405 insertions, 642 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 9a3e122e8..615e48af0 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,6 +1,6 @@
import * as OpenSocket from 'socket.io-client';
import { MessageStore, Types } from "./../server/Message";
-import { Opt, FieldWaiting, RefField, HandleUpdate } from '../fields/NewDoc';
+import { Opt, FieldWaiting, RefField, HandleUpdate } from '../new_fields/Doc';
import { Utils } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 7273c3fe4..ac70aba9d 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,5 +1,5 @@
import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
-import { Field } from "../../fields/NewDoc";
+import { Field } from "../../new_fields/Doc";
export namespace SerializationHelper {
let serializing: number = 0;
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 6a677f80f..8b9c9fa0b 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,8 +1,12 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import { serialize, deserialize, map } from 'serializr';
-import { URLField, Doc, createSchema, makeInterface, makeStrictInterface, List, ListSpec } from '../fields/NewDoc';
import { SerializationHelper } from '../client/util/SerializationHelper';
+import { createSchema, makeInterface, makeStrictInterface } from '../new_fields/Schema';
+import { URLField } from '../new_fields/URLField';
+import { Doc } from '../new_fields/Doc';
+import { ListSpec } from '../new_fields/Types';
+import { List } from '../new_fields/List';
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
const schema1 = createSchema({
hello: "number",
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
deleted file mode 100644
index a1d880070..000000000
--- a/src/fields/HtmlField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class HtmlField extends BasicField<string> {
- constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new HtmlField("${this.Data}")`;
- }
-
- Copy() {
- return new HtmlField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Html,
- data: this.Data,
- id: this.Id,
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
deleted file mode 100644
index e24099126..000000000
--- a/src/fields/ListField.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { FieldMap } from "../client/SocketStub";
-import { ScriptField } from "./ScriptField";
-
-export class ListField<T extends Field> extends BasicField<T[]> {
- private _proxies: string[] = [];
- private _scriptIds: string[] = [];
- private scripts: ScriptField[] = [];
-
- constructor(data: T[] = [], scripts: ScriptField[] = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- this.scripts = scripts;
- this.updateProxies();
- this._scriptIds = this.scripts.map(script => script.Id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeList();
- }
-
- private _processingServerUpdate: boolean = false;
-
- private observeDisposer: Lambda | undefined;
- private observeList(): void {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
- const target = change.object;
- this.updateProxies();
- if (change.type === "splice") {
- this.runScripts(change.removed, false);
- UndoManager.AddEvent({
- undo: () => target.splice(change.index, change.addedCount, ...change.removed),
- redo: () => target.splice(change.index, change.removedCount, ...change.added)
- });
- this.runScripts(change.added, true);
- } else {
- this.runScripts([change.oldValue], false);
- UndoManager.AddEvent({
- undo: () => target[change.index] = change.oldValue,
- redo: () => target[change.index] = change.newValue
- });
- this.runScripts([change.newValue], true);
- }
- if (!this._processingServerUpdate) {
- Server.UpdateField(this);
- }
- });
- }
-
- private runScripts(fields: T[], added: boolean) {
- for (const script of this.scripts) {
- this.runScript(fields, script, added);
- }
- }
-
- private runScript(fields: T[], script: ScriptField, added: boolean) {
- if (!this._processingServerUpdate) {
- for (const field of fields) {
- script.script.run({ field, added });
- }
- }
- }
-
- addScript(script: ScriptField) {
- this.scripts.push(script);
- this._scriptIds.push(script.Id);
-
- this.runScript(this.Data, script, true);
- UndoManager.AddEvent({
- undo: () => this.removeScript(script),
- redo: () => this.addScript(script),
- });
- Server.UpdateField(this);
- }
-
- removeScript(script: ScriptField) {
- const index = this.scripts.indexOf(script);
- if (index === -1) {
- return;
- }
- this.scripts.splice(index, 1);
- this._scriptIds.splice(index, 1);
- UndoManager.AddEvent({
- undo: () => this.addScript(script),
- redo: () => this.removeScript(script),
- });
- this.runScript(this.Data, script, false);
- Server.UpdateField(this);
- }
-
- protected setData(value: T[]) {
- this.runScripts(this.data, false);
-
- this.data = observable(value);
- this.updateProxies();
- this.observeList();
- this.runScripts(this.data, true);
- }
-
- private updateProxies() {
- this._proxies = this.Data.map(field => field.Id);
- }
-
- private arraysEqual(a: any[], b: any[]) {
- if (a === b) return true;
- if (a === null || b === null) return false;
- if (a.length !== b.length) return false;
-
- // If you don't care about the order of the elements inside
- // the array, you should sort both arrays here.
- // Please note that calling sort on an array will modify that array.
- // you might want to clone your array first.
-
- for (var i = 0; i < a.length; ++i) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
-
- init(callback: (field: Field) => any) {
- const fieldsPromise = Server.GetFields(this._proxies).then(action((fields: FieldMap) => {
- if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
- var dataids = this.data.map(d => d.Id);
- var proxies = this._proxies.map(p => p);
- var added = this.data.length < this._proxies.length;
- var deleted = this.data.length > this._proxies.length;
- for (let i = 0; i < dataids.length && added; i++) {
- added = proxies.indexOf(dataids[i]) !== -1;
- }
- for (let i = 0; i < this._proxies.length && deleted; i++) {
- deleted = dataids.indexOf(proxies[i]) !== -1;
- }
-
- this._processingServerUpdate = true;
- for (let i = 0; i < proxies.length && added; i++) {
- if (dataids.indexOf(proxies[i]) === -1) {
- this.Data.splice(i, 0, fields[proxies[i]] as T);
- }
- }
- for (let i = dataids.length - 1; i >= 0 && deleted; i--) {
- if (proxies.indexOf(dataids[i]) === -1) {
- this.Data.splice(i, 1);
- }
- }
- if (!added && !deleted) {// otherwise, just rebuild the whole list
- this.setData(proxies.map(id => fields[id] as T));
- }
- this._processingServerUpdate = false;
- }
- }));
-
- const scriptsPromise = Server.GetFields(this._scriptIds).then((fields: FieldMap) => {
- this.scripts = this._scriptIds.map(id => fields[id] as ScriptField);
- });
-
- Promise.all([fieldsPromise, scriptsPromise]).then(() => callback(this));
- }
-
- ToScriptString(): string {
- return "new ListField([" + this.Data.map(field => field.ToScriptString()).join(", ") + "])";
- }
-
- Copy(): Field {
- return new ListField<T>(this.Data);
- }
-
-
- UpdateFromServer(data: { fields: string[], scripts: string[] }) {
- this._proxies = data.fields;
- this._scriptIds = data.scripts;
- }
- ToJson() {
- return {
- type: Types.List,
- data: {
- fields: this._proxies,
- scripts: this._scriptIds,
- },
- id: this.Id
- };
- }
-
- static FromJson(id: string, data: { fields: string[], scripts: string[] }): ListField<Field> {
- let list = new ListField([], [], id, false);
- list._proxies = data.fields;
- list._scriptIds = data.scripts;
- return list;
- }
-} \ No newline at end of file
diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts
deleted file mode 100644
index 70dc1867c..000000000
--- a/src/fields/NewDoc.ts
+++ /dev/null
@@ -1,358 +0,0 @@
-import { observable, action } from "mobx";
-import { UndoManager } from "../client/util/UndoManager";
-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";
-
-export const HandleUpdate = Symbol("HandleUpdate");
-const Id = Symbol("Id");
-export abstract class RefField {
- @serializable(alias("id", primitive()))
- private __id: string;
- readonly [Id]: string;
-
- constructor(id?: string) {
- this.__id = id || Utils.GenerateGuid();
- this[Id] = this.__id;
- }
-
- protected [HandleUpdate]?(diff: any): void;
-}
-
-const Update = Symbol("Update");
-const OnUpdate = Symbol("OnUpdate");
-const Parent = Symbol("Parent");
-export class ObjectField {
- protected [OnUpdate]?: (diff?: any) => void;
- private [Parent]?: Doc;
-}
-
-function url() {
- return {
- serializer: function (value: URL) {
- return value.href;
- },
- deserializer: function (jsonValue: string, done: (err: any, val: any) => void) {
- done(undefined, new URL(jsonValue));
- }
- };
-}
-
-@Deserializable("url")
-export class URLField extends ObjectField {
- @serializable(url())
- readonly url: URL;
-
- constructor(url: URL) {
- super();
- this.url = url;
- }
-}
-
-@Deserializable("proxy")
-export class ProxyField<T extends RefField> extends ObjectField {
- constructor();
- constructor(value: T);
- constructor(value?: T) {
- super();
- if (value) {
- this.cache = value;
- this.fieldId = value[Id];
- }
- }
-
- @serializable(primitive())
- readonly fieldId: string = "";
-
- // This getter/setter and nested object thing is
- // because mobx doesn't play well with observable proxies
- @observable.ref
- private _cache: { readonly field: T | undefined } = { field: undefined };
- private get cache(): T | undefined {
- return this._cache.field;
- }
- private set cache(field: T | undefined) {
- this._cache = { field };
- }
-
- private failed = false;
- private promise?: Promise<any>;
-
- value(callback?: ((field: T | undefined) => void)): T | undefined | null {
- if (this.cache) {
- callback && callback(this.cache);
- return this.cache;
- }
- if (this.failed) {
- return undefined;
- }
- if (!this.promise) {
- // this.promise = Server.GetField(this.fieldId).then(action((field: any) => {
- // this.promise = undefined;
- // this.cache = field;
- // if (field === undefined) this.failed = true;
- // return field;
- // }));
- this.promise = new Promise(r => r());
- }
- callback && this.promise.then(callback);
- return null;
- }
-}
-
-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");
-
-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;
-}
-
-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);
-}
-
-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;
- }
- }
- 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;
- }
-
- [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: 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"); },
- });
- if (!id || forceSave) {
- DocServer.CreateField(SerializationHelper.Serialize(doc));
- }
- return doc;
- }
-
- [key: string]: Field | null | undefined;
-
- @serializable(alias("fields", map(autoObject())))
- @observable
- private __fields: { [key: string]: Field | null | undefined } = {};
-
- private [Update] = (diff: any) => {
- DocServer.UpdateField(this[Id], diff);
- }
-
- private [Self] = this;
-}
-
-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 function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
- const self = doc[Self];
- return new Promise(async res => {
- const field = await GetAsync(doc, key, ignoreProto);
- return Cast(field, ctor);
- });
- }
- export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined {
- const self = doc[Self];
- return getField(self, key, ignoreProto);
- }
- export function GetT<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Field | null | undefined {
- return Cast(Get(doc, key, ignoreProto), ctor);
- }
- export const Prototype = Symbol("Prototype");
-}
-
-export const GetAsync = Doc.GetAsync;
-
-export type ToType<T> =
- T extends "string" ? string :
- T extends "number" ? number :
- T extends "boolean" ? boolean :
- 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]: ToConstructor<Field> | ListSpec<Field>;
- // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
-}
-
-type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<T>;
-
-function Cast<T extends FieldCtor<Field>>(field: Field | null | undefined, ctor: T): ToType<T> | null | undefined {
- if (field !== undefined && field !== null) {
- if (typeof ctor === "string") {
- if (typeof field === ctor) {
- return field as ToType<T>;
- }
- } else if (typeof ctor === "object") {
- if (field instanceof List) {
- return field as ToType<T>;
- }
- } else if (field instanceof (ctor as any)) {
- return field as ToType<T>;
- }
- } else {
- 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 as any);
- },
- set(value) {
- value = Cast(value, type as any);
- 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
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
deleted file mode 100644
index 347f1fa05..000000000
--- a/src/fields/TupleField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-
-export class TupleField<T, U> extends BasicField<[T, U]> {
- constructor(data: [T, U], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeTuple();
- }
-
- private observeDisposer: Lambda | undefined;
- private observeTuple(): void {
- this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
- if (change.type === "update") {
- UndoManager.AddEvent({
- undo: () => this.Data[change.index] = change.oldValue,
- redo: () => this.Data[change.index] = change.newValue
- });
- Server.UpdateField(this);
- } else {
- throw new Error("Why are you messing with the length of a tuple, huh?");
- }
- });
- }
-
- protected setData(value: [T, U]) {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.data = observable(value) as (T | U)[] as [T, U];
- this.observeTuple();
- }
-
- UpdateFromServer(values: [T, U]) {
- this.setData(values);
- }
-
- ToScriptString(): string {
- return `new TupleField([${this.Data[0], this.Data[1]}])`;
- }
-
- Copy(): Field {
- return new TupleField<T, U>(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Tuple,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
new file mode 100644
index 000000000..c67170573
--- /dev/null
+++ b/src/new_fields/Doc.ts
@@ -0,0 +1,90 @@
+import { observable, action } from "mobx";
+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";
+import { setter, getter, getField } from "./util";
+import { Cast, FieldCtor } from "./Types";
+
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export abstract class RefField {
+ @serializable(alias("id", primitive()))
+ private __id: string;
+ readonly [Id]: string;
+
+ constructor(id?: string) {
+ this.__id = id || Utils.GenerateGuid();
+ this[Id] = this.__id;
+ }
+
+ protected [HandleUpdate]?(diff: any): void;
+}
+
+export const Update = Symbol("Update");
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export class ObjectField {
+ protected [OnUpdate]?: (diff?: any) => void;
+ private [Parent]?: Doc;
+}
+
+export type Field = number | string | boolean | ObjectField | RefField;
+export type Opt<T> = T | undefined;
+export type FieldWaiting = null;
+export const FieldWaiting: FieldWaiting = null;
+
+export const Self = Symbol("Self");
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
+ constructor(id?: string, forceSave?: boolean) {
+ super(id);
+ const doc = new Proxy<this>(this, {
+ 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"); },
+ });
+ if (!id || forceSave) {
+ DocServer.CreateField(SerializationHelper.Serialize(doc));
+ }
+ return doc;
+ }
+
+ [key: string]: Field | null | undefined;
+
+ @serializable(alias("fields", map(autoObject())))
+ @observable
+ private __fields: { [key: string]: Field | null | undefined } = {};
+
+ private [Update] = (diff: any) => {
+ DocServer.UpdateField(this[Id], diff);
+ }
+
+ private [Self] = this;
+}
+
+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 function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+ const self = doc[Self];
+ return new Promise(async res => {
+ const field = await GetAsync(doc, key, ignoreProto);
+ return Cast(field, ctor);
+ });
+ }
+ export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined {
+ const self = doc[Self];
+ return getField(self, key, ignoreProto);
+ }
+ export function GetT<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Field | null | undefined {
+ return Cast(Get(doc, key, ignoreProto), ctor);
+ }
+ export const Prototype = Symbol("Prototype");
+}
+
+export const GetAsync = Doc.GetAsync; \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
new file mode 100644
index 000000000..f8e54ade5
--- /dev/null
+++ b/src/new_fields/HtmlField.ts
@@ -0,0 +1,14 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField } from "./Doc";
+
+@Deserializable("html")
+export class URLField extends ObjectField {
+ @serializable(primitive())
+ readonly html: string;
+
+ constructor(html: string) {
+ super();
+ this.html = html;
+ }
+}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
new file mode 100644
index 000000000..a1a623f83
--- /dev/null
+++ b/src/new_fields/List.ts
@@ -0,0 +1,35 @@
+import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Field, ObjectField, Update, OnUpdate, Self } from "./Doc";
+import { setter, getter } from "./util";
+import { serializable, alias, list } from "serializr";
+import { observable } from "mobx";
+
+@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;
+ }
+
+ [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; \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
new file mode 100644
index 000000000..3b4b2e452
--- /dev/null
+++ b/src/new_fields/Proxy.ts
@@ -0,0 +1,55 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { RefField, Id, ObjectField } from "./Doc";
+import { primitive, serializable } from "serializr";
+import { observable } from "mobx";
+
+@Deserializable("proxy")
+export class ProxyField<T extends RefField> extends ObjectField {
+ constructor();
+ constructor(value: T);
+ constructor(value?: T) {
+ super();
+ if (value) {
+ this.cache = value;
+ this.fieldId = value[Id];
+ }
+ }
+
+ @serializable(primitive())
+ readonly fieldId: string = "";
+
+ // This getter/setter and nested object thing is
+ // because mobx doesn't play well with observable proxies
+ @observable.ref
+ private _cache: { readonly field: T | undefined } = { field: undefined };
+ private get cache(): T | undefined {
+ return this._cache.field;
+ }
+ private set cache(field: T | undefined) {
+ this._cache = { field };
+ }
+
+ private failed = false;
+ private promise?: Promise<any>;
+
+ value(callback?: ((field: T | undefined) => void)): T | undefined | null {
+ if (this.cache) {
+ callback && callback(this.cache);
+ return this.cache;
+ }
+ if (this.failed) {
+ return undefined;
+ }
+ if (!this.promise) {
+ // this.promise = Server.GetField(this.fieldId).then(action((field: any) => {
+ // this.promise = undefined;
+ // this.cache = field;
+ // if (field === undefined) this.failed = true;
+ // return field;
+ // }));
+ this.promise = new Promise(r => r());
+ }
+ callback && this.promise.then(callback);
+ return null;
+ }
+}
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
new file mode 100644
index 000000000..c7d2f0801
--- /dev/null
+++ b/src/new_fields/Schema.ts
@@ -0,0 +1,47 @@
+import { Interface, ToInterface, Cast } from "./Types";
+import { Doc } from "./Doc";
+
+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 as any);
+ },
+ set(value) {
+ value = Cast(value, type as any);
+ 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;
+}
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
new file mode 100644
index 000000000..416298a64
--- /dev/null
+++ b/src/new_fields/Types.ts
@@ -0,0 +1,58 @@
+import { Field, Opt } from "./Doc";
+import { List } from "./List";
+
+export type ToType<T> =
+ T extends "string" ? string :
+ T extends "number" ? number :
+ T extends "boolean" ? boolean :
+ 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];
+
+export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
+export type Tail<T extends any[]> =
+ ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
+export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+
+export interface Interface {
+ [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
+}
+
+export type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<T>;
+
+export function Cast<T extends FieldCtor<Field>>(field: Field | null | undefined, ctor: T): ToType<T> | null | undefined {
+ if (field !== undefined && field !== null) {
+ if (typeof ctor === "string") {
+ if (typeof field === ctor) {
+ return field as ToType<T>;
+ }
+ } else if (typeof ctor === "object") {
+ if (field instanceof List) {
+ return field as ToType<T>;
+ }
+ } else if (field instanceof (ctor as any)) {
+ return field as ToType<T>;
+ }
+ } else {
+ return field;
+ }
+ return undefined;
+}
+
+export function FieldValue<T extends Field>(field: Opt<Field> | Promise<Opt<Field>>): Opt<Field> {
+ return field instanceof Promise ? undefined : field;
+}
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
new file mode 100644
index 000000000..d27a2b692
--- /dev/null
+++ b/src/new_fields/URLField.ts
@@ -0,0 +1,25 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable } from "serializr";
+import { ObjectField } from "./Doc";
+
+function url() {
+ return {
+ serializer: function (value: URL) {
+ return value.href;
+ },
+ deserializer: function (jsonValue: string, done: (err: any, val: any) => void) {
+ done(undefined, new URL(jsonValue));
+ }
+ };
+}
+
+@Deserializable("url")
+export class URLField extends ObjectField {
+ @serializable(url())
+ readonly url: URL;
+
+ constructor(url: URL) {
+ super();
+ this.url = url;
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
new file mode 100644
index 000000000..0f08ecf03
--- /dev/null
+++ b/src/new_fields/util.ts
@@ -0,0 +1,73 @@
+import { UndoManager } from "../client/util/UndoManager";
+import { Update, OnUpdate, Parent, ObjectField, RefField, Doc, Id, Field } from "./Doc";
+import { SerializationHelper } from "../client/util/SerializationHelper";
+import { ProxyField } from "./Proxy";
+
+export 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;
+}
+
+export 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);
+}
+
+export 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;
+ }
+ }
+ callback && callback(field);
+ return field;
+
+}