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.3.2-0 to 0.4.0-0

dist/doc-client.d.ts

4

dist/client.d.ts
import RoomClient from "./room-client";
import { KeyValueObject } from "./types";
import { Obj } from "./types";
export default class RoomServiceClient {

@@ -8,3 +8,3 @@ private readonly _authorizationUrl;

});
room<T extends KeyValueObject>(roomReference: string, state?: T): RoomClient<T>;
room<T extends Obj>(roomReference: string, defaultDoc?: T): RoomClient;
}

@@ -9,7 +9,7 @@ "use strict";

}
room(roomReference, state) {
room(roomReference, defaultDoc) {
return new room_client_1.default({
authUrl: this._authorizationUrl,
reference: roomReference,
state
roomReference,
defaultDoc
});

@@ -16,0 +16,0 @@ }

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

const test_socket_1 = require("./test-socket");
const lodash_1 = require("lodash");
const URL = "https://coolsite.com";

@@ -50,6 +51,10 @@ jest.mock("idb-keyval");

await room.init();
const [url, args] = mock.calls[0];
expect(url).toBe("https://api.roomservice.dev");
const urls = mock.calls.map(([url]) => url);
const args = mock.calls.map(([_, args]) => args);
expect(lodash_1.uniq(urls.sort())).toStrictEqual([
"https://api.roomservice.dev",
"https://api.roomservice.dev/v1/presence"
].sort());
// @ts-ignore because bad typings make me sad
expect(args.transportOptions.polling.extraHeaders.authorization).toBe("Bearer short-lived-token");
expect(args[0].transportOptions.polling.extraHeaders.authorization).toBe("Bearer short-lived-token");
});

@@ -65,3 +70,3 @@ test("room.publish() can change a document", async () => {

sockets.emit("connect");
const newState = await room.publishDoc(prevState => {
const newState = await room.setDoc((prevState) => {
prevState.someOption = "hello!";

@@ -106,5 +111,5 @@ });

const cb = jest.fn();
room.onUpdateDoc(cb);
room.onSetDoc(cb);
// @ts-ignore private
const onUpdateSocket = room._onUpdateSocketCallback;
const onUpdateSocket = room._docClient._onUpdateSocketCallback;
expect(onUpdateSocket).toBeTruthy();

@@ -111,0 +116,0 @@ await room.init();

@@ -1,42 +0,30 @@

import { KeyValueObject } from "./types";
export default class RoomClient<T extends KeyValueObject> {
private readonly _peer;
private readonly _reference;
import { PresenceMeta } from "./presence-client";
import { Obj } from "./types";
interface RoomClientParameters {
authUrl: string;
roomReference: string;
defaultDoc?: Obj;
}
export default class RoomClient {
private readonly _docClient;
private readonly _presenceClient;
private readonly _authorizationUrl;
private _socket?;
private _roomId?;
private _doc?;
private _actorId?;
private _socketURL;
private _onUpdateSocketCallback?;
private _onConnectSocketCallback?;
private _onDisconnectSocketCallback?;
private _saveOffline;
constructor(parameters: {
authUrl: string;
reference: string;
state?: T;
});
private readActorIdThenCreateDoc;
private createDoc;
/**
* Manually attempt to restore the state from offline storage.
*/
restore(): Promise<T>;
/**
* Attempts to go online.
*/
private readonly _roomReference;
constructor(parameters: RoomClientParameters);
private set _socketURL(value);
private _init;
init(): Promise<{
doc: T;
doc: string;
} | {
doc: Obj;
}>;
/**
* Manually goes offline
*/
restore(): Promise<any>;
onConnect(callback: () => void): void;
onDisconnect(callback: () => void): void;
disconnect(): void;
onUpdateDoc(callback: (state: Readonly<T>) => any): void;
onConnect(callback: () => any): void;
onDisconnect(callback: () => any): void;
private syncOfflineCache;
private _sendMsgToSocket;
publishDoc(callback: (state: T) => void): T;
setDoc<D extends Obj>(change: (prevDoc: D) => void): Readonly<D>;
onSetDoc<D extends Obj>(callback: (newDoc: Readonly<D>) => void): void;
setPresence<P extends Obj>(key: string, value: P): void;
onSetPresence<P extends Obj>(callback: (meta: PresenceMeta, value: P) => void): void;
}
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const automerge_1 = tslib_1.__importStar(require("automerge"));
const immutable_1 = require("immutable");
const invariant_1 = tslib_1.__importDefault(require("invariant"));
const doc_client_1 = tslib_1.__importDefault(require("./doc-client"));
const presence_client_1 = tslib_1.__importDefault(require("./presence-client"));
const authorize_1 = tslib_1.__importDefault(require("./authorize"));
const lodash_1 = require("lodash");
const manymerge_1 = require("manymerge");
const safe_json_stringify_1 = tslib_1.__importDefault(require("safe-json-stringify"));
const authorize_1 = tslib_1.__importDefault(require("./authorize"));
const constants_1 = require("./constants");
const offline_1 = tslib_1.__importDefault(require("./offline"));
const socket_1 = tslib_1.__importDefault(require("./socket"));
function asRoomStr(room) {
return safe_json_stringify_1.default(room);
}
class RoomClient {
constructor(parameters) {
// We define this as a local variable to make testing easier
this._socketURL = constants_1.ROOM_SERICE_SOCKET_URL;
// The automerge client will call this function when
// it picks up changes from the docset.
//
// WARNING: This function is an arrow function specifically because
// it needs to access this._socket. If you use a regular function,
// it won't work.
this._sendMsgToSocket = (automergeMsg) => {
// we're offline, so don't do anything
if (!this._socket) {
return;
this._init = lodash_1.throttle(async () => {
var _a;
let room;
let session;
try {
const params = await authorize_1.default(this._authorizationUrl, this._roomReference);
room = params.room;
session = params.session;
}
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.");
const room = {
meta: {
roomId: this._roomId
},
payload: {
msg: automergeMsg
catch (err) {
console.warn(err);
}
// We're on the server, so we shouldn't init, because we don't need
// to connect to the clients.
if (typeof window === "undefined") {
// This would signal that the server side can't access the auth endpoint
if (!room) {
throw new Error("Room Service can't access the auth endpoint on the server. More details: https://err.sh/getroomservice/browser/server-side-no-network");
}
};
socket_1.default.emit(this._socket, "sync_room_state", asRoomStr(room));
};
this._reference = parameters.reference;
return { doc: (_a = room) === null || _a === void 0 ? void 0 : _a.state };
}
// Presence client
this._presenceClient.init({
room,
session
});
// Doc client
const { doc } = await this._docClient.init({
room,
session
});
return { doc };
}, 100, {
leading: true
});
this._docClient = new doc_client_1.default(parameters);
this._presenceClient = new presence_client_1.default(parameters);
this._authorizationUrl = parameters.authUrl;
this._peer = new manymerge_1.Peer(this._sendMsgToSocket);
// Whenever possible, we try to use the actorId defined in storage
this.readActorIdThenCreateDoc(parameters.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.setDoc(this._reference, docId, automerge_1.save(doc));
};
this._saveOffline = lodash_1.debounce(saveOffline, 120);
this._roomReference = parameters.roomReference;
}
async readActorIdThenCreateDoc(state) {
const actorId = await offline_1.default.getOrCreateActor();
this._actorId = actorId;
return this.createDoc(actorId, state);
// used for testing locally
set _socketURL(url) {
this._docClient._socketURL = url;
this._presenceClient._socketURL = url;
}
createDoc(actorId, state) {
if (this._doc) {
return this._doc;
}
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;
// Start the client, sync from cache, and connect.
// This function is throttled at 100ms, since it's only
// supposed to be called once, but
async init() {
return this._init();
}
/**
* Manually attempt to restore the state from offline storage.
*/
// Manually restore from cache
async restore() {
if (!this._doc) {
await this.readActorIdThenCreateDoc();
}
return this.syncOfflineCache();
const doc = await this._docClient.restore();
return doc;
}
/**
* Attempts to go online.
*/
async init() {
let room;
let session;
if (!this._doc) {
await this.readActorIdThenCreateDoc();
}
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 {
doc: this._doc
};
}
this._roomId = room.id;
this._socket = socket_1.default.newSocket(this._socketURL, {
transportOptions: {
polling: {
extraHeaders: {
authorization: "Bearer " + session.token
}
}
}
});
/**
* Errors
*/
socket_1.default.on(this._socket, "error", (data) => {
const { message } = JSON.parse(data);
console.error(`Error from Socket: ${message}`);
});
// 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.");
}
});
/**
* We don't require these to be defined before hand since they're
* optional
*/
if (this._onUpdateSocketCallback) {
socket_1.default.on(this._socket, "sync_room_state", this._onUpdateSocketCallback);
}
if (this._onConnectSocketCallback) {
socket_1.default.on(this._socket, "connect", this._onConnectSocketCallback);
}
if (this._onDisconnectSocketCallback) {
socket_1.default.on(this._socket, "disconnect", this._onDisconnectSocketCallback);
}
// Merge RoomService's online cache with what we have locally
let state;
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);
// @ts-ignore no trust me I swear
this._doc = state;
this._peer.notify(this._doc);
}
catch (err) {
console.error(err);
state = {};
}
return { doc: state };
// Connection
onConnect(callback) {
this._docClient.onConnect(callback);
}
/**
* Manually goes offline
*/
onDisconnect(callback) {
this._docClient.onDisconnect(callback);
}
disconnect() {
if (this._socket) {
socket_1.default.disconnect(this._socket);
}
this._socket = undefined;
this._docClient.disconnect();
}
onUpdateDoc(callback) {
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 = async (data) => {
const { meta, payload } = JSON.parse(data);
if (!this._roomId) {
throw new Error("Expected a _roomId to be defined before we invoked the the onUpdate callback. This is a sign of a broken client, please contact us if you're seeing this.");
}
// This socket event will fire for ALL rooms, so we need to check
// if this callback refers to this particular room.
if (meta.roomId !== this._roomId) {
return;
}
if (!payload.msg) {
throw new Error("The room's state object does not include an 'msg' attribute, which could signal a corrupted room. If you're seeing this in production, that's quite bad and represents a fixable bug within the SDK itself. Please let us know and we'll fix it immediately!");
}
// This is effectively impossible tbh, but we like to be cautious
if (!this._doc) {
await this.readActorIdThenCreateDoc();
}
// 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
// if a message is corrupted in some way that it doesn't like.
// In these cases, we shouldn't actually save it offline otherwise
// we'd create a hard-to-fix corruption.
if (!newDoc) {
throw new Error(`Response from RoomService API seems corrupted, aborting. Response: ${data}`);
}
this._doc = newDoc;
this._saveOffline("default", this._doc);
callback(this._doc);
};
// If we're offline, just wait till we're back online to assign this callback
if (!this._socket) {
this._onUpdateSocketCallback = socketCallback;
return;
}
socket_1.default.on(this._socket, "sync_room_state", socketCallback);
// Documents
setDoc(change) {
return this._docClient.setDoc(change);
}
onConnect(callback) {
// If we're offline, cue this up for later.
if (!this._socket) {
this._onConnectSocketCallback = callback;
return;
}
this._socket.on("connect", callback);
onSetDoc(callback) {
this._docClient.onSetDoc(callback);
}
onDisconnect(callback) {
// If we're offline, cue this up for later.
if (!this._socket) {
this._onDisconnectSocketCallback = callback;
return;
}
this._socket.on("disconnect", callback);
// Presence
setPresence(key, value) {
this._presenceClient.setPresence(key, value);
}
async syncOfflineCache() {
const data = await offline_1.default.getDoc(this._reference, "default");
if (!data) {
return this._doc;
}
// 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;
onSetPresence(callback) {
this._presenceClient.onSetPresence(callback);
}
publishDoc(callback) {
if (!this._doc) {
console.error("Attempting to call publishDoc .init() has finished.");
return {};
}
if (typeof callback !== "function") {
throw new Error(`room.publishDoc expects a function.`);
}
let newDoc = automerge_1.default.change(this._doc, callback);
if (!newDoc) {
invariant_1.default(!!this._actorId, "The client is trying to regenerate a deleted document, but isn't able to access the cached actor id. This is probably a bug in the client, if you see this, we're incredibly sorry! Please let us know. In the meantime, you may be able work around this by ensuring 'await room.restore()' has finished before calling 'publishState'.");
// this happens if someone deletes the doc, so we should just reinit it.
newDoc = this.createDoc(this._actorId);
}
this._doc = newDoc;
this._saveOffline("default", newDoc);
this._peer.notify(newDoc);
return newDoc;
}
}
exports.default = RoomClient;
//# sourceMappingURL=room-client.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;
emit(socket: SocketIOClient.Socket, event: "sync_room_state", ...args: any[]): void;
on(socket: SocketIOClient.Socket, event: "error" | "connect" | "disconnect" | "sync_room_state" | "update_presence", fn: (...args: any[]) => void): void;
emit(socket: SocketIOClient.Socket, event: "sync_room_state" | "update_presence", ...args: any[]): void;
disconnect(socket: SocketIOClient.Socket): void;
};
export default Sockets;

@@ -1,3 +0,11 @@

export declare type KeyValueObject = {
export declare type Obj = {
[key: string]: any;
};
export interface Room {
reference: string;
id: string;
state?: any;
}
export interface Session {
token: string;
}
{
"name": "@roomservice/browser",
"version": "0.3.2-0",
"version": "0.4.0-0",
"main": "dist/index",

@@ -5,0 +5,0 @@ "types": "dist/index",

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