@roomservice/browser
Advanced tools
Comparing version 0.0.16 to 0.1.0
import Automerge from "automerge"; | ||
import { KeyValueObject } from "./types"; | ||
declare class RoomClient<T extends KeyValueObject> { | ||
private _automergeConn; | ||
private _docs; | ||
private _peer; | ||
private _socket?; | ||
@@ -10,2 +9,3 @@ private _roomId?; | ||
private readonly _authorizationUrl; | ||
private _doc?; | ||
private _socketURL; | ||
@@ -17,2 +17,3 @@ private _onUpdateSocketCallback?; | ||
constructor(authorizationUrl: string, reference: string, state?: T); | ||
private init; | ||
/** | ||
@@ -26,3 +27,3 @@ * Manually attempt to restore the state from offline storage. | ||
connect(): Promise<{ | ||
state: T | Automerge.FreezeObject<T>; | ||
state: T; | ||
reference: string; | ||
@@ -37,6 +38,6 @@ }>; | ||
onDisconnect(callback: () => any): void; | ||
state(): Promise<Automerge.FreezeObject<T>>; | ||
state(): Promise<Automerge.FreezeObject<T> | undefined>; | ||
private syncOfflineCache; | ||
private _sendMsgToSocket; | ||
publishState(callback: (state: T) => void): T; | ||
publishState(callback: (state: T) => void): Promise<T>; | ||
} | ||
@@ -43,0 +44,0 @@ export default class RoomServiceClient { |
@@ -5,4 +5,6 @@ "use strict"; | ||
const automerge_1 = tslib_1.__importStar(require("automerge")); | ||
const immutable_1 = require("immutable"); | ||
const invariant_1 = tslib_1.__importDefault(require("invariant")); | ||
const lodash_1 = require("lodash"); | ||
const manymerge_1 = require("manymerge"); | ||
const safe_json_stringify_1 = tslib_1.__importDefault(require("safe-json-stringify")); | ||
@@ -27,5 +29,6 @@ const authorize_1 = tslib_1.__importDefault(require("./authorize")); | ||
this._sendMsgToSocket = (automergeMsg) => { | ||
// Note that this._automergeConn.open() must be called after the socket | ||
// definition | ||
invariant_1.default(!!this._socket, "Expected this._socket to be defined. This is a sign of a broken client, if you're seeing this, please contact us."); | ||
// we're offline, so don't do anything | ||
if (!this._socket) { | ||
return; | ||
} | ||
invariant_1.default(this._roomId, "Expected a _roomId to exist when publishing. This is a sign of a broken client, if you're seeing this, please contact us."); | ||
@@ -44,18 +47,27 @@ const room = { | ||
this._authorizationUrl = authorizationUrl; | ||
// Automerge technically supports sending multiple docs | ||
// over the wire at the same time, but for simplicity's sake | ||
// we just use one doc at for the moment. | ||
// | ||
// In the future, we may support multiple documents per room. | ||
const defaultDoc = automerge_1.default.from(state || {}); | ||
this._docs = new automerge_1.default.DocSet(); | ||
this._docs.setDoc("default", defaultDoc); | ||
this._automergeConn = new automerge_1.default.Connection(this._docs, this._sendMsgToSocket); | ||
this._peer = new manymerge_1.Peer(this._sendMsgToSocket); | ||
// Whenever possible, we try to use the actorId defined in storage | ||
this.init(state); | ||
// We define this here so we can debounce the save function | ||
// Otherwise we'll get quite the performance hit | ||
let saveOffline = (docId, doc) => { | ||
offline_1.default.set(this._reference, docId, automerge_1.save(doc)); | ||
offline_1.default.setDoc(this._reference, docId, automerge_1.save(doc)); | ||
}; | ||
this._saveOffline = lodash_1.debounce(saveOffline, 120); | ||
} | ||
async init(state) { | ||
if (this._doc) { | ||
return this._doc; | ||
} | ||
const actorId = await offline_1.default.getOrCreateActor(); | ||
const defaultDoc = automerge_1.default.from(state || {}, { actorId }); | ||
// Automerge technically supports sending multiple docs | ||
// over the wire at the same time, but for simplicity's sake | ||
// we just use one doc at for the moment. | ||
// | ||
// In the future, we may support multiple documents per room. | ||
this._doc = defaultDoc; | ||
this._peer.notify(this._doc); | ||
return this._doc; | ||
} | ||
/** | ||
@@ -73,2 +85,5 @@ * Manually attempt to restore the state from offline storage. | ||
let session; | ||
if (!this._doc) { | ||
await this.init(); | ||
} | ||
try { | ||
@@ -83,3 +98,3 @@ const params = await authorize_1.default(this._authorizationUrl, this._reference); | ||
return { | ||
state: this._docs.getDoc("default"), | ||
state: this._doc, | ||
reference: this._reference | ||
@@ -107,5 +122,11 @@ }; | ||
socket_1.default.on(this._socket, "connect", () => { | ||
this._automergeConn.open(); | ||
this._peer.notify(this._doc); | ||
this.syncOfflineCache(); | ||
}); | ||
// Required disconnect handler | ||
socket_1.default.on(this._socket, "disconnect", reason => { | ||
if (reason === "io server disconnect") { | ||
console.warn("The RoomService client was forcibly disconnected from the server, likely due to invalid auth."); | ||
} | ||
}); | ||
/** | ||
@@ -127,6 +148,10 @@ * We don't require these to be defined before hand since they're | ||
try { | ||
// NOTE: we purposefully don't define an actor id, | ||
// since it's not assumed this state is defined by our actor. | ||
state = automerge_1.default.load(room.state); | ||
const local = await this.syncOfflineCache(); | ||
state = automerge_1.merge(local, state); | ||
this._docs.setDoc("default", state); | ||
// @ts-ignore no trust me I swear | ||
this._doc = state; | ||
this._peer.notify(this._doc); | ||
} | ||
@@ -153,3 +178,3 @@ catch (err) { | ||
invariant_1.default(!this._onUpdateSocketCallback, "It looks like you've called onUpdate multiple times. Since this can cause quite severe performance issues if used incorrectly, we're not currently supporting this behavior. If you've got a use-case we haven't thought of, file a github issue and we may change this."); | ||
const socketCallback = (data) => { | ||
const socketCallback = async (data) => { | ||
const { meta, payload } = JSON.parse(data); | ||
@@ -167,3 +192,9 @@ if (!this._roomId) { | ||
} | ||
const newDoc = this._automergeConn.receiveMsg(payload.msg); | ||
// This is effectively impossible tbh, but we like to be cautious | ||
if (!this._doc) { | ||
await this.init(); | ||
} | ||
// convert the payload clock to a map | ||
payload.msg.clock = immutable_1.Map(payload.msg.clock); | ||
const newDoc = this._peer.applyMessage(payload.msg, this._doc); | ||
// Automerge, in it's infinite wisdom, will just return undefined | ||
@@ -176,4 +207,5 @@ // if a message is corrupted in some way that it doesn't like. | ||
} | ||
this._saveOffline("default", newDoc); | ||
callback(newDoc); | ||
this._doc = newDoc; | ||
this._saveOffline("default", this._doc); | ||
callback(this._doc); | ||
}; | ||
@@ -204,33 +236,29 @@ // If we're offline, just wait till we're back online to assign this callback | ||
async state() { | ||
return this._docs.getDoc("default"); | ||
if (!this._doc) { | ||
await this.init(); | ||
} | ||
return this._doc; | ||
} | ||
async syncOfflineCache() { | ||
const data = await offline_1.default.get(this._reference, "default"); | ||
const data = await offline_1.default.getDoc(this._reference, "default"); | ||
if (!data) { | ||
return this._docs.getDoc("default"); | ||
return this._doc; | ||
} | ||
const offlineDoc = automerge_1.load(data); | ||
const inMemDoc = this._docs.getDoc("default"); | ||
// Merge the offline doc with the current in-memory doc | ||
let newDoc; | ||
if (inMemDoc) { | ||
newDoc = automerge_1.merge(inMemDoc, offlineDoc); | ||
// We explictly do not add | ||
const offlineDoc = automerge_1.load(data, { | ||
actorId: await offline_1.default.getOrCreateActor() | ||
}); | ||
this._doc = offlineDoc; | ||
this._peer.notify(this._doc); | ||
return offlineDoc; | ||
} | ||
async publishState(callback) { | ||
let newDoc = automerge_1.default.change(this._doc, callback); | ||
if (!newDoc) { | ||
// this happens if someone deletes the doc, so we should just reinit it. | ||
newDoc = await this.init(); | ||
} | ||
else { | ||
newDoc = offlineDoc; | ||
} | ||
this._docs.setDoc("default", newDoc); | ||
return newDoc; | ||
} | ||
publishState(callback) { | ||
const newDoc = automerge_1.default.change(this._docs.getDoc("default"), callback); | ||
// Through a series of Automerge magic watchers, this call | ||
// publishes the document to socket.io if we're connected. | ||
// | ||
// setDoc | ||
// => Automerge.DocSet fires handler set in... | ||
// => Automerge.Connection fires handler set in... | ||
// => this._sendMsgToSocket() | ||
this._docs.setDoc("default", newDoc); | ||
this._doc = newDoc; | ||
this._saveOffline("default", newDoc); | ||
this._peer.notify(newDoc); | ||
return newDoc; | ||
@@ -237,0 +265,0 @@ } |
@@ -57,3 +57,3 @@ "use strict"; | ||
sockets.emit("connect"); | ||
const newState = room.publishState(prevState => { | ||
const newState = await room.publishState(prevState => { | ||
prevState.someOption = "hello!"; | ||
@@ -71,3 +71,3 @@ }); | ||
const room = client.room("my-room"); | ||
jest.spyOn(offline_1.default, "get").mockImplementation(async (ref, doc) => { | ||
jest.spyOn(offline_1.default, "getDoc").mockImplementation(async (ref, doc) => { | ||
return automerge_1.save(automerge_1.from({ name: "offlinedoc" })); | ||
@@ -82,3 +82,3 @@ }); | ||
// setup offline | ||
jest.spyOn(offline_1.default, "get").mockImplementation(async (ref, doc) => { | ||
jest.spyOn(offline_1.default, "getDoc").mockImplementation(async (ref, doc) => { | ||
return automerge_1.save(automerge_1.from({ offline: "offline" })); | ||
@@ -106,3 +106,3 @@ }); | ||
room._roomId = "my-room-id"; | ||
const setOffline = jest.spyOn(offline_1.default, "set"); | ||
const setOffline = jest.spyOn(offline_1.default, "setDoc"); | ||
onUpdateSocket(JSON.stringify({ | ||
@@ -109,0 +109,0 @@ meta: { |
@@ -7,6 +7,7 @@ /** | ||
interface IOffline { | ||
get: (roomRef: string, docId: string) => Promise<string>; | ||
set: (roomRef: string, docId: string, value: string) => Promise<any>; | ||
getDoc: (roomRef: string, docId: string) => Promise<string>; | ||
setDoc: (roomRef: string, docId: string, value: string) => Promise<any>; | ||
getOrCreateActor: () => Promise<string>; | ||
} | ||
declare const Offline: IOffline; | ||
export default Offline; |
@@ -8,8 +8,19 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const idb_keyval_1 = require("idb-keyval"); | ||
const v4_1 = tslib_1.__importDefault(require("uuid/v4")); | ||
const Offline = { | ||
get: (roomRef, docId) => idb_keyval_1.get("rs:" + roomRef + "/" + docId), | ||
set: (roomRef, docId, value) => idb_keyval_1.set("rs:" + roomRef + "/" + docId, value) | ||
getDoc: (roomRef, docId) => idb_keyval_1.get("rs:" + roomRef + "/" + docId), | ||
setDoc: (roomRef, docId, value) => idb_keyval_1.set("rs:" + roomRef + "/" + docId, value), | ||
getOrCreateActor: async () => { | ||
const actor = await idb_keyval_1.get("rs:actor"); | ||
if (actor) { | ||
return actor; | ||
} | ||
const id = v4_1.default(); | ||
idb_keyval_1.set("rs:actor", id); | ||
return id; | ||
} | ||
}; | ||
exports.default = Offline; | ||
//# sourceMappingURL=offline.js.map |
/// <reference types="socket.io-client" /> | ||
declare const Sockets: { | ||
newSocket(url: string, opts: SocketIOClient.ConnectOpts): SocketIOClient.Socket; | ||
on(socket: SocketIOClient.Socket, event: "error" | "connect" | "disconnect" | "sync_room_state", fn: (...args: any[]) => void): void; | ||
on(socket: SocketIOClient.Socket, event: "error" | "disconnect" | "connect" | "sync_room_state", fn: (...args: any[]) => void): void; | ||
emit(socket: SocketIOClient.Socket, event: "sync_room_state", ...args: any[]): void; | ||
@@ -6,0 +6,0 @@ disconnect(socket: SocketIOClient.Socket): void; |
{ | ||
"name": "@roomservice/browser", | ||
"version": "0.0.16", | ||
"version": "0.1.0", | ||
"main": "dist/index", | ||
@@ -23,2 +23,3 @@ "types": "dist/index", | ||
"@types/socket.io-client": "^1.4.32", | ||
"@types/uuid": "^3.4.6", | ||
"jest": "^24.9.0", | ||
@@ -32,2 +33,3 @@ "nock": "^11.7.0", | ||
"idb-keyval": "^3.2.0", | ||
"immutable": "^4.0.0-rc.12", | ||
"invariant": "^2.2.4", | ||
@@ -37,5 +39,7 @@ "ky": "^0.16.1", | ||
"lodash": "^4.17.15", | ||
"manymerge": "2.2.0", | ||
"safe-json-stringify": "^1.2.0", | ||
"socket.io-client": "^2.3.0" | ||
"socket.io-client": "^2.3.0", | ||
"uuid": "^3.3.3" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
43195
651
11
11
+ Addedimmutable@^4.0.0-rc.12
+ Addedmanymerge@2.2.0
+ Addeduuid@^3.3.3
+ Added@types/events@3.0.3(transitive)
+ Addedautomerge-clocks@1.2.0(transitive)
+ Addedimmutable@4.3.7(transitive)
+ Addedmanymerge@2.2.0(transitive)
+ Addeduuid@3.4.0(transitive)