Socket
Socket
Sign inDemoInstall

y-protocols

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

y-protocols - npm Package Compare versions

Comparing version 0.0.5 to 0.0.6

LICENSE

270

awareness.js

@@ -5,34 +5,110 @@ /**

import * as Y from 'yjs'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
import * as time from 'lib0/time.js'
import * as math from 'lib0/math.js'
import { Observable } from 'lib0/observable.js'
import * as Y from 'yjs'
const messageUsersStateChanged = 0
const outdatedTimeout = 30000
/**
* This is just a template for a Yjs instance with awareness.
* We do not really extend it.
* @typedef {Object} MetaClientState
* @property {number} MetaClientState.clock
* @property {number} MetaClientState.lastUpdated unix timestamp
*/
export class YWithAwareness extends Y.Doc {
constructor () {
/**
* The Awareness class implements a simple shared state protocol that can be used for non-persistent data like awareness information
* (cursor, username, status, ..). Each client can update its own local state and listen to state changes of
* remote clients. Every client may set a state of a remote peer to `null` to mark the client as offline.
*
* Each client is identified by a unique client id (something we borrow from `doc.clientID`). A client can override
* its own state by propagating a message with an increasing timestamp (`clock`). If such a message is received, it is
* applied if the known state of that client is older than the new state (`clock < newClock`). If a client thinks that
* a remote client is offline, it may propagate a message with
* `{ clock: currentClientClock, state: null, client: remoteClient }`. If such a
* message is received, and the known clock of that client equals the received clock, it will override the state with `null`.
*
* Before a client disconnects, it should propagate a `null` state with an updated clock.
*
* Awareness states must be updated every 30 seconds. Otherwise the Awareness instance will delete the client state.
*
* @extends {Observable<string>}
*/
export class Awareness extends Observable {
/**
* @param {Y.Doc} doc
*/
constructor (doc) {
super()
this.doc = doc
/**
* @type {Object<string,Object>}
* Maps from client id to client state
* @type {Map<number, Object<string, any>>}
*/
this._localAwarenessState = {}
this.awareness = new Map()
this.awarenessClock = new Map()
this.states = new Map()
/**
* @type {Map<number, MetaClientState>}
*/
this.meta = new Map()
this._checkInterval = setInterval(() => {
const now = time.getUnixTime()
if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(doc.clientID)).lastUpdated) {
// renew local clock
this.setLocalState(this.getLocalState())
}
/**
* @type {Array<number>}
*/
const remove = []
this.meta.forEach((meta, clientid) => {
if (outdatedTimeout <= now - meta.lastUpdated) {
remove.push(clientid)
}
})
if (remove.length > 0) {
removeAwarenessStates(this, remove, 'timeout')
}
}, math.floor(outdatedTimeout / 10))
doc.on('destroy', () => {
this.destroy()
})
}
destroy () {
clearInterval(this._checkInterval)
}
/**
* @return {Object<string,Object>}
* @return {Object<string,Object>|null}
*/
getLocalAwarenessInfo () {
throw error.methodUnimplemented()
getLocalState () {
return this.states.get(this.doc.clientID) || null
}
/**
* @return {Map<string,Object<string,Object>>}
* @param {Object<string,any>|null} state
*/
getAwarenessInfo () {
throw error.methodUnimplemented()
setLocalState (state) {
const clientID = this.doc.clientID
const currLocalMeta = this.meta.get(clientID)
const clock = currLocalMeta === undefined ? 0 : currLocalMeta.clock + 1
if (state === null) {
this.states.delete(clientID)
} else {
this.states.set(clientID, state)
}
this.meta.set(clientID, {
clock,
lastUpdated: time.getUnixTime()
})
const added = []
const updated = []
const removed = []
if (state === null) {
removed.push(clientID)
} else if (currLocalMeta === undefined) {
added.push(clientID)
} else {
updated.push(clientID)
}
this.emit('change', [{ added, updated, removed }, 'local'])
}

@@ -43,24 +119,58 @@ /**

*/
setAwarenessField (field, value) {
throw error.methodUnimplemented()
setLocalStateField (field, value) {
const state = this.getLocalState()
if (state !== null) {
state[field] = value
this.setLocalState(state)
}
}
/**
* @return {Map<number,Object<string,any>>}
*/
getStates () {
return this.states
}
}
/**
* @typedef {Object} UserStateUpdate
* @property {number} UserStateUpdate.clientID
* @property {number} UserStateUpdate.clock
* @property {Object} UserStateUpdate.state
* Mark (remote) clients as inactive and remove them from the list of active peers.
* This change will be propagated to remote clients.
*
* @param {Awareness} awareness
* @param {Array<number>} clients
* @param {any} origin
*/
export const removeAwarenessStates = (awareness, clients, origin) => {
const removed = []
for (let i = 0; i < clients.length; i++) {
const clientID = clients[i]
if (awareness.states.has(clientID)) {
awareness.states.delete(clientID)
if (clientID === awareness.doc.clientID) {
const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID))
curMeta.clock++
curMeta.lastUpdated = time.getUnixTime()
awareness.meta.set(clientID, curMeta)
}
removed.push(clientID)
}
}
if (removed.length > 0) {
awareness.emit('change', [{ added: [], updated: [], removed }, origin])
}
}
/**
* @param {encoding.Encoder} encoder
* @param {Array<UserStateUpdate>} stateUpdates
* @param {Awareness} awareness
* @param {Array<number>} clients
* @return {Uint8Array}
*/
export const writeUsersStateChange = (encoder, stateUpdates) => {
const len = stateUpdates.length
encoding.writeVarUint(encoder, messageUsersStateChanged)
export const encodeAwarenessUpdate = (awareness, clients) => {
const len = clients.length
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const { clientID, state, clock } = stateUpdates[i]
const clientID = clients[i]
const state = awareness.states.get(clientID) || null
const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock
encoding.writeVarUint(encoder, clientID)

@@ -70,9 +180,13 @@ encoding.writeVarUint(encoder, clock)

}
return encoding.toUint8Array(encoder)
}
/**
* @param {decoding.Decoder} decoder
* @param {YWithAwareness} y
* @param {Awareness} awareness
* @param {Uint8Array} update
* @param {any} origin This will be added to the emitted change event
*/
export const readUsersStateChange = (decoder, y) => {
export const applyAwarenessUpdate = (awareness, update, origin) => {
const decoder = decoding.createDecoder(update)
const timestamp = time.getUnixTime()
const added = []

@@ -86,84 +200,28 @@ const updated = []

const state = JSON.parse(decoding.readVarString(decoder))
const uClock = y.awarenessClock.get(clientID) || 0
y.awarenessClock.set(clientID, clock)
if (state === null) {
// only write if clock increases. cannot overwrite
if (y.awareness.has(clientID) && uClock < clock) {
y.awareness.delete(clientID)
const clientMeta = awareness.meta.get(clientID)
const uClock = clientMeta === undefined ? 0 : clientMeta.clock
if (uClock < clock || (uClock === clock && state === null && awareness.states.has(clientID))) {
if (state === null) {
awareness.states.delete(clientID)
} else {
awareness.states.set(clientID, state)
}
awareness.meta.set(clientID, {
clock,
lastUpdated: timestamp
})
if (clientMeta === undefined && state !== null) {
added.push(clientID)
} else if (clientMeta !== undefined && state === null) {
removed.push(clientID)
}
} else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
if (y.awareness.has(clientID)) {
} else if (state !== null) {
updated.push(clientID)
} else {
added.push(clientID)
}
y.awareness.set(clientID, state)
y.awarenessClock.set(clientID, clock)
}
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
// @ts-ignore We know emit is defined
y.emit('awareness', [{
awareness.emit('change', [{
added, updated, removed
}])
}, origin])
}
}
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserStateUpdate>}
*/
export const forwardUsersStateChange = (decoder, encoder) => {
const len = decoding.readVarUint(decoder)
const updates = []
encoding.writeVarUint(encoder, messageUsersStateChanged)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const clientID = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
const state = decoding.readVarString(decoder)
encoding.writeVarUint(encoder, clientID)
encoding.writeVarUint(encoder, clock)
encoding.writeVarString(encoder, state)
updates.push({ clientID, state: JSON.parse(state), clock })
}
return updates
}
/**
* @param {decoding.Decoder} decoder
* @param {YWithAwareness} y
*/
export const readAwarenessMessage = (decoder, y) => {
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
readUsersStateChange(decoder, y)
break
}
}
/**
* @typedef {Object} UserState
* @property {number} UserState.clientID
* @property {any} UserState.state
* @property {number} UserState.clock
*/
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserState>} Array of state updates
*/
export const forwardAwarenessMessage = (decoder, encoder) => {
/**
* @type {Array<UserState>}
*/
let s = []
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
s = forwardUsersStateChange(decoder, encoder)
}
return s
}

@@ -38,3 +38,3 @@ 'use strict';

exports.messagePermissionDenied = messagePermissionDenied;
exports.readAuthMessage = readAuthMessage;
exports.writePermissionDenied = writePermissionDenied;
exports.readAuthMessage = readAuthMessage;

@@ -7,4 +7,6 @@ 'use strict';

var decoding = require('lib0/dist/decoding.js');
var Y = require('yjs');
var error = require('lib0/dist/error.js');
var time = require('lib0/dist/time.js');
var math = require('lib0/dist/math.js');
var observable_js = require('lib0/dist/observable.js');
require('yjs');

@@ -15,29 +17,103 @@ /**

const messageUsersStateChanged = 0;
const outdatedTimeout = 30000;
/**
* This is just a template for a Yjs instance with awareness.
* We do not really extend it.
* @typedef {Object} MetaClientState
* @property {number} MetaClientState.clock
* @property {number} MetaClientState.lastUpdated unix timestamp
*/
class YWithAwareness extends Y.Doc {
constructor () {
/**
* The Awareness class implements a simple shared state protocol that can be used for non-persistent data like awareness information
* (cursor, username, status, ..). Each client can update its own local state and listen to state changes of
* remote clients. Every client may set a state of a remote peer to `null` to mark the client as offline.
*
* Each client is identified by a unique client id (something we borrow from `doc.clientID`). A client can override
* its own state by propagating a message with an increasing timestamp (`clock`). If such a message is received, it is
* applied if the known state of that client is older than the new state (`clock < newClock`). If a client thinks that
* a remote client is offline, it may propagate a message with
* `{ clock: currentClientClock, state: null, client: remoteClient }`. If such a
* message is received, and the known clock of that client equals the received clock, it will override the state with `null`.
*
* Before a client disconnects, it should propagate a `null` state with an updated clock.
*
* Awareness states must be updated every 30 seconds. Otherwise the Awareness instance will delete the client state.
*
* @extends {Observable<string>}
*/
class Awareness extends observable_js.Observable {
/**
* @param {Y.Doc} doc
*/
constructor (doc) {
super();
this.doc = doc;
/**
* @type {Object<string,Object>}
* Maps from client id to client state
* @type {Map<number, Object<string, any>>}
*/
this._localAwarenessState = {};
this.awareness = new Map();
this.awarenessClock = new Map();
this.states = new Map();
/**
* @type {Map<number, MetaClientState>}
*/
this.meta = new Map();
this._checkInterval = setInterval(() => {
const now = time.getUnixTime();
if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(doc.clientID)).lastUpdated) {
// renew local clock
this.setLocalState(this.getLocalState());
}
/**
* @type {Array<number>}
*/
const remove = [];
this.meta.forEach((meta, clientid) => {
if (outdatedTimeout <= now - meta.lastUpdated) {
remove.push(clientid);
}
});
if (remove.length > 0) {
removeAwarenessStates(this, remove, 'timeout');
}
}, math.floor(outdatedTimeout / 10));
doc.on('destroy', () => {
this.destroy();
});
}
destroy () {
clearInterval(this._checkInterval);
}
/**
* @return {Object<string,Object>}
* @return {Object<string,Object>|null}
*/
getLocalAwarenessInfo () {
throw error.methodUnimplemented()
getLocalState () {
return this.states.get(this.doc.clientID) || null
}
/**
* @return {Map<string,Object<string,Object>>}
* @param {Object<string,any>|null} state
*/
getAwarenessInfo () {
throw error.methodUnimplemented()
setLocalState (state) {
const clientID = this.doc.clientID;
const currLocalMeta = this.meta.get(clientID);
const clock = currLocalMeta === undefined ? 0 : currLocalMeta.clock + 1;
if (state === null) {
this.states.delete(clientID);
} else {
this.states.set(clientID, state);
}
this.meta.set(clientID, {
clock,
lastUpdated: time.getUnixTime()
});
const added = [];
const updated = [];
const removed = [];
if (state === null) {
removed.push(clientID);
} else if (currLocalMeta === undefined) {
added.push(clientID);
} else {
updated.push(clientID);
}
this.emit('change', [{ added, updated, removed }, 'local']);
}

@@ -48,24 +124,58 @@ /**

*/
setAwarenessField (field, value) {
throw error.methodUnimplemented()
setLocalStateField (field, value) {
const state = this.getLocalState();
if (state !== null) {
state[field] = value;
this.setLocalState(state);
}
}
/**
* @return {Map<number,Object<string,any>>}
*/
getStates () {
return this.states
}
}
/**
* @typedef {Object} UserStateUpdate
* @property {number} UserStateUpdate.clientID
* @property {number} UserStateUpdate.clock
* @property {Object} UserStateUpdate.state
* Mark (remote) clients as inactive and remove them from the list of active peers.
* This change will be propagated to remote clients.
*
* @param {Awareness} awareness
* @param {Array<number>} clients
* @param {any} origin
*/
const removeAwarenessStates = (awareness, clients, origin) => {
const removed = [];
for (let i = 0; i < clients.length; i++) {
const clientID = clients[i];
if (awareness.states.has(clientID)) {
awareness.states.delete(clientID);
if (clientID === awareness.doc.clientID) {
const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID));
curMeta.clock++;
curMeta.lastUpdated = time.getUnixTime();
awareness.meta.set(clientID, curMeta);
}
removed.push(clientID);
}
}
if (removed.length > 0) {
awareness.emit('change', [{ added: [], updated: [], removed }, origin]);
}
};
/**
* @param {encoding.Encoder} encoder
* @param {Array<UserStateUpdate>} stateUpdates
* @param {Awareness} awareness
* @param {Array<number>} clients
* @return {Uint8Array}
*/
const writeUsersStateChange = (encoder, stateUpdates) => {
const len = stateUpdates.length;
encoding.writeVarUint(encoder, messageUsersStateChanged);
const encodeAwarenessUpdate = (awareness, clients) => {
const len = clients.length;
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, len);
for (let i = 0; i < len; i++) {
const { clientID, state, clock } = stateUpdates[i];
const clientID = clients[i];
const state = awareness.states.get(clientID) || null;
const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock;
encoding.writeVarUint(encoder, clientID);

@@ -75,9 +185,13 @@ encoding.writeVarUint(encoder, clock);

}
return encoding.toUint8Array(encoder)
};
/**
* @param {decoding.Decoder} decoder
* @param {YWithAwareness} y
* @param {Awareness} awareness
* @param {Uint8Array} update
* @param {any} origin This will be added to the emitted change event
*/
const readUsersStateChange = (decoder, y) => {
const applyAwarenessUpdate = (awareness, update, origin) => {
const decoder = decoding.createDecoder(update);
const timestamp = time.getUnixTime();
const added = [];

@@ -91,91 +205,33 @@ const updated = [];

const state = JSON.parse(decoding.readVarString(decoder));
const uClock = y.awarenessClock.get(clientID) || 0;
y.awarenessClock.set(clientID, clock);
if (state === null) {
// only write if clock increases. cannot overwrite
if (y.awareness.has(clientID) && uClock < clock) {
y.awareness.delete(clientID);
const clientMeta = awareness.meta.get(clientID);
const uClock = clientMeta === undefined ? 0 : clientMeta.clock;
if (uClock < clock || (uClock === clock && state === null && awareness.states.has(clientID))) {
if (state === null) {
awareness.states.delete(clientID);
} else {
awareness.states.set(clientID, state);
}
awareness.meta.set(clientID, {
clock,
lastUpdated: timestamp
});
if (clientMeta === undefined && state !== null) {
added.push(clientID);
} else if (clientMeta !== undefined && state === null) {
removed.push(clientID);
}
} else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
if (y.awareness.has(clientID)) {
} else if (state !== null) {
updated.push(clientID);
} else {
added.push(clientID);
}
y.awareness.set(clientID, state);
y.awarenessClock.set(clientID, clock);
}
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
// @ts-ignore We know emit is defined
y.emit('awareness', [{
awareness.emit('change', [{
added, updated, removed
}]);
}, origin]);
}
};
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserStateUpdate>}
*/
const forwardUsersStateChange = (decoder, encoder) => {
const len = decoding.readVarUint(decoder);
const updates = [];
encoding.writeVarUint(encoder, messageUsersStateChanged);
encoding.writeVarUint(encoder, len);
for (let i = 0; i < len; i++) {
const clientID = decoding.readVarUint(decoder);
const clock = decoding.readVarUint(decoder);
const state = decoding.readVarString(decoder);
encoding.writeVarUint(encoder, clientID);
encoding.writeVarUint(encoder, clock);
encoding.writeVarString(encoder, state);
updates.push({ clientID, state: JSON.parse(state), clock });
}
return updates
};
/**
* @param {decoding.Decoder} decoder
* @param {YWithAwareness} y
*/
const readAwarenessMessage = (decoder, y) => {
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
readUsersStateChange(decoder, y);
break
}
};
/**
* @typedef {Object} UserState
* @property {number} UserState.clientID
* @property {any} UserState.state
* @property {number} UserState.clock
*/
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserState>} Array of state updates
*/
const forwardAwarenessMessage = (decoder, encoder) => {
/**
* @type {Array<UserState>}
*/
let s = [];
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
s = forwardUsersStateChange(decoder, encoder);
}
return s
};
exports.YWithAwareness = YWithAwareness;
exports.writeUsersStateChange = writeUsersStateChange;
exports.readUsersStateChange = readUsersStateChange;
exports.forwardUsersStateChange = forwardUsersStateChange;
exports.readAwarenessMessage = readAwarenessMessage;
exports.forwardAwarenessMessage = forwardAwarenessMessage;
exports.Awareness = Awareness;
exports.applyAwarenessUpdate = applyAwarenessUpdate;
exports.encodeAwarenessUpdate = encodeAwarenessUpdate;
exports.removeAwarenessStates = removeAwarenessStates;

@@ -49,3 +49,3 @@ 'use strict';

exports.readHistorySnapshot = readHistorySnapshot;
exports.writeHistorySnapshot = writeHistorySnapshot;
exports.readHistorySnapshot = readHistorySnapshot;

@@ -54,3 +54,3 @@ 'use strict';

encoding.writeVarUint(encoder, messageYjsSyncStep1);
const sv = Y.encodeDocumentStateVector(doc);
const sv = Y.encodeStateVector(doc);
encoding.writeVarUint8Array(encoder, sv);

@@ -135,8 +135,8 @@ };

exports.messageYjsUpdate = messageYjsUpdate;
exports.readSyncMessage = readSyncMessage;
exports.readSyncStep1 = readSyncStep1;
exports.readSyncStep2 = readSyncStep2;
exports.readUpdate = readUpdate;
exports.writeSyncStep1 = writeSyncStep1;
exports.writeSyncStep2 = writeSyncStep2;
exports.readSyncStep1 = readSyncStep1;
exports.readSyncStep2 = readSyncStep2;
exports.writeUpdate = writeUpdate;
exports.readUpdate = readUpdate;
exports.readSyncMessage = readSyncMessage;
{
"name": "y-protocols",
"version": "0.0.5",
"version": "0.0.6",
"description": "Yjs encoding protocols",

@@ -33,5 +33,6 @@ "files": [

"dependencies": {
"lib0": "0.0.4"
"lib0": "0.0.5"
},
"devDependencies": {
"yjs": "13.0.0-83",
"rollup": "^1.1.2",

@@ -38,0 +39,0 @@ "rollup-cli": "^1.0.9",

@@ -50,3 +50,3 @@ /**

encoding.writeVarUint(encoder, messageYjsSyncStep1)
const sv = Y.encodeDocumentStateVector(doc)
const sv = Y.encodeStateVector(doc)
encoding.writeVarUint8Array(encoder, sv)

@@ -53,0 +53,0 @@ }

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