From a4c26187068cc3b50501baa2229fdf854745792f Mon Sep 17 00:00:00 2001 From: moates Date: Fri, 31 Mar 2023 13:23:09 +1100 Subject: [PATCH] Issue #26 - Add support for passing a different state encoder to exported encoding and decoding methods, and add a binary encoding strategy. --- awareness.js | 65 ++++++++++++++++++++++++++++++++++++++++++----- awareness.test.js | 15 ++++++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/awareness.js b/awareness.js index 8f9ae94..ff0220d 100644 --- a/awareness.js +++ b/awareness.js @@ -156,6 +156,54 @@ export class Awareness extends Observable { } } +/** + * A state encoder that serializes state into JSON. + * + * This is the default strategy used in this package. + */ +export class DefaultAwarenessStateEncoder { + /** + * Encode an awareness state entry + * @param {encoding.Encoder} encoder + * @param {any} update + */ + static encodeState (encoder, update) { + encoding.writeVarString(encoder, JSON.stringify(update)) + } + + /** + * Decode an awareness state entry + * @param {decoding.Decoder} decoder + * @returns {string} + */ + static decodeState (decoder) { + return JSON.parse(decoding.readVarString(decoder)) + } +} + +/** + * A state encoder that serializes state into a binary format. + */ +export class BinaryAwarenessStateEncoder { + /** + * Encode an awareness state entry + * @param {encoding.Encoder} encoder + * @param {any} update + */ + static encodeState (encoder, update) { + encoding.writeAny(encoder, update) + } + + /** + * Decode an awareness state entry + * @param {decoding.Decoder} decoder + * @returns {any} + */ + static decodeState (decoder) { + return decoding.readAny(decoder) + } +} + /** * Mark (remote) clients as inactive and remove them from the list of active peers. * This change will be propagated to remote clients. @@ -189,9 +237,10 @@ export const removeAwarenessStates = (awareness, clients, origin) => { /** * @param {Awareness} awareness * @param {Array} clients + * @param {typeof DefaultAwarenessStateEncoder|typeof BinaryAwarenessStateEncoder} stateEncoder The encoder to use for encoding and decoding each state entry * @return {Uint8Array} */ -export const encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => { +export const encodeAwarenessUpdate = (awareness, clients, states = awareness.states, stateEncoder = DefaultAwarenessStateEncoder) => { const len = clients.length const encoder = encoding.createEncoder() encoding.writeVarUint(encoder, len) @@ -201,7 +250,7 @@ export const encodeAwarenessUpdate = (awareness, clients, states = awareness.sta const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock encoding.writeVarUint(encoder, clientID) encoding.writeVarUint(encoder, clock) - encoding.writeVarString(encoder, JSON.stringify(state)) + stateEncoder.encodeState(encoder, state) } return encoding.toUint8Array(encoder) } @@ -214,9 +263,10 @@ export const encodeAwarenessUpdate = (awareness, clients, states = awareness.sta * * @param {Uint8Array} update * @param {function(any):any} modify + * @param {typeof DefaultAwarenessStateEncoder|typeof BinaryAwarenessStateEncoder} stateEncoder The encoder to use for encoding and decoding each state entry * @return {Uint8Array} */ -export const modifyAwarenessUpdate = (update, modify) => { +export const modifyAwarenessUpdate = (update, modify, stateEncoder = DefaultAwarenessStateEncoder) => { const decoder = decoding.createDecoder(update) const encoder = encoding.createEncoder() const len = decoding.readVarUint(decoder) @@ -224,11 +274,11 @@ export const modifyAwarenessUpdate = (update, modify) => { for (let i = 0; i < len; i++) { const clientID = decoding.readVarUint(decoder) const clock = decoding.readVarUint(decoder) - const state = JSON.parse(decoding.readVarString(decoder)) + const state = stateEncoder.decodeState(decoder) const modifiedState = modify(state) encoding.writeVarUint(encoder, clientID) encoding.writeVarUint(encoder, clock) - encoding.writeVarString(encoder, JSON.stringify(modifiedState)) + stateEncoder.encodeState(encoder, modifiedState) } return encoding.toUint8Array(encoder) } @@ -237,8 +287,9 @@ export const modifyAwarenessUpdate = (update, modify) => { * @param {Awareness} awareness * @param {Uint8Array} update * @param {any} origin This will be added to the emitted change event + * @param {typeof DefaultAwarenessStateEncoder|typeof BinaryAwarenessStateEncoder} stateEncoder The encoder to use for encoding each state entry */ -export const applyAwarenessUpdate = (awareness, update, origin) => { +export const applyAwarenessUpdate = (awareness, update, origin, stateEncoder = DefaultAwarenessStateEncoder) => { const decoder = decoding.createDecoder(update) const timestamp = time.getUnixTime() const added = [] @@ -249,7 +300,7 @@ export const applyAwarenessUpdate = (awareness, update, origin) => { for (let i = 0; i < len; i++) { const clientID = decoding.readVarUint(decoder) let clock = decoding.readVarUint(decoder) - const state = JSON.parse(decoding.readVarString(decoder)) + const state = stateEncoder.decodeState(decoder) const clientMeta = awareness.meta.get(clientID) const prevState = awareness.states.get(clientID) const currClock = clientMeta === undefined ? 0 : clientMeta.clock diff --git a/awareness.test.js b/awareness.test.js index 0f17a6c..4dc33bd 100644 --- a/awareness.test.js +++ b/awareness.test.js @@ -4,9 +4,11 @@ import * as t from 'lib0/testing' import * as awareness from './awareness' /** - * @param {t.TestCase} tc + * @param {typeof awareness.DefaultAwarenessStateEncoder|typeof awareness.BinaryAwarenessStateEncoder} encoder + * @param {typeof awareness.DefaultAwarenessStateEncoder|typeof awareness.BinaryAwarenessStateEncoder} decoder + * @return {function(t.TestCase): void} */ -export const testAwareness = tc => { +const testAwarenessWithEncoding = (encoder = awareness.DefaultAwarenessStateEncoder, decoder = awareness.DefaultAwarenessStateEncoder) => tc => { const doc1 = new Y.Doc() doc1.clientID = 0 const doc2 = new Y.Doc() @@ -14,8 +16,8 @@ export const testAwareness = tc => { const aw1 = new awareness.Awareness(doc1) const aw2 = new awareness.Awareness(doc2) aw1.on('update', /** @param {any} p */ ({ added, updated, removed }) => { - const enc = awareness.encodeAwarenessUpdate(aw1, added.concat(updated).concat(removed)) - awareness.applyAwarenessUpdate(aw2, enc, 'custom') + const enc = awareness.encodeAwarenessUpdate(aw1, added.concat(updated).concat(removed), aw1.states, encoder) + awareness.applyAwarenessUpdate(aw2, enc, 'custom', decoder) }) let lastChangeLocal = /** @type {any} */ (null) aw1.on('change', /** @param {any} change */ change => { @@ -51,3 +53,8 @@ export const testAwareness = tc => { t.compare(aw1.getStates().get(0), undefined) t.compare(lastChangeLocal, lastChange) } + +export const testAwarenessWithBinary = testAwarenessWithEncoding(awareness.BinaryAwarenessStateEncoder, awareness.BinaryAwarenessStateEncoder) +export const testAwarenessWithDefault = testAwarenessWithEncoding(awareness.DefaultAwarenessStateEncoder, awareness.DefaultAwarenessStateEncoder) +export const testAwarenessBackwardsCompatDecoder = testAwarenessWithEncoding(awareness.DefaultAwarenessStateEncoder, undefined) +export const testAwarenessBackwardsCompatEncoder = testAwarenessWithEncoding(undefined, awareness.DefaultAwarenessStateEncoder)