aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Utils.ts10
-rw-r--r--src/client/DocServer.ts285
-rw-r--r--src/client/util/SerializationHelper.ts4
-rw-r--r--src/new_fields/Doc.ts4
-rw-r--r--src/new_fields/util.ts4
-rw-r--r--src/server/index.ts12
6 files changed, 235 insertions, 84 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 611c61135..657bd673e 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -73,14 +73,14 @@ export class Utils {
};
}
- public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
+ public static emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
this.log("Emit", message.Name, args, false);
socket.emit(message.Message, args);
}
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
+ public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
+ public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
+ public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
this.log("Emit", message.Name, args, false);
if (fn) {
socket.emit(message.Message, args, this.loggingCallback('Receiving', fn, message.Name));
@@ -89,7 +89,7 @@ export class Utils {
}
}
- public static AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
+ public static addServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
socket.on(message.Message, this.loggingCallback('Incoming', handler, message.Name));
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index d759b4757..ad7c706b6 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,35 +1,116 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore } from "./../server/Message";
+import { MessageStore, Diff } from "./../server/Message";
import { Opt } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../new_fields/RefField';
import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+/**
+ * This class encapsulates the transfer and cross-client synchronization of
+ * data stored only in documents (RefFields). In the process, it also
+ * creates and maintains a cache of documents so that they can be accessed
+ * more efficiently. Currently, there is no cache eviction scheme in place.
+ *
+ * NOTE: while this class is technically abstracted to work with any [RefField], because
+ * [Doc] instances are the only [RefField] we need / have implemented at the moment, the documentation
+ * will treat all data used here as [Doc]s
+ *
+ * Any time we want to write a new field to the database (via the server)
+ * or update ourselves based on the server's update message, that occurs here
+ */
export namespace DocServer {
+ // a document cache for efficient document retrieval
const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ // the handle / client side endpoint of the web socket (https://bit.ly/2TeALea for more info) connection established with the server
const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+ // this client's distinct GUID created at initialization
const GUID: string = Utils.GenerateGuid();
+ // indicates whether or not a document is currently being udpated, and, if so, its id
+ let updatingId: string | undefined;
- export function makeReadOnly() {
- _CreateField = emptyFunction;
- _UpdateField = emptyFunction;
- _respondToUpdate = emptyFunction;
- }
+ export namespace Util {
- export function prepend(extension: string): string {
- return window.location.origin + extension;
- }
+ /**
+ * Whenever the server sends us its handshake message on our
+ * websocket, we use the above function to return the handshake.
+ */
+ Utils.addServerHandler(_socket, MessageStore.Foo, onConnection);
+
+ /**
+ * This function emits a message (with this client's
+ * unique GUID) to the server
+ * indicating that this client has connected
+ */
+ function onConnection() {
+ _socket.emit(MessageStore.Bar.Message, GUID);
+ }
+
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
+ export function prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+
+ /**
+ * Emits a message to the server that wipes
+ * all documents in the database.
+ */
+ export function deleteDatabase() {
+ Utils.emit(_socket, MessageStore.DeleteAll, {});
+ }
+
+ /**
+ * This disables this client's ability to write new fields,
+ * update existing fields, and update and reflect the changes if
+ * other clients update shared fields. Thus, the client can only read
+ * a static snapshot of their workspaces
+ *
+ * Currently this is conditionally called in MainView.tsx when analyzing
+ * the document's url.
+ */
+ export function makeReadOnly() {
+ // replaces default functionality with no-ops
+ _createField = emptyFunction;
+ _emitFieldUpdate = emptyFunction;
+ _respondToUpdate = emptyFunction;
+ }
- export function DeleteDatabase() {
- Utils.Emit(_socket, MessageStore.DeleteAll, {});
}
- export async function GetRefField(id: string): Promise<Opt<RefField>> {
+ // RETRIEVE DOCS FROM SERVER
+
+ /**
+ * Given a single Doc GUID, this utility function will asynchronously attempt to fetch the id's associated
+ * field, first looking in the RefField cache and then communicating with
+ * the server if the document has not been cached.
+ * @param id the id of the requested document
+ */
+ export async function getRefField(id: string): Promise<Opt<RefField>> {
+ // an initial pass through the cache to determine whether the document needs to be fetched,
+ // is already in the process of being fetched or already exists in the
+ // cache
let cached = _cache[id];
if (cached === undefined) {
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => {
+ // NOT CACHED => we'll have to send a request to the server
+
+ // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
+ // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of
+ // the field has been returned from the server
+ const getSerializedField = Utils.emitCallback(_socket, MessageStore.GetRefField, id);
+
+ // when the serialized RefField has been received, go head and begin deserializing it into an object.
+ // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all
+ // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
+ const deserializeField = getSerializedField.then(async fieldJson => {
+ // deserialize
const field = SerializationHelper.Deserialize(fieldJson);
+ // either way, overwrite or delete any promises cached at this id (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
if (field !== undefined) {
await field.proto;
_cache[id] = field;
@@ -38,41 +119,45 @@ export namespace DocServer {
}
return field;
});
- _cache[id] = prom;
- return prom;
+ // here, indicate that the document associated with this id is currently
+ // being retrieved and cached
+ _cache[id] = deserializeField;
+ return deserializeField;
} else if (cached instanceof Promise) {
+ // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
+ // and requested the document I'm looking for. Shouldn't fetch again, just
+ // return this promise which will resolve to the field itself (see 7)
return cached;
} else {
+ // CACHED => great, let's just return the cached field we have
return cached;
}
}
/**
- * Given a list of Doc GUIDs, this utility function will asynchronously attempt to fetch each document
- * associated with a given input id, first looking in the RefField cache and then communicating with
- * the server if the document was not found there.
- *
+ * Given a list of Doc GUIDs, this utility function will asynchronously attempt to each id's associated
+ * field, first looking in the RefField cache and then communicating with
+ * the server if the document has not been cached.
* @param ids the ids that map to the reqested documents
*/
- export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+ export async function getRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
const requestedIds: string[] = [];
const waitingIds: string[] = [];
const promises: Promise<Opt<RefField>>[] = [];
const map: { [id: string]: Opt<RefField> } = {};
- // 1) An initial pass through the cache to determine which documents need to be fetched,
+ // 1) an initial pass through the cache to determine which documents need to be fetched,
// which are already in the process of being fetched and which already exist in the
// cache
for (const id of ids) {
const cached = _cache[id];
-
if (cached === undefined) {
// NOT CACHED => we'll have to send a request to the server
requestedIds.push(id);
} else if (cached instanceof Promise) {
- // BEING CACHED => someone else previously (likely recently) called GetRefFields,
+ // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
// and requested one of the documents I'm looking for. Shouldn't fetch again, just
- // wait until this promise is resolved (see the second to last line of the function)
+ // wait until this promise is resolved (see 7)
promises.push(cached);
waitingIds.push(id);
} else {
@@ -81,46 +166,49 @@ export namespace DocServer {
}
}
- // 2) Synchronously, we emit a single callback to the server requesting the documents for the given ids.
- // This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
+ // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
+ // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
// the fields have been returned from the server
- const fieldsReceived: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+ const getSerializedFields: Promise<any> = Utils.emitCallback(_socket, MessageStore.GetRefFields, requestedIds);
- // 3) When the serialized RefFields have been received, go head and begin deserializing them into objects.
+ // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
// Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
- // future .proto calls won't have to go farther than the cache to get their actual value.
- const fieldsDeserialized = fieldsReceived.then(async fields => {
+ // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
+ const deserializeFields = getSerializedFields.then(async fields => {
const fieldMap: { [id: string]: RefField } = {};
- const deserializedFields: any = [];
+ const protosToLoad: any = [];
for (const field of fields) {
if (field !== undefined) {
// deserialize
let deserialized: any = SerializationHelper.Deserialize(field);
fieldMap[field.id] = deserialized;
- deserializedFields.push(deserialized.proto);
+ // adds to a list of promises that will be awaited asynchronously
+ protosToLoad.push(deserialized.proto);
}
}
- // this actually handles the loeading of prototypes
- await Promise.all(deserializedFields);
+ // this actually handles the loading of prototypes
+ await Promise.all(protosToLoad);
return fieldMap;
});
- // 4) Here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+ // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
// we set the value at the field's id to a promise that will resolve to the field.
// When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
- requestedIds.forEach(id => _cache[id] = fieldsDeserialized.then(fields => fields[id]));
+ // The mapping in the .then call ensures that when other callers await these promises, they'll
+ // get the resolved field
+ requestedIds.forEach(id => _cache[id] = deserializeFields.then(fields => fields[id]));
- // 5) At this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
+ // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
// prototype documents, if any, have also been fetched and cached.
- const fields = await fieldsDeserialized;
+ const fields = await deserializeFields;
- // 6) With this confidence, we can now go through and update the cache at the ids of the fields that
+ // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
// we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
// id to the soon to be returned field mapping.
requestedIds.forEach(id => {
const field = fields[id];
- // either way, overwrite or delete any promises that we inserted as flags
- // to indicate that the field was in the process of being fetched. Now everything
+ // either way, overwrite or delete any promises (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
// should be an actual value within or entirely absent from the cache.
if (field !== undefined) {
_cache[id] = field;
@@ -130,78 +218,141 @@ export namespace DocServer {
map[id] = field;
});
- // 7) Those promises we encountered in the else if of 1), which represent
+ // 7) those promises we encountered in the else if of 1), which represent
// other callers having already submitted a request to the server for (a) document(s)
// in which we're interested, must still be awaited so that we can return the proper
// values for those as well.
//
- // Fortunately, those other callers will also hit their own version of 6) and clean up
+ // fortunately, those other callers will also hit their own version of 6) and clean up
// the shared cache when these promises resolve, so all we have to do is...
const otherCallersFetching = await Promise.all(promises);
// ...extract the RefFields returned from the resolution of those promises and add them to our
// own map.
waitingIds.forEach((id, index) => map[id] = otherCallersFetching[index]);
- // Now, we return our completed mapping from all of the ids that were passed into the method
+ // now, we return our completed mapping from all of the ids that were passed into the method
// to their actual RefField | undefined values. This return value either becomes the input
// argument to the caller's promise (i.e. GetRefFields.then(map => //do something with map...))
// or it is the direct return result if the promise is awaited.
return map;
}
- let _UpdateField = (id: string, diff: any) => {
+ // WRITE A NEW DOCUMENT TO THE SERVER
+
+ /**
+ * A wrapper around the function local variable _createField.
+ * This allows us to swap in different executions while comfortably
+ * calling the same function throughout the code base (such as in Util.makeReadonly())
+ * @param field the [RefField] to be serialized and sent to the server to be stored in the database
+ */
+ export function createField(field: RefField) {
+ _createField(field);
+ }
+
+ /**
+ * The default behavior for field creation. This inserts the [Doc] instance
+ * in the cache at its id, serializes the [Doc]'s initial state
+ * and finally sends that seruialized data to the server.
+ * @param field the [RefField] to be serialized and sent to the server to be stored in the database
+ */
+ let _createField = (field: RefField) => {
+ _cache[field[Id]] = field;
+ const initialState = SerializationHelper.serialize(field);
+ Utils.emit(_socket, MessageStore.CreateField, initialState);
+ };
+
+ // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE
+
+ /**
+ * A wrapper around the function local variable _emitFieldUpdate.
+ * This allows us to swap in different executions while comfortably
+ * calling the same function throughout the code base (such as in Util.makeReadonly())
+ * @param id the id of the [Doc] whose state has been updated in our client
+ * @param updatedState the new value of the document. At some point, this
+ * should actually be a proper diff, to improve efficiency
+ */
+ export function emitFieldUpdate(id: string, updatedState: any) {
+ _emitFieldUpdate(id, updatedState);
+ }
+
+ /**
+ * The default behavior for indicating to the server that we've locally updated
+ * a document.
+ * @param id the id of the [Doc] whose state has been updated in our client
+ * @param updatedState the new value of the document. At some point, this
+ * should actually be a proper diff, to improve efficiency
+ */
+ let _emitFieldUpdate = (id: string, updatedState: any) => {
+ // don't emit a duplicate message if the server is already
+ // (asynchronously) still updating this document's state.
if (id === updatingId) {
return;
}
- Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ // creates the diff object to send to the server
+ let diff: Diff = { id, diff: updatedState };
+ // emit this diff to notify server
+ Utils.emit(_socket, MessageStore.UpdateField, diff);
};
- export function UpdateField(id: string, diff: any) {
- _UpdateField(id, diff);
- }
+ // RESPOND TO THE SERVER'S INDICATION THAT A DOC'S STATE HAS BEEN UPDATED
- let _CreateField = (field: RefField) => {
- _cache[field[Id]] = field;
- const initialState = SerializationHelper.Serialize(field);
- Utils.Emit(_socket, MessageStore.CreateField, initialState);
- };
+ /**
+ * Whenever the client receives an update, execute the
+ * current behavior.
+ */
+ Utils.addServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
- export function CreateField(field: RefField) {
- _CreateField(field);
+ /**
+ * A wrapper around the function local variable _respondToUpdate.
+ * This allows us to swap in different executions while comfortably
+ * calling the same function throughout the code base (such as in Util.makeReadonly())
+ * @param diff kept as [any], but actually the [Diff] object sent from the server containing
+ * the [Doc]'s id and its new state
+ */
+ function respondToUpdate(diff: any) {
+ _respondToUpdate(diff);
}
- let updatingId: string | undefined;
+ /**
+ * The default behavior for responding to another client's indication
+ * that it has updated the state of a [Doc] that is also in use by
+ * this client
+ * @param diff kept as [any], but actually the [Diff] object sent from the server containing
+ * the [Doc]'s id and its new state
+ */
let _respondToUpdate = (diff: any) => {
const id = diff.id;
+ // to be valid, the Diff object must reference
+ // a document's id
if (id === undefined) {
return;
}
- const field = _cache[id];
const update = (f: Opt<RefField>) => {
+ // if the RefField is absent from the cache or
+ // its promise in the cache resolves to undefined, there
+ // can't be anything to update
if (f === undefined) {
return;
}
+ // extract this Doc's update handler
const handler = f[HandleUpdate];
if (handler) {
+ // set the 'I'm currently updating this Doc' flag
updatingId = id;
handler.call(f, diff.diff);
+ // reset to indicate no ongoing updates
updatingId = undefined;
}
};
+ // check the cache for the field
+ const field = _cache[id];
if (field instanceof Promise) {
+ // if the field is still being retrieved, update when the promise is resolved
field.then(update);
} else {
+ // otherwise, just execute the update
update(field);
}
};
- function respondToUpdate(diff: any) {
- _respondToUpdate(diff);
- }
-
- function connected() {
- _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 7ded85e43..ea8af3834 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -7,7 +7,7 @@ export namespace SerializationHelper {
return serializing > 0;
}
- export function Serialize(obj: Field): any {
+ export function serialize(obj: Field): any {
if (obj === undefined || obj === null) {
return undefined;
}
@@ -124,7 +124,7 @@ export namespace Deserializable {
export function autoObject(): PropSchema {
return custom(
- (s) => SerializationHelper.Serialize(s),
+ (s) => SerializationHelper.serialize(s),
(s) => SerializationHelper.Deserialize(s)
);
} \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index af65f5482..9da8912fe 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -80,7 +80,7 @@ export class Doc extends RefField {
});
this[SelfProxy] = doc;
if (!id || forceSave) {
- DocServer.CreateField(doc);
+ DocServer.createField(doc);
}
return doc;
}
@@ -108,7 +108,7 @@ export class Doc extends RefField {
private ___fields: any = {};
private [Update] = (diff: any) => {
- DocServer.UpdateField(this[Id], diff);
+ DocServer.emitFieldUpdate(this[Id], diff);
}
private [Self] = this;
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 8caceb063..7709d6c24 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -43,7 +43,7 @@ export const setter = action(function (target: any, prop: string | symbol | numb
} else {
target.__fields[prop] = value;
}
- target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+ target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
@@ -103,7 +103,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
let current = ObjectField.MakeCopy(value);
return (diff?: any) => {
if (true || !diff) {
- diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ diff = { '$set': { ["fields." + prop]: SerializationHelper.serialize(value) } };
const oldValue = current;
const newValue = ObjectField.MakeCopy(value);
current = newValue;
diff --git a/src/server/index.ts b/src/server/index.ts
index fd66c90b4..d9fe9d93d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -301,16 +301,16 @@ let clients: Map = {};
server.on("connection", function (socket: Socket) {
console.log("a user has connected");
- Utils.Emit(socket, MessageStore.Foo, "handshooken");
+ Utils.emit(socket, MessageStore.Foo, "handshooken");
- Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
- Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
+ Utils.addServerHandler(socket, MessageStore.Bar, barReceived);
+ Utils.addServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
- Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ 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.CreateField, CreateField);
+ Utils.addServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
});