Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Socket
Sign inDemoInstall

@roomservice/browser

Package Overview
Dependencies
Maintainers
1
Versions
126
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@roomservice/browser - npm Package Compare versions

Comparing version 0.0.15 to 0.0.16-rc.1

dist/test-socket.d.ts

3

dist/authorize.js

@@ -19,2 +19,5 @@ "use strict";

invariant_1.default(result.status !== 405, "Your authorization endpoint does not appear to accept a POST request.");
if (result.status < 200 || result.status >= 400) {
throw new Error(`Your Auth endpoint at '${authorizationUrl}' is not functioning properly, returned status of ${result.status}.`);
}
const res = await result.json();

@@ -21,0 +24,0 @@ const { room, session } = res;

11

dist/client.d.ts
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;
}
/**

@@ -71,3 +83,20 @@ * Manually attempt to restore the state from offline storage.

async connect() {
const { room, session } = await authorize_1.default(this._authorizationUrl, this._reference);
let room;
let session;
if (!this._doc) {
await this.init();
}
try {
const params = await authorize_1.default(this._authorizationUrl, this._reference);
room = params.room;
session = params.session;
}
catch (err) {
console.warn(err);
await this.syncOfflineCache();
return {
state: this._doc,
reference: this._reference
};
}
this._roomId = room.id;

@@ -90,2 +119,13 @@ this._socket = socket_1.default.newSocket(this._socketURL, {

});
// Required connect handler
socket_1.default.on(this._socket, "connect", () => {
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.");
}
});
/**

@@ -99,6 +139,3 @@ * We don't require these to be defined before hand since they're

if (this._onConnectSocketCallback) {
socket_1.default.on(this._socket, "connect", () => {
this._onConnectSocketCallback();
this.syncOfflineCache();
});
socket_1.default.on(this._socket, "connect", this._onConnectSocketCallback);
}

@@ -111,6 +148,10 @@ if (this._onDisconnectSocketCallback) {

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);
}

@@ -121,3 +162,2 @@ catch (err) {

}
this._automergeConn.open();
return {

@@ -139,3 +179,3 @@ state,

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);

@@ -153,3 +193,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

@@ -162,4 +208,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);
};

@@ -190,33 +237,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;

@@ -223,0 +266,0 @@ }

@@ -9,2 +9,3 @@ "use strict";

const socket_1 = tslib_1.__importDefault(require("./socket"));
const test_socket_1 = require("./test-socket");
const URL = "https://coolsite.com";

@@ -26,133 +27,99 @@ jest.mock("idb-keyval");

}
describe("RoomServiceClient", () => {
it("should call the authorization endpoint when creating a room", async () => {
const socket = test_socket_1.injectFakeSocket();
const scope = mockAuthEndpoint();
// @ts-ignore
jest.spyOn(socket_1.default, "newSocket").mockImplementation(() => ({
on: jest.fn()
}));
it("should call the authorization endpoint when creating a room", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
await room.connect();
expect(scope.isDone()).toBeTruthy();
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
await room.connect();
expect(scope.isDone()).toBeTruthy();
});
test("room gets called with bearer token", async () => {
mockAuthEndpoint();
const mock = jest
.spyOn(socket_1.default, "newSocket")
.mockImplementation((url, connectopts) => {
// @ts-ignore
return { on: jest.fn() };
}).mock;
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
await room.connect();
const [url, args] = mock.calls[0];
expect(url).toBe("https://api.roomservice.dev");
// @ts-ignore because bad typings make me sad
expect(args.transportOptions.polling.extraHeaders.authorization).toBe("Bearer short-lived-token");
});
test("room.publish() can change a document", async (done) => {
mockAuthEndpoint();
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
const sockets = test_socket_1.injectFakeSocket();
await room.connect();
sockets.emit("connect");
const newState = await room.publishState(prevState => {
prevState.someOption = "hello!";
});
test("room gets called with bearer token", async () => {
mockAuthEndpoint();
const mock = jest
.spyOn(socket_1.default, "newSocket")
.mockImplementation((url, connectopts) => {
// @ts-ignore
return { on: jest.fn() };
}).mock;
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
await room.connect();
const [url, args] = mock.calls[0];
expect(url).toBe("https://api.roomservice.dev");
// @ts-ignore because bad typings make me sad
expect(args.transportOptions.polling.extraHeaders.authorization).toBe("Bearer short-lived-token");
expect(newState.someOption).toBe("hello!");
setTimeout(() => {
// @ts-ignore
expect(room._automergeConn._ourClock.get("default").size).toEqual(1);
done();
}, 150);
});
test("room.restore() attempts to restore from offline", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
jest.spyOn(offline_1.default, "getDoc").mockImplementation(async (ref, doc) => {
return automerge_1.save(automerge_1.from({ name: "offlinedoc" }));
});
test("room.publish() can change a document", async () => {
mockAuthEndpoint();
const emit = jest.fn();
jest
.spyOn(socket_1.default, "newSocket")
// @ts-ignore because typescript doesn't like our deep testing magic
.mockImplementation((url, connectopts) => {
return {
emit,
on: jest.fn()
};
});
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
await room.connect();
const newState = room.publishState(prevState => {
prevState.someOption = "hello!";
});
expect(newState.someOption).toBe("hello!");
const doc = await room.restore();
expect(doc).toEqual({ name: "offlinedoc" });
});
test("room.connect() will merge online data with offline data", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
// setup offline
jest.spyOn(offline_1.default, "getDoc").mockImplementation(async (ref, doc) => {
return automerge_1.save(automerge_1.from({ offline: "offline" }));
});
test("room.restore() attempts to restore from offline", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
jest.spyOn(offline_1.default, "get").mockImplementation(async (ref, doc) => {
return automerge_1.save(automerge_1.from({ name: "offlinedoc" }));
});
const doc = await room.restore();
expect(doc).toEqual({ name: "offlinedoc" });
// setup online
mockAuthEndpoint(automerge_1.save(automerge_1.from({ online: "online" })));
const { state } = await room.connect();
expect(state).toEqual({
offline: "offline",
online: "online"
});
test("room.connect() will merge online data with offline data", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
// setup offline
jest.spyOn(offline_1.default, "get").mockImplementation(async (ref, doc) => {
return automerge_1.save(automerge_1.from({ offline: "offline" }));
});
// setup online
mockAuthEndpoint(automerge_1.save(automerge_1.from({ online: "online" })));
const { state } = await room.connect();
expect(state).toEqual({
offline: "offline",
online: "online"
});
});
test("room.onUpdate callback tries to save the document to offline", async (done) => {
mockAuthEndpoint();
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
const cb = jest.fn();
room.onUpdate(cb);
// @ts-ignore private
const onUpdateSocket = room._onUpdateSocketCallback;
expect(onUpdateSocket).toBeTruthy();
await room.connect();
// @ts-ignore private; we'd normally get this from the auth endpoint
room._roomId = "my-room-id";
const setOffline = jest.spyOn(offline_1.default, "set");
onUpdateSocket(JSON.stringify({
meta: {
roomId: "my-room-id"
},
payload: {
msg: {
clock: new Map(),
docId: "default"
}
});
test("room.onUpdate callback tries to save the document to offline", async (done) => {
mockAuthEndpoint();
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
const cb = jest.fn();
room.onUpdate(cb);
// @ts-ignore private
const onUpdateSocket = room._onUpdateSocketCallback;
expect(onUpdateSocket).toBeTruthy();
await room.connect();
// @ts-ignore private; we'd normally get this from the auth endpoint
room._roomId = "my-room-id";
const setOffline = jest.spyOn(offline_1.default, "setDoc");
onUpdateSocket(JSON.stringify({
meta: {
roomId: "my-room-id"
},
payload: {
msg: {
clock: new Map(),
docId: "default"
}
}));
// Sanity check that our onUpdate callback was called
expect(cb.mock.calls.length).toBe(1);
// We wait here because saving offline is debounced.
setTimeout(() => {
expect(setOffline.mock.calls.length).toBeGreaterThan(1);
done();
}, 160); // Debounce time
});
test("will update net with off-synced doc after coming online", async () => {
const client = new client_1.default(URL + "/api/roomservice");
const room = client.room("my-room");
// Offline
jest.spyOn(offline_1.default, "get").mockImplementation(async (ref, doc) => {
return automerge_1.save(automerge_1.from({ offline: "offline" }));
});
// Current online
mockAuthEndpoint(automerge_1.save(automerge_1.from({ current: "current" })));
// Attempt to publish an update while offline
room.publishState(state => {
state.update = "update";
});
const emit = jest.fn();
jest
.spyOn(socket_1.default, "newSocket")
// @ts-ignore because typescript doesn't like our deep testing magic
.mockImplementation((url, connectopts) => {
return {
emit,
on: jest.fn()
};
});
await room.connect();
console.log(emit.mock.calls);
});
}
}));
// Sanity check that our onUpdate callback was called
expect(cb.mock.calls.length).toBeGreaterThan(1);
// We wait here because saving offline is debounced.
setTimeout(() => {
expect(setOffline.mock.calls.length).toBeGreaterThanOrEqual(1);
done();
}, 160); // Debounce time
});
//# sourceMappingURL=client.test.js.map

@@ -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: Function): 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.15",
"version": "0.0.16-rc.1",
"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

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc