From 4ab218a6101176662059af5e9c64c4170a13026d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stian=20H=C3=A5klev?= Date: Thu, 4 Apr 2019 13:52:55 +0200 Subject: [PATCH 01/17] Presence changes --- lib/json0.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/lib/json0.js b/lib/json0.js index dc3a405..ed603e8 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -133,6 +133,87 @@ function convertToText(c) { delete c.o; } +function isValidPresence(presence) { + if ( + presence == null || + typeof presence.u !== 'string' || + typeof presence.c !== 'number' || + !isFinite(presence.c) || + Math.floor(presence.c) !== presence.c || + !Array.isArray(presence.s) + ) { + return false; + } + + var selections = presence.s; + + for (var i = 0, l = selections.length; i < l; ++i) { + var selection = selections[i]; + + if ( + !Array.isArray(selection) || + selection.length !== 2 || + selection[0] !== (selection[0] | 0) || + selection[1] !== (selection[1] | 0) + ) { + return false; + } + } + + return true; +} + +json.createPresence = function(presence) { + return presence; +}; + +json.comparePresence = function(pres1, pres2) { + if (!pres1 || !pres2) { + return false; + } + if (!pres1.p || !pres2.p) { + return false; + } + if (pres1.t !== pres2.t) { + return false; + } + if (pres1.t && subtypes[pres1.t]) { + if (pres1.p[0] === pres2.p[0]) { + return subtypes[pres1.t].comparePresence(pres1, pres2); + } + } else return pres1 === pres2; +}; + +json.transformPresence = function(presence, op, isOwn) { + console.group('json transform', presence, op); + if (op.length < 1) { + return presence; + } + const exOp = op[0]; + const opT = op[0].t; + console.log('exop', exOp, opT, exOp.p); + if (opT && subtypes[opT] && exOp.p) { + if (!presence.p || !presence.p[0] || presence.p[0] !== exOp.p[0]) { + console.log('creating presence', presence, exOp.p[0]); + presence = { + ...subtypes[opT].createPresence(), + p: op[0].p, + u: presence.u, + t: op[0].t + }; + console.log(presence); + } + presence = { + ...subtypes[opT].transformPresence(presence, op, isOwn), + p: op[0].p, + t: op[0].t + }; + console.log('result', presence); + console.groupEnd() + } + return presence; +}; + json.apply = function(snapshot, op) { json.checkValidOp(op); From ac8210f3788092803f248be28e2893c265f8daf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stian=20H=C3=A5klev?= Date: Thu, 4 Apr 2019 14:01:17 +0200 Subject: [PATCH 02/17] WIP --- lib/json0.js | 66 +++++++++++++++++----------------------------------- package.json | 6 ++--- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/lib/json0.js b/lib/json0.js index ed603e8..31152e8 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -133,40 +133,15 @@ function convertToText(c) { delete c.o; } -function isValidPresence(presence) { - if ( - presence == null || - typeof presence.u !== 'string' || - typeof presence.c !== 'number' || - !isFinite(presence.c) || - Math.floor(presence.c) !== presence.c || - !Array.isArray(presence.s) - ) { - return false; - } - - var selections = presence.s; - - for (var i = 0, l = selections.length; i < l; ++i) { - var selection = selections[i]; - - if ( - !Array.isArray(selection) || - selection.length !== 2 || - selection[0] !== (selection[0] | 0) || - selection[1] !== (selection[1] | 0) - ) { - return false; - } - } - - return true; -} - +// not checking anything here, we should probably check that u: exists +// (only thing we care about at json0 top level), and then delegate +// to any subtypes if there is already subtype presence data json.createPresence = function(presence) { return presence; }; +// this needs more thinking/testing, looking a bit more carefully at +// how this is implemented in ot-rich-text, etc. json.comparePresence = function(pres1, pres2) { if (!pres1 || !pres2) { return false; @@ -184,32 +159,33 @@ json.comparePresence = function(pres1, pres2) { } else return pres1 === pres2; }; +// this is the key function, always run client-side, both on +// the client that creates a text-change, and on the clients +// that receive text-changes (ops). if there are no ops, just +// return presence, if there are ops, delegate to the subtype +// responsible for those ops (currently only ot-rich-text). +// I am making assumptions many places that all ops will be +// of the same subtype, not sure if this is a given. +// We're only concerned about the first level of object/array, +// not sure if the spec allows nesting of subtypes. json.transformPresence = function(presence, op, isOwn) { - console.group('json transform', presence, op); if (op.length < 1) { return presence; } - const exOp = op[0]; - const opT = op[0].t; - console.log('exop', exOp, opT, exOp.p); - if (opT && subtypes[opT] && exOp.p) { + const representativeOp = op[0]; + const opTtype = op[0].t; + const path = representativeOp.p && representativeOp.p[0] + if (opType && subtypes[opType] && path) { if (!presence.p || !presence.p[0] || presence.p[0] !== exOp.p[0]) { - console.log('creating presence', presence, exOp.p[0]); - presence = { - ...subtypes[opT].createPresence(), - p: op[0].p, - u: presence.u, - t: op[0].t - }; - console.log(presence); + return presence } + // return result of running the subtype's transformPresence, + // but add path and type, which the subtype will not include presence = { ...subtypes[opT].transformPresence(presence, op, isOwn), p: op[0].p, t: op[0].t }; - console.log('result', presence); - console.groupEnd() } return presence; }; diff --git a/package.json b/package.json index b6c9df6..3d98220 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ot-json0", - "version": "1.1.0", + "name": "@houshuang/ot-json0", + "version": "1.2.0", "description": "JSON OT type", "main": "lib/index.js", "directories": { @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git://github.com/ottypes/json0" + "url": "git://github.com/houshuang/json0" }, "keywords": [ "ot", From cc705bf0ed5b1e96f7193c3529968ae4c04afa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stian=20H=C3=A5klev?= Date: Thu, 4 Apr 2019 14:10:26 +0200 Subject: [PATCH 03/17] WIP --- lib/json0.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/json0.js b/lib/json0.js index 31152e8..4b562e7 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -173,16 +173,16 @@ json.transformPresence = function(presence, op, isOwn) { return presence; } const representativeOp = op[0]; - const opTtype = op[0].t; + const opType = op[0].t; const path = representativeOp.p && representativeOp.p[0] if (opType && subtypes[opType] && path) { - if (!presence.p || !presence.p[0] || presence.p[0] !== exOp.p[0]) { + if (!presence.p || !presence.p[0] || presence.p[0] !== path) { return presence } // return result of running the subtype's transformPresence, // but add path and type, which the subtype will not include presence = { - ...subtypes[opT].transformPresence(presence, op, isOwn), + ...subtypes[opType].transformPresence(presence, op, isOwn), p: op[0].p, t: op[0].t }; From 539733aa27c05515906c93e648e2c8f4ff837d87 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 06:32:54 +0530 Subject: [PATCH 04/17] Add README content for presence --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index cc1de54..6314a10 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ The JSON OT type can be used to edit arbitrary JSON documents. +It has been forked from https://github.com/ottypes/json0 and modified to add Presence. + +## Presence + +(inspired by https://github.com/Teamwork/ot-rich-text#presence) + +The shape of our presence data is as follows: + +``` +{ + u: '123', // user ID + c: 8, // number of changes made by this user + s: [ // list of selections + [ 1, 1 ], // collapsed selection + [ 5, 7 ], // forward selection + [ 9, 4 ] // backward selection + ] +} +``` + +Each selection listed in `s` ends with a 2-element array containing the selection start index and the selection end index. The elements in the array preceeding the last two represent the path of a `text0` entry within the `json0` data structure. + +For example, the following entry in the `s` array represents the user's cursor position within the `content` field (`data.content`): + +``` +['content', 2, 2] +``` + +We can access deeply nested entries with this structure as well. For example, the following `s` entry represents a text selection in `data.files[3].text`: + +``` +['files', 3, 'text', 4, 7] +``` + +The rest of the README content is from the original repo https://github.com/ottypes/json0. + ## Features The JSON OT type supports the following operations: From 15ea264933ebad608353140891634903b46c0d81 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 12:11:37 +0530 Subject: [PATCH 05/17] Add failing tests for presence functions --- .gitignore | 1 + test/presence.js | 329 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 test/presence.js diff --git a/.gitignore b/.gitignore index 691378f..3344b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.swp *.DS_Store node_modules +package-lock.json diff --git a/test/presence.js b/test/presence.js new file mode 100644 index 0000000..b3de35d --- /dev/null +++ b/test/presence.js @@ -0,0 +1,329 @@ +const assert = require('assert') + +// These tests are inspired by the ones found here: +// https://github.com/Teamwork/ot-rich-text/blob/master/test/Operation.js +describe('createPresence', function() { + it('basic tests', function() { + const defaultPresence = { u: '', c: 0, s: [] }; + const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; + + assert.deepEqual(createPresence(), defaultPresence); + assert.deepEqual(createPresence(null), defaultPresence); + assert.deepEqual(createPresence(true), defaultPresence); + assert.deepEqual( + createPresence({ u: 5, c: 8, s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: '8', s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: 8, s: [1.5, 2] }), + defaultPresence + ); + assert.strictEqual(createPresence(presence), presence); + }); +}); + +describe('transformPresence', function() { + it('basic tests', function() { + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [], + true + ), + { + u: 'user', + c: 8, + s: [[5, 7]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [], + false + ), + { + u: 'user', + c: 8, + s: [[5, 7]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(3), createDelete(2), createInsertText('a')], + true + ), + { + u: 'user', + c: 8, + s: [[4, 6]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(3), createDelete(2), createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[3, 6]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(5), createDelete(2), createInsertText('a')], + true + ), + { + u: 'user', + c: 8, + s: [[6, 6]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(5), createDelete(2), createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[5, 5]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7], [8, 2]] + }, + [createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[6, 8], [9, 3]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[1, 1], [2, 2]] + }, + [createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[2, 2], [3, 3]] + } + ); + }); +}); + +describe('comparePresence', function() { + it('basic tests', function() { + assert.strictEqual(comparePresence(), true); + assert.strictEqual(comparePresence(undefined, undefined), true); + assert.strictEqual(comparePresence(null, null), true); + assert.strictEqual(comparePresence(null, undefined), false); + assert.strictEqual(comparePresence(undefined, null), false); + assert.strictEqual( + comparePresence(undefined, { u: '', c: 0, s: [] }), + false + ); + assert.strictEqual(comparePresence(null, { u: '', c: 0, s: [] }), false); + assert.strictEqual( + comparePresence({ u: '', c: 0, s: [] }, undefined), + false + ); + assert.strictEqual(comparePresence({ u: '', c: 0, s: [] }, null), false); + + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 2]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2], [4, 6]] }, + { u: 'user', c: 8, s: [[1, 2], [4, 6]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 }, + { u: 'user', c: 8, s: [[1, 2]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'userX', c: 8, s: [[1, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 9, s: [[1, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[3, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 3]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8], [3, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8], [1, 3]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8]] } + ), + false + ); + }); +}); + +describe('isValidPresence', function() { + it('basic tests', function() { + assert.strictEqual(isValidPresence(), false); + assert.strictEqual(isValidPresence(null), false); + assert.strictEqual(isValidPresence([]), false); + assert.strictEqual(isValidPresence({}), false); + assert.strictEqual(isValidPresence({ u: 5, c: 8, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: '8', s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8.5, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: Infinity, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: NaN, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: {} }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [] }), true); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[]] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1]] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1, 2]] }), true); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2, 3]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], []] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, '6']] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6.1]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, Infinity]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, NaN]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -0]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -1]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], ['3', 0]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, '2'], [4, 0]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [['1', 2], [4, 0]] }), + false + ); + }); +}); From 5eaaeb512ec0bc36cf2c1618703814a4184f9b09 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 12:20:36 +0530 Subject: [PATCH 06/17] Add first actual test for presence --- lib/json0.js | 8 +- test/json0.coffee | 2 +- test/presence.js | 639 +++++++++++++++++++++++----------------------- test/text0.coffee | 2 +- 4 files changed, 326 insertions(+), 325 deletions(-) diff --git a/lib/json0.js b/lib/json0.js index 4b562e7..af3240b 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -133,14 +133,12 @@ function convertToText(c) { delete c.o; } -// not checking anything here, we should probably check that u: exists -// (only thing we care about at json0 top level), and then delegate -// to any subtypes if there is already subtype presence data +const defaultPresence = { u: '', c: 0, s: [] }; json.createPresence = function(presence) { - return presence; + return presence || defaultPresence; }; -// this needs more thinking/testing, looking a bit more carefully at +// This needs more thinking/testing, looking a bit more carefully at // how this is implemented in ot-rich-text, etc. json.comparePresence = function(pres1, pres2) { if (!pres1 || !pres2) { diff --git a/test/json0.coffee b/test/json0.coffee index 531f76e..4a5f661 100644 --- a/test/json0.coffee +++ b/test/json0.coffee @@ -389,7 +389,7 @@ genTests = (type) -> assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'left' assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'right' - describe 'randomizer', -> + describe.skip 'randomizer', -> @timeout 20000 @slow 6000 it 'passes', -> diff --git a/test/presence.js b/test/presence.js index b3de35d..8ef389a 100644 --- a/test/presence.js +++ b/test/presence.js @@ -1,329 +1,332 @@ const assert = require('assert') +const json = require('../lib/json0') + +const { createPresence } = json; // These tests are inspired by the ones found here: // https://github.com/Teamwork/ot-rich-text/blob/master/test/Operation.js -describe('createPresence', function() { - it('basic tests', function() { +describe('createPresence', () => { + it('basic tests', () => { const defaultPresence = { u: '', c: 0, s: [] }; const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; assert.deepEqual(createPresence(), defaultPresence); - assert.deepEqual(createPresence(null), defaultPresence); - assert.deepEqual(createPresence(true), defaultPresence); - assert.deepEqual( - createPresence({ u: 5, c: 8, s: [1, 2] }), - defaultPresence - ); - assert.deepEqual( - createPresence({ u: '5', c: '8', s: [1, 2] }), - defaultPresence - ); - assert.deepEqual( - createPresence({ u: '5', c: 8, s: [1.5, 2] }), - defaultPresence - ); - assert.strictEqual(createPresence(presence), presence); - }); -}); - -describe('transformPresence', function() { - it('basic tests', function() { - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [], - true - ), - { - u: 'user', - c: 8, - s: [[5, 7]] - } - ); - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [], - false - ), - { - u: 'user', - c: 8, - s: [[5, 7]] - } - ); - - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [createRetain(3), createDelete(2), createInsertText('a')], - true - ), - { - u: 'user', - c: 8, - s: [[4, 6]] - } - ); - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [createRetain(3), createDelete(2), createInsertText('a')], - false - ), - { - u: 'user', - c: 8, - s: [[3, 6]] - } - ); - - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [createRetain(5), createDelete(2), createInsertText('a')], - true - ), - { - u: 'user', - c: 8, - s: [[6, 6]] - } - ); - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7]] - }, - [createRetain(5), createDelete(2), createInsertText('a')], - false - ), - { - u: 'user', - c: 8, - s: [[5, 5]] - } - ); - - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[5, 7], [8, 2]] - }, - [createInsertText('a')], - false - ), - { - u: 'user', - c: 8, - s: [[6, 8], [9, 3]] - } - ); - - assert.deepEqual( - transformPresence( - { - u: 'user', - c: 8, - s: [[1, 1], [2, 2]] - }, - [createInsertText('a')], - false - ), - { - u: 'user', - c: 8, - s: [[2, 2], [3, 3]] - } - ); + //assert.deepEqual(createPresence(null), defaultPresence); + //assert.deepEqual(createPresence(true), defaultPresence); + //assert.deepEqual( + // createPresence({ u: 5, c: 8, s: [1, 2] }), + // defaultPresence + //); + //assert.deepEqual( + // createPresence({ u: '5', c: '8', s: [1, 2] }), + // defaultPresence + //); + //assert.deepEqual( + // createPresence({ u: '5', c: 8, s: [1.5, 2] }), + // defaultPresence + //); + //assert.strictEqual(createPresence(presence), presence); }); }); -describe('comparePresence', function() { - it('basic tests', function() { - assert.strictEqual(comparePresence(), true); - assert.strictEqual(comparePresence(undefined, undefined), true); - assert.strictEqual(comparePresence(null, null), true); - assert.strictEqual(comparePresence(null, undefined), false); - assert.strictEqual(comparePresence(undefined, null), false); - assert.strictEqual( - comparePresence(undefined, { u: '', c: 0, s: [] }), - false - ); - assert.strictEqual(comparePresence(null, { u: '', c: 0, s: [] }), false); - assert.strictEqual( - comparePresence({ u: '', c: 0, s: [] }, undefined), - false - ); - assert.strictEqual(comparePresence({ u: '', c: 0, s: [] }, null), false); - - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'user', c: 8, s: [[1, 2]] } - ), - true - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2], [4, 6]] }, - { u: 'user', c: 8, s: [[1, 2], [4, 6]] } - ), - true - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 }, - { u: 'user', c: 8, s: [[1, 2]] } - ), - true - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 } - ), - true - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'userX', c: 8, s: [[1, 2]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'user', c: 9, s: [[1, 2]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'user', c: 8, s: [[3, 2]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[1, 2]] }, - { u: 'user', c: 8, s: [[1, 3]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, - { u: 'user', c: 8, s: [[9, 8], [3, 2]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, - { u: 'user', c: 8, s: [[9, 8], [1, 3]] } - ), - false - ); - assert.strictEqual( - comparePresence( - { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, - { u: 'user', c: 8, s: [[9, 8]] } - ), - false - ); - }); -}); - -describe('isValidPresence', function() { - it('basic tests', function() { - assert.strictEqual(isValidPresence(), false); - assert.strictEqual(isValidPresence(null), false); - assert.strictEqual(isValidPresence([]), false); - assert.strictEqual(isValidPresence({}), false); - assert.strictEqual(isValidPresence({ u: 5, c: 8, s: [] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: '8', s: [] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: 8.5, s: [] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: Infinity, s: [] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: NaN, s: [] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: 8, s: {} }), false); - assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [] }), true); - assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[]] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1]] }), false); - assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1, 2]] }), true); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2, 3]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], []] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6]] }), - true - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, '6']] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6.1]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, Infinity]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, NaN]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -0]] }), - true - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -1]] }), - true - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, 2], ['3', 0]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [[1, '2'], [4, 0]] }), - false - ); - assert.strictEqual( - isValidPresence({ u: '5', c: 8, s: [['1', 2], [4, 0]] }), - false - ); - }); -}); +// describe('transformPresence', () => { +// it('basic tests', () => { +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [], +// true +// ), +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// } +// ); +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [], +// false +// ), +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// } +// ); +// +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [createRetain(3), createDelete(2), createInsertText('a')], +// true +// ), +// { +// u: 'user', +// c: 8, +// s: [[4, 6]] +// } +// ); +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [createRetain(3), createDelete(2), createInsertText('a')], +// false +// ), +// { +// u: 'user', +// c: 8, +// s: [[3, 6]] +// } +// ); +// +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [createRetain(5), createDelete(2), createInsertText('a')], +// true +// ), +// { +// u: 'user', +// c: 8, +// s: [[6, 6]] +// } +// ); +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7]] +// }, +// [createRetain(5), createDelete(2), createInsertText('a')], +// false +// ), +// { +// u: 'user', +// c: 8, +// s: [[5, 5]] +// } +// ); +// +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[5, 7], [8, 2]] +// }, +// [createInsertText('a')], +// false +// ), +// { +// u: 'user', +// c: 8, +// s: [[6, 8], [9, 3]] +// } +// ); +// +// assert.deepEqual( +// transformPresence( +// { +// u: 'user', +// c: 8, +// s: [[1, 1], [2, 2]] +// }, +// [createInsertText('a')], +// false +// ), +// { +// u: 'user', +// c: 8, +// s: [[2, 2], [3, 3]] +// } +// ); +// }); +// }); +// +// describe('comparePresence', () => { +// it('basic tests', () => { +// assert.strictEqual(comparePresence(), true); +// assert.strictEqual(comparePresence(undefined, undefined), true); +// assert.strictEqual(comparePresence(null, null), true); +// assert.strictEqual(comparePresence(null, undefined), false); +// assert.strictEqual(comparePresence(undefined, null), false); +// assert.strictEqual( +// comparePresence(undefined, { u: '', c: 0, s: [] }), +// false +// ); +// assert.strictEqual(comparePresence(null, { u: '', c: 0, s: [] }), false); +// assert.strictEqual( +// comparePresence({ u: '', c: 0, s: [] }, undefined), +// false +// ); +// assert.strictEqual(comparePresence({ u: '', c: 0, s: [] }, null), false); +// +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'user', c: 8, s: [[1, 2]] } +// ), +// true +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2], [4, 6]] }, +// { u: 'user', c: 8, s: [[1, 2], [4, 6]] } +// ), +// true +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 }, +// { u: 'user', c: 8, s: [[1, 2]] } +// ), +// true +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 } +// ), +// true +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'userX', c: 8, s: [[1, 2]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'user', c: 9, s: [[1, 2]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'user', c: 8, s: [[3, 2]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[1, 2]] }, +// { u: 'user', c: 8, s: [[1, 3]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, +// { u: 'user', c: 8, s: [[9, 8], [3, 2]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, +// { u: 'user', c: 8, s: [[9, 8], [1, 3]] } +// ), +// false +// ); +// assert.strictEqual( +// comparePresence( +// { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, +// { u: 'user', c: 8, s: [[9, 8]] } +// ), +// false +// ); +// }); +// }); +// +// describe('isValidPresence', () => { +// it('basic tests', () => { +// assert.strictEqual(isValidPresence(), false); +// assert.strictEqual(isValidPresence(null), false); +// assert.strictEqual(isValidPresence([]), false); +// assert.strictEqual(isValidPresence({}), false); +// assert.strictEqual(isValidPresence({ u: 5, c: 8, s: [] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: '8', s: [] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: 8.5, s: [] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: Infinity, s: [] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: NaN, s: [] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: 8, s: {} }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [] }), true); +// assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[]] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1]] }), false); +// assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1, 2]] }), true); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2, 3]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], []] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6]] }), +// true +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, '6']] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6.1]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, Infinity]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, NaN]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -0]] }), +// true +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -1]] }), +// true +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, 2], ['3', 0]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [[1, '2'], [4, 0]] }), +// false +// ); +// assert.strictEqual( +// isValidPresence({ u: '5', c: 8, s: [['1', 2], [4, 0]] }), +// false +// ); +// }); +// }); diff --git a/test/text0.coffee b/test/text0.coffee index 15592dd..715b1a2 100644 --- a/test/text0.coffee +++ b/test/text0.coffee @@ -113,7 +113,7 @@ describe 'text0', -> t [{d:'abc', p:10}, {d:'xyz', p:11}] - describe 'randomizer', -> it 'passes', -> + describe.skip 'randomizer', -> it 'passes', -> @timeout 4000 @slow 4000 fuzzer text0, require('./text0-generator') From 650fe32d53a56bced1eae5275ad1a23e16351e71 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 12:28:33 +0530 Subject: [PATCH 07/17] Refactor: split presence operations into separate module --- lib/json0-presence.js | 91 +++++++++++++++++++++++++ lib/json0.js | 59 +--------------- test/{presence.js => json0-presence.js} | 2 +- 3 files changed, 94 insertions(+), 58 deletions(-) create mode 100644 lib/json0-presence.js rename test/{presence.js => json0-presence.js} (99%) diff --git a/lib/json0-presence.js b/lib/json0-presence.js new file mode 100644 index 0000000..179697a --- /dev/null +++ b/lib/json0-presence.js @@ -0,0 +1,91 @@ +// Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js +function isValidPresence(presence) { + if ( + presence == null || + typeof presence.u !== 'string' || + typeof presence.c !== 'number' || + !isFinite(presence.c) || + Math.floor(presence.c) !== presence.c || + !Array.isArray(presence.s) + ) { + return false + } + + const selections = presence.s + + for (let i = 0, l = selections.length; i < l; ++i) { + const selection = selections[i] + + if ( + !Array.isArray(selection) || + selection.length !== 2 || + selection[0] !== (selection[0] | 0) || + selection[1] !== (selection[1] | 0) + ) { + return false + } + } + + return true +} +const defaultPresence = { u: '', c: 0, s: [] }; +const createPresence = function(presence) { + return presence || defaultPresence; +}; + +// This needs more thinking/testing, looking a bit more carefully at +// how this is implemented in ot-rich-text, etc. +const comparePresence = function(pres1, pres2) { + if (!pres1 || !pres2) { + return false; + } + if (!pres1.p || !pres2.p) { + return false; + } + if (pres1.t !== pres2.t) { + return false; + } + if (pres1.t && subtypes[pres1.t]) { + if (pres1.p[0] === pres2.p[0]) { + return subtypes[pres1.t].comparePresence(pres1, pres2); + } + } else return pres1 === pres2; +}; + +// this is the key function, always run client-side, both on +// the client that creates a text-change, and on the clients +// that receive text-changes (ops). if there are no ops, just +// return presence, if there are ops, delegate to the subtype +// responsible for those ops (currently only ot-rich-text). +// I am making assumptions many places that all ops will be +// of the same subtype, not sure if this is a given. +// We're only concerned about the first level of object/array, +// not sure if the spec allows nesting of subtypes. +const transformPresence = function(presence, op, isOwn) { + if (op.length < 1) { + return presence; + } + const representativeOp = op[0]; + const opType = op[0].t; + const path = representativeOp.p && representativeOp.p[0] + if (opType && subtypes[opType] && path) { + if (!presence.p || !presence.p[0] || presence.p[0] !== path) { + return presence + } + // return result of running the subtype's transformPresence, + // but add path and type, which the subtype will not include + presence = { + ...subtypes[opType].transformPresence(presence, op, isOwn), + p: op[0].p, + t: op[0].t + }; + } + return presence; +}; + +module.exports = { + isValidPresence, + createPresence, + comparePresence, + transformPresence +}; diff --git a/lib/json0.js b/lib/json0.js index af3240b..c76cb2a 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -1,3 +1,4 @@ +var presence = require('./json0-presence') /* This is the implementation of the JSON OT type. @@ -133,61 +134,6 @@ function convertToText(c) { delete c.o; } -const defaultPresence = { u: '', c: 0, s: [] }; -json.createPresence = function(presence) { - return presence || defaultPresence; -}; - -// This needs more thinking/testing, looking a bit more carefully at -// how this is implemented in ot-rich-text, etc. -json.comparePresence = function(pres1, pres2) { - if (!pres1 || !pres2) { - return false; - } - if (!pres1.p || !pres2.p) { - return false; - } - if (pres1.t !== pres2.t) { - return false; - } - if (pres1.t && subtypes[pres1.t]) { - if (pres1.p[0] === pres2.p[0]) { - return subtypes[pres1.t].comparePresence(pres1, pres2); - } - } else return pres1 === pres2; -}; - -// this is the key function, always run client-side, both on -// the client that creates a text-change, and on the clients -// that receive text-changes (ops). if there are no ops, just -// return presence, if there are ops, delegate to the subtype -// responsible for those ops (currently only ot-rich-text). -// I am making assumptions many places that all ops will be -// of the same subtype, not sure if this is a given. -// We're only concerned about the first level of object/array, -// not sure if the spec allows nesting of subtypes. -json.transformPresence = function(presence, op, isOwn) { - if (op.length < 1) { - return presence; - } - const representativeOp = op[0]; - const opType = op[0].t; - const path = representativeOp.p && representativeOp.p[0] - if (opType && subtypes[opType] && path) { - if (!presence.p || !presence.p[0] || presence.p[0] !== path) { - return presence - } - // return result of running the subtype's transformPresence, - // but add path and type, which the subtype will not include - presence = { - ...subtypes[opType].transformPresence(presence, op, isOwn), - p: op[0].p, - t: op[0].t - }; - } - return presence; -}; - json.apply = function(snapshot, op) { json.checkValidOp(op); @@ -714,5 +660,4 @@ require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp var text = require('./text0'); json.registerSubtype(text); -module.exports = json; - +module.exports = Object.assign(json, presence); diff --git a/test/presence.js b/test/json0-presence.js similarity index 99% rename from test/presence.js rename to test/json0-presence.js index 8ef389a..bd49ab9 100644 --- a/test/presence.js +++ b/test/json0-presence.js @@ -11,7 +11,7 @@ describe('createPresence', () => { const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; assert.deepEqual(createPresence(), defaultPresence); - //assert.deepEqual(createPresence(null), defaultPresence); + assert.deepEqual(createPresence(null), defaultPresence); //assert.deepEqual(createPresence(true), defaultPresence); //assert.deepEqual( // createPresence({ u: 5, c: 8, s: [1, 2] }), From 938e435088b186732b9e56e5210804775f2287ea Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 14:42:54 +0530 Subject: [PATCH 08/17] First pass at json0 createPresence --- lib/json0-presence.js | 61 +++++++++++++++++++++--------------------- test/json0-presence.js | 28 +++++++++---------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lib/json0-presence.js b/lib/json0-presence.js index 179697a..5a95aca 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -1,37 +1,38 @@ // Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js function isValidPresence(presence) { - if ( - presence == null || - typeof presence.u !== 'string' || - typeof presence.c !== 'number' || - !isFinite(presence.c) || - Math.floor(presence.c) !== presence.c || - !Array.isArray(presence.s) - ) { - return false - } + if ( + presence == null || + typeof presence.u !== 'string' || + typeof presence.c !== 'number' || + !isFinite(presence.c) || + Math.floor(presence.c) !== presence.c || + !Array.isArray(presence.s) + ) { + return false; + } - const selections = presence.s + const selections = presence.s; - for (let i = 0, l = selections.length; i < l; ++i) { - const selection = selections[i] + for (let i = 0, l = selections.length; i < l; ++i) { + const selection = selections[i]; - if ( - !Array.isArray(selection) || - selection.length !== 2 || - selection[0] !== (selection[0] | 0) || - selection[1] !== (selection[1] | 0) - ) { - return false - } + if ( + !Array.isArray(selection) || + selection.length !== 2 || + selection[0] !== (selection[0] | 0) || + selection[1] !== (selection[1] | 0) + ) { + return false; } + } - return true + return true; } -const defaultPresence = { u: '', c: 0, s: [] }; -const createPresence = function(presence) { - return presence || defaultPresence; -}; + +const defaultPresence = {u: '', c: 0, s: []}; + +const createPresence = presence => + isValidPresence(presence) ? presence : defaultPresence; // This needs more thinking/testing, looking a bit more carefully at // how this is implemented in ot-rich-text, etc. @@ -67,17 +68,17 @@ const transformPresence = function(presence, op, isOwn) { } const representativeOp = op[0]; const opType = op[0].t; - const path = representativeOp.p && representativeOp.p[0] + const path = representativeOp.p && representativeOp.p[0]; if (opType && subtypes[opType] && path) { if (!presence.p || !presence.p[0] || presence.p[0] !== path) { - return presence + return presence; } // return result of running the subtype's transformPresence, // but add path and type, which the subtype will not include presence = { ...subtypes[opType].transformPresence(presence, op, isOwn), p: op[0].p, - t: op[0].t + t: op[0].t, }; } return presence; @@ -87,5 +88,5 @@ module.exports = { isValidPresence, createPresence, comparePresence, - transformPresence + transformPresence, }; diff --git a/test/json0-presence.js b/test/json0-presence.js index bd49ab9..fe456c6 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -12,20 +12,20 @@ describe('createPresence', () => { assert.deepEqual(createPresence(), defaultPresence); assert.deepEqual(createPresence(null), defaultPresence); - //assert.deepEqual(createPresence(true), defaultPresence); - //assert.deepEqual( - // createPresence({ u: 5, c: 8, s: [1, 2] }), - // defaultPresence - //); - //assert.deepEqual( - // createPresence({ u: '5', c: '8', s: [1, 2] }), - // defaultPresence - //); - //assert.deepEqual( - // createPresence({ u: '5', c: 8, s: [1.5, 2] }), - // defaultPresence - //); - //assert.strictEqual(createPresence(presence), presence); + assert.deepEqual(createPresence(true), defaultPresence); + assert.deepEqual( + createPresence({ u: 5, c: 8, s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: '8', s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: 8, s: [1.5, 2] }), + defaultPresence + ); + assert.strictEqual(createPresence(presence), presence); }); }); From b0915b8a267925d30262f49ad97f3df932da586c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 14:45:31 +0530 Subject: [PATCH 09/17] Add basic test for json0 transformPresence --- test/json0-presence.js | 273 ++++++++++++++++++----------------------- 1 file changed, 120 insertions(+), 153 deletions(-) diff --git a/test/json0-presence.js b/test/json0-presence.js index fe456c6..5a815cc 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -1,171 +1,138 @@ -const assert = require('assert') -const json = require('../lib/json0') - -const { createPresence } = json; +const assert = require('assert'); +const {createPresence, transformPresence} = require('../lib/json0'); // These tests are inspired by the ones found here: // https://github.com/Teamwork/ot-rich-text/blob/master/test/Operation.js describe('createPresence', () => { it('basic tests', () => { - const defaultPresence = { u: '', c: 0, s: [] }; - const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; + const defaultPresence = {u: '', c: 0, s: []}; + const presence = {u: '5', c: 8, s: [[1, 2], [9, 5]]}; assert.deepEqual(createPresence(), defaultPresence); assert.deepEqual(createPresence(null), defaultPresence); assert.deepEqual(createPresence(true), defaultPresence); + assert.deepEqual(createPresence({u: 5, c: 8, s: [1, 2]}), defaultPresence); assert.deepEqual( - createPresence({ u: 5, c: 8, s: [1, 2] }), - defaultPresence - ); - assert.deepEqual( - createPresence({ u: '5', c: '8', s: [1, 2] }), - defaultPresence + createPresence({u: '5', c: '8', s: [1, 2]}), + defaultPresence, ); assert.deepEqual( - createPresence({ u: '5', c: 8, s: [1.5, 2] }), - defaultPresence + createPresence({u: '5', c: 8, s: [1.5, 2]}), + defaultPresence, ); assert.strictEqual(createPresence(presence), presence); }); }); -// describe('transformPresence', () => { -// it('basic tests', () => { -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [], -// true -// ), -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// } -// ); -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [], -// false -// ), -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// } -// ); -// -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [createRetain(3), createDelete(2), createInsertText('a')], -// true -// ), -// { -// u: 'user', -// c: 8, -// s: [[4, 6]] -// } -// ); -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [createRetain(3), createDelete(2), createInsertText('a')], -// false -// ), -// { -// u: 'user', -// c: 8, -// s: [[3, 6]] -// } -// ); -// -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [createRetain(5), createDelete(2), createInsertText('a')], -// true -// ), -// { -// u: 'user', -// c: 8, -// s: [[6, 6]] -// } -// ); -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7]] -// }, -// [createRetain(5), createDelete(2), createInsertText('a')], -// false -// ), -// { -// u: 'user', -// c: 8, -// s: [[5, 5]] -// } -// ); -// -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[5, 7], [8, 2]] -// }, -// [createInsertText('a')], -// false -// ), -// { -// u: 'user', -// c: 8, -// s: [[6, 8], [9, 3]] -// } -// ); -// -// assert.deepEqual( -// transformPresence( -// { -// u: 'user', -// c: 8, -// s: [[1, 1], [2, 2]] -// }, -// [createInsertText('a')], -// false -// ), -// { -// u: 'user', -// c: 8, -// s: [[2, 2], [3, 3]] -// } -// ); -// }); -// }); -// +describe('transformPresence', () => { + it('basic tests', () => { + assert.deepEqual( + transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], true), + {u: 'user', c: 8, s: [[5, 7]]}, + ); + // assert.deepEqual( + // transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], false), + // {u: 'user', c: 8, s: [[5, 7]]}, + // ); + + //assert.deepEqual( + // transformPresence( + // {u: 'user', c: 8, s: [[5, 7]]}, + // [createRetain(3), createDelete(2), createInsertText('a')], + // true, + // ), + // { + // u: 'user', + // c: 8, + // s: [[4, 6]], + // }, + //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(3), createDelete(2), createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[3, 6]], + // }, + //); + + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(5), createDelete(2), createInsertText('a')], + // true, + // ), + // { + // u: 'user', + // c: 8, + // s: [[6, 6]], + // }, + //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(5), createDelete(2), createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[5, 5]], + // }, + //); + + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7], [8, 2]], + // }, + // [createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[6, 8], [9, 3]], + // }, + //); + + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[1, 1], [2, 2]], + // }, + // [createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[2, 2], [3, 3]], + // }, + //); + }); +}); +// // describe('comparePresence', () => { // it('basic tests', () => { // assert.strictEqual(comparePresence(), true); @@ -183,7 +150,7 @@ describe('createPresence', () => { // false // ); // assert.strictEqual(comparePresence({ u: '', c: 0, s: [] }, null), false); -// +// // assert.strictEqual( // comparePresence( // { u: 'user', c: 8, s: [[1, 2]] }, @@ -263,7 +230,7 @@ describe('createPresence', () => { // ); // }); // }); -// +// // describe('isValidPresence', () => { // it('basic tests', () => { // assert.strictEqual(isValidPresence(), false); From f6fb7e5c386364d669696c9fb6575953c95b58f7 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 15:35:22 +0530 Subject: [PATCH 10/17] Leverage transformCursor --- lib/json0-presence.js | 43 ++++++++++++++++-------------------------- lib/json0.js | 20 +++----------------- lib/utils.js | 18 ++++++++++++++++++ test/json0-presence.js | 16 ++++++++++++---- 4 files changed, 49 insertions(+), 48 deletions(-) create mode 100644 lib/utils.js diff --git a/lib/json0-presence.js b/lib/json0-presence.js index 5a95aca..ec330c4 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -1,3 +1,6 @@ +const { transformCursor } = require('./text0'); +const { convertFromText } = require('./utils'); + // Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js function isValidPresence(presence) { if ( @@ -18,7 +21,7 @@ function isValidPresence(presence) { if ( !Array.isArray(selection) || - selection.length !== 2 || + selection.length < 2 || selection[0] !== (selection[0] | 0) || selection[1] !== (selection[1] | 0) ) { @@ -53,33 +56,19 @@ const comparePresence = function(pres1, pres2) { } else return pres1 === pres2; }; -// this is the key function, always run client-side, both on -// the client that creates a text-change, and on the clients -// that receive text-changes (ops). if there are no ops, just -// return presence, if there are ops, delegate to the subtype -// responsible for those ops (currently only ot-rich-text). -// I am making assumptions many places that all ops will be -// of the same subtype, not sure if this is a given. -// We're only concerned about the first level of object/array, -// not sure if the spec allows nesting of subtypes. -const transformPresence = function(presence, op, isOwn) { - if (op.length < 1) { - return presence; - } - const representativeOp = op[0]; - const opType = op[0].t; - const path = representativeOp.p && representativeOp.p[0]; - if (opType && subtypes[opType] && path) { - if (!presence.p || !presence.p[0] || presence.p[0] !== path) { - return presence; +const transformPresence = (presence, op, isOwn) => { + for (let c of op){ + if(c.si || c.sd){ + convertFromText(c) + } + if(c.t === 'text0') { + presence = Object.assign({}, presence, { + s: presence.s.map(([start, end]) => ([ + transformCursor(start, c.o), + transformCursor(end, c.o), + ])) + }); } - // return result of running the subtype's transformPresence, - // but add path and type, which the subtype will not include - presence = { - ...subtypes[opType].transformPresence(presence, op, isOwn), - p: op[0].p, - t: op[0].t, - }; } return presence; }; diff --git a/lib/json0.js b/lib/json0.js index c76cb2a..b22ab66 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -1,4 +1,7 @@ var presence = require('./json0-presence') +var utils = require('./utils'); +var convertFromText = utils.convertFromText; +var convertToText = utils.convertToText; /* This is the implementation of the JSON OT type. @@ -117,23 +120,6 @@ json.checkObj = function(elem) { } }; -// helper functions to convert old string ops to and from subtype ops -function convertFromText(c) { - c.t = 'text0'; - var o = {p: c.p.pop()}; - if (c.si != null) o.i = c.si; - if (c.sd != null) o.d = c.sd; - c.o = [o]; -} - -function convertToText(c) { - c.p.push(c.o[0].p); - if (c.o[0].i != null) c.si = c.o[0].i; - if (c.o[0].d != null) c.sd = c.o[0].d; - delete c.t; - delete c.o; -} - json.apply = function(snapshot, op) { json.checkValidOp(op); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..8090e2a --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,18 @@ +// helper functions to convert old string ops to and from subtype ops +function convertFromText(c) { + c.t = 'text0'; + var o = {p: c.p.pop()}; + if (c.si != null) o.i = c.si; + if (c.sd != null) o.d = c.sd; + c.o = [o]; +} + +function convertToText(c) { + c.p.push(c.o[0].p); + if (c.o[0].i != null) c.si = c.o[0].i; + if (c.o[0].d != null) c.sd = c.o[0].d; + delete c.t; + delete c.o; +} + +module.exports = { convertFromText, convertToText }; diff --git a/test/json0-presence.js b/test/json0-presence.js index 5a815cc..066ab9b 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -30,10 +30,18 @@ describe('transformPresence', () => { transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], true), {u: 'user', c: 8, s: [[5, 7]]}, ); - // assert.deepEqual( - // transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], false), - // {u: 'user', c: 8, s: [[5, 7]]}, - // ); + assert.deepEqual( + transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], false), + {u: 'user', c: 8, s: [[5, 7]]}, + ); + assert.deepEqual( + transformPresence( + {u: 'user', c: 8, s: [[5, 7]]}, + [{p: [0], si: 'a'}], // Insert the 'a' character at position 0. + true, + ), + { u: 'user', c: 8, s: [[6, 8]] }, + ); //assert.deepEqual( // transformPresence( From 1a0aafdb66e5a313248973236c246d60b21c833c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 15:36:54 +0530 Subject: [PATCH 11/17] Minor cleanup --- lib/json0-presence.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/json0-presence.js b/lib/json0-presence.js index ec330c4..3d8f24c 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -40,20 +40,21 @@ const createPresence = presence => // This needs more thinking/testing, looking a bit more carefully at // how this is implemented in ot-rich-text, etc. const comparePresence = function(pres1, pres2) { - if (!pres1 || !pres2) { - return false; - } - if (!pres1.p || !pres2.p) { - return false; - } - if (pres1.t !== pres2.t) { - return false; - } - if (pres1.t && subtypes[pres1.t]) { - if (pres1.p[0] === pres2.p[0]) { - return subtypes[pres1.t].comparePresence(pres1, pres2); - } - } else return pres1 === pres2; + // TODO add tests + // if (!pres1 || !pres2) { + // return false; + // } + // if (!pres1.p || !pres2.p) { + // return false; + // } + // if (pres1.t !== pres2.t) { + // return false; + // } + // if (pres1.t && subtypes[pres1.t]) { + // if (pres1.p[0] === pres2.p[0]) { + // return subtypes[pres1.t].comparePresence(pres1, pres2); + // } + // } else return pres1 === pres2; }; const transformPresence = (presence, op, isOwn) => { @@ -61,6 +62,7 @@ const transformPresence = (presence, op, isOwn) => { if(c.si || c.sd){ convertFromText(c) } + // TODO generalize to delegate to registered subtypes if(c.t === 'text0') { presence = Object.assign({}, presence, { s: presence.s.map(([start, end]) => ([ From b4a3a0ca61d1725cdebb536f11f8b7ee4042f557 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 16:32:32 +0530 Subject: [PATCH 12/17] Add more top level string insert tests --- lib/json0-presence.js | 19 +++++++++++++++++++ lib/json0.js | 1 + test/json0-presence.js | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/json0-presence.js b/lib/json0-presence.js index 3d8f24c..d673f47 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -72,6 +72,25 @@ const transformPresence = (presence, op, isOwn) => { }); } } + + //if (op.length < 1) { + // return presence; + //} + //const representativeOp = op[0]; + //const opType = op[0].t; + //const path = representativeOp.p && representativeOp.p[0] + //if (opType && subtypes[opType] && path) { + // if (!presence.p || !presence.p[0] || presence.p[0] !== path) { + // return presence + // } + // // return result of running the subtype's transformPresence, + // // but add path and type, which the subtype will not include + // presence = { + // ...subtypes[opType].transformPresence(presence, op, isOwn), + // p: op[0].p, + // t: op[0].t + // }; + //} return presence; }; diff --git a/lib/json0.js b/lib/json0.js index b22ab66..4bb2bbb 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -646,4 +646,5 @@ require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp var text = require('./text0'); json.registerSubtype(text); + module.exports = Object.assign(json, presence); diff --git a/test/json0-presence.js b/test/json0-presence.js index 066ab9b..3b0369c 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -34,6 +34,10 @@ describe('transformPresence', () => { transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], false), {u: 'user', c: 8, s: [[5, 7]]}, ); + }); + it('top level string insert', () => { + + // Before selection assert.deepEqual( transformPresence( {u: 'user', c: 8, s: [[5, 7]]}, @@ -43,6 +47,37 @@ describe('transformPresence', () => { { u: 'user', c: 8, s: [[6, 8]] }, ); + // Inside selection + assert.deepEqual( + transformPresence( + {u: 'user', c: 8, s: [[5, 7]]}, + [{p: [6], si: 'a'}], + true, + ), + { u: 'user', c: 8, s: [[5, 8]] }, + ); + + // Multiple characters + assert.deepEqual( + transformPresence( + {u: 'user', c: 8, s: [[5, 7]]}, + [{p: [6], si: 'abc'}], + true, + ), + { u: 'user', c: 8, s: [[5, 10]] }, + ); + + // After selection + assert.deepEqual( + transformPresence( + {u: 'user', c: 8, s: [[5, 7]]}, + [{p: [8], si: 'a'}], + true, + ), + { u: 'user', c: 8, s: [[5, 7]] }, + ); + }); + //assert.deepEqual( // transformPresence( // {u: 'user', c: 8, s: [[5, 7]]}, @@ -138,7 +173,6 @@ describe('transformPresence', () => { // s: [[2, 2], [3, 3]], // }, //); - }); }); // // describe('comparePresence', () => { From 517d7004329308e945eb77cf6ae45c6ca23c2c3c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 16:36:26 +0530 Subject: [PATCH 13/17] Add string deletion test --- test/json0-presence.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/json0-presence.js b/test/json0-presence.js index 3b0369c..4519444 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -35,7 +35,7 @@ describe('transformPresence', () => { {u: 'user', c: 8, s: [[5, 7]]}, ); }); - it('top level string insert', () => { + it('top level string operations', () => { // Before selection assert.deepEqual( @@ -67,6 +67,16 @@ describe('transformPresence', () => { { u: 'user', c: 8, s: [[5, 10]] }, ); + // String deletion + assert.deepEqual( + transformPresence( + {u: 'user', c: 8, s: [[5, 7]]}, + [{p: [5], sd: 'abc'}], + true, + ), + { u: 'user', c: 8, s: [[5, 5]] }, + ); + // After selection assert.deepEqual( transformPresence( From 4a05091498baadaed268c7026d7861b349485ec8 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 16:45:16 +0530 Subject: [PATCH 14/17] Begin work on nested paths --- lib/json0-presence.js | 13 +- test/json0-presence.js | 273 ++++++++++++++++++++++------------------- 2 files changed, 156 insertions(+), 130 deletions(-) diff --git a/lib/json0-presence.js b/lib/json0-presence.js index d673f47..c3e2155 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -62,13 +62,16 @@ const transformPresence = (presence, op, isOwn) => { if(c.si || c.sd){ convertFromText(c) } - // TODO generalize to delegate to registered subtypes if(c.t === 'text0') { presence = Object.assign({}, presence, { - s: presence.s.map(([start, end]) => ([ - transformCursor(start, c.o), - transformCursor(end, c.o), - ])) + s: presence.s.map(selection => { + const path = selection.slice(0, selection.length - 2); + const [start, end] = selection.slice(selection.length - 2); + return path.concat([ + transformCursor(start, c.o), + transformCursor(end, c.o), + ]); + }) }); } } diff --git a/test/json0-presence.js b/test/json0-presence.js index 4519444..3fff318 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -1,24 +1,27 @@ const assert = require('assert'); -const {createPresence, transformPresence} = require('../lib/json0'); +const { createPresence, transformPresence } = require('../lib/json0'); // These tests are inspired by the ones found here: // https://github.com/Teamwork/ot-rich-text/blob/master/test/Operation.js describe('createPresence', () => { it('basic tests', () => { - const defaultPresence = {u: '', c: 0, s: []}; - const presence = {u: '5', c: 8, s: [[1, 2], [9, 5]]}; + const defaultPresence = { u: '', c: 0, s: [] }; + const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; assert.deepEqual(createPresence(), defaultPresence); assert.deepEqual(createPresence(null), defaultPresence); assert.deepEqual(createPresence(true), defaultPresence); - assert.deepEqual(createPresence({u: 5, c: 8, s: [1, 2]}), defaultPresence); assert.deepEqual( - createPresence({u: '5', c: '8', s: [1, 2]}), - defaultPresence, + createPresence({ u: 5, c: 8, s: [1, 2] }), + defaultPresence ); assert.deepEqual( - createPresence({u: '5', c: 8, s: [1.5, 2]}), - defaultPresence, + createPresence({ u: '5', c: '8', s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: 8, s: [1.5, 2] }), + defaultPresence ); assert.strictEqual(createPresence(presence), presence); }); @@ -27,162 +30,182 @@ describe('createPresence', () => { describe('transformPresence', () => { it('basic tests', () => { assert.deepEqual( - transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], true), - {u: 'user', c: 8, s: [[5, 7]]}, + transformPresence({ u: 'user', c: 8, s: [[5, 7]] }, [], true), + { u: 'user', c: 8, s: [[5, 7]] } ); assert.deepEqual( - transformPresence({u: 'user', c: 8, s: [[5, 7]]}, [], false), - {u: 'user', c: 8, s: [[5, 7]]}, + transformPresence({ u: 'user', c: 8, s: [[5, 7]] }, [], false), + { u: 'user', c: 8, s: [[5, 7]] } ); }); - it('top level string operations', () => { + it('top level string operations', () => { // Before selection assert.deepEqual( transformPresence( - {u: 'user', c: 8, s: [[5, 7]]}, - [{p: [0], si: 'a'}], // Insert the 'a' character at position 0. - true, + { u: 'user', c: 8, s: [[5, 7]] }, + [{ p: [0], si: 'a' }], // Insert the 'a' character at position 0. + true ), - { u: 'user', c: 8, s: [[6, 8]] }, + { u: 'user', c: 8, s: [[6, 8]] } ); // Inside selection assert.deepEqual( transformPresence( - {u: 'user', c: 8, s: [[5, 7]]}, - [{p: [6], si: 'a'}], - true, + { u: 'user', c: 8, s: [[5, 7]] }, + [{ p: [6], si: 'a' }], + true ), - { u: 'user', c: 8, s: [[5, 8]] }, + { u: 'user', c: 8, s: [[5, 8]] } ); // Multiple characters assert.deepEqual( transformPresence( - {u: 'user', c: 8, s: [[5, 7]]}, - [{p: [6], si: 'abc'}], - true, + { u: 'user', c: 8, s: [[5, 7]] }, + [{ p: [6], si: 'abc' }], + true ), - { u: 'user', c: 8, s: [[5, 10]] }, + { u: 'user', c: 8, s: [[5, 10]] } ); // String deletion assert.deepEqual( transformPresence( - {u: 'user', c: 8, s: [[5, 7]]}, - [{p: [5], sd: 'abc'}], - true, + { u: 'user', c: 8, s: [[5, 7]] }, + [{ p: [5], sd: 'abc' }], + true ), - { u: 'user', c: 8, s: [[5, 5]] }, + { u: 'user', c: 8, s: [[5, 5]] } ); // After selection assert.deepEqual( transformPresence( - {u: 'user', c: 8, s: [[5, 7]]}, - [{p: [8], si: 'a'}], - true, + { u: 'user', c: 8, s: [[5, 7]] }, + [{ p: [8], si: 'a' }], + true + ), + { u: 'user', c: 8, s: [[5, 7]] } + ); + }); + + it('nested string operations', () => { + assert.deepEqual( + transformPresence( + { u: 'user', c: 8, s: [['content', 5, 7]] }, + [{ p: ['content', 0], si: 'a' }], // Insert the 'a' character at position 0. + true + ), + { u: 'user', c: 8, s: [['content', 6, 8]] } + ); + + assert.deepEqual( + transformPresence( + { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 5, 7]] }, + [{ p: ['content', 'deeply', 'nested', 0], si: 'a' }], // Insert the 'a' character at position 0. + true ), - { u: 'user', c: 8, s: [[5, 7]] }, + { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 6, 8]] } ); }); - //assert.deepEqual( - // transformPresence( - // {u: 'user', c: 8, s: [[5, 7]]}, - // [createRetain(3), createDelete(2), createInsertText('a')], - // true, - // ), - // { - // u: 'user', - // c: 8, - // s: [[4, 6]], - // }, - //); - //assert.deepEqual( - // transformPresence( - // { - // u: 'user', - // c: 8, - // s: [[5, 7]], - // }, - // [createRetain(3), createDelete(2), createInsertText('a')], - // false, - // ), - // { - // u: 'user', - // c: 8, - // s: [[3, 6]], - // }, - //); + //assert.deepEqual( + // transformPresence( + // {u: 'user', c: 8, s: [[5, 7]]}, + // [createRetain(3), createDelete(2), createInsertText('a')], + // true, + // ), + // { + // u: 'user', + // c: 8, + // s: [[4, 6]], + // }, + //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(3), createDelete(2), createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[3, 6]], + // }, + //); - //assert.deepEqual( - // transformPresence( - // { - // u: 'user', - // c: 8, - // s: [[5, 7]], - // }, - // [createRetain(5), createDelete(2), createInsertText('a')], - // true, - // ), - // { - // u: 'user', - // c: 8, - // s: [[6, 6]], - // }, - //); - //assert.deepEqual( - // transformPresence( - // { - // u: 'user', - // c: 8, - // s: [[5, 7]], - // }, - // [createRetain(5), createDelete(2), createInsertText('a')], - // false, - // ), - // { - // u: 'user', - // c: 8, - // s: [[5, 5]], - // }, - //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(5), createDelete(2), createInsertText('a')], + // true, + // ), + // { + // u: 'user', + // c: 8, + // s: [[6, 6]], + // }, + //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7]], + // }, + // [createRetain(5), createDelete(2), createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[5, 5]], + // }, + //); - //assert.deepEqual( - // transformPresence( - // { - // u: 'user', - // c: 8, - // s: [[5, 7], [8, 2]], - // }, - // [createInsertText('a')], - // false, - // ), - // { - // u: 'user', - // c: 8, - // s: [[6, 8], [9, 3]], - // }, - //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[5, 7], [8, 2]], + // }, + // [createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[6, 8], [9, 3]], + // }, + //); - //assert.deepEqual( - // transformPresence( - // { - // u: 'user', - // c: 8, - // s: [[1, 1], [2, 2]], - // }, - // [createInsertText('a')], - // false, - // ), - // { - // u: 'user', - // c: 8, - // s: [[2, 2], [3, 3]], - // }, - //); + //assert.deepEqual( + // transformPresence( + // { + // u: 'user', + // c: 8, + // s: [[1, 1], [2, 2]], + // }, + // [createInsertText('a')], + // false, + // ), + // { + // u: 'user', + // c: 8, + // s: [[2, 2], [3, 3]], + // }, + //); }); // // describe('comparePresence', () => { From 161e6a14e330ece6b1f4cd17945dc29cfdc15ce6 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 17:01:41 +0530 Subject: [PATCH 15/17] Handle case of op not matching selection path --- lib/json0-presence.js | 16 ++++++++++------ lib/json0.js | 35 ++++------------------------------- lib/utils.js | 32 +++++++++++++++++++++++++++++++- test/json0-presence.js | 13 +++++++++++++ 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/lib/json0-presence.js b/lib/json0-presence.js index c3e2155..2552385 100644 --- a/lib/json0-presence.js +++ b/lib/json0-presence.js @@ -1,5 +1,5 @@ const { transformCursor } = require('./text0'); -const { convertFromText } = require('./utils'); +const { convertFromText, canOpAffectPath } = require('./utils'); // Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js function isValidPresence(presence) { @@ -63,14 +63,18 @@ const transformPresence = (presence, op, isOwn) => { convertFromText(c) } if(c.t === 'text0') { + //json.canOpAffectPath = function(op, path) { presence = Object.assign({}, presence, { s: presence.s.map(selection => { const path = selection.slice(0, selection.length - 2); - const [start, end] = selection.slice(selection.length - 2); - return path.concat([ - transformCursor(start, c.o), - transformCursor(end, c.o), - ]); + if(canOpAffectPath(c, path)) { + const [start, end] = selection.slice(selection.length - 2); + return path.concat([ + transformCursor(start, c.o), + transformCursor(end, c.o), + ]); + } + return selection; }) }); } diff --git a/lib/json0.js b/lib/json0.js index 4bb2bbb..84bad44 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -2,6 +2,7 @@ var presence = require('./json0-presence') var utils = require('./utils'); var convertFromText = utils.convertFromText; var convertToText = utils.convertToText; + /* This is the implementation of the JSON OT type. @@ -53,7 +54,9 @@ var clone = function(o) { */ var json = { name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' + uri: 'http://sharejs.org/types/JSONv0', + commonLengthForOps: utils.commonLengthForOps, + canOpAffectPath: utils.canOpAffectPath }; // You can register another OT type as a subtype in a JSON document using @@ -356,36 +359,6 @@ json.normalize = function(op) { return newOp; }; -// Returns the common length of the paths of ops a and b -json.commonLengthForOps = function(a, b) { - var alen = a.p.length; - var blen = b.p.length; - if (a.na != null || a.t) - alen++; - - if (b.na != null || b.t) - blen++; - - if (alen === 0) return -1; - if (blen === 0) return null; - - alen--; - blen--; - - for (var i = 0; i < alen; i++) { - var p = a.p[i]; - if (i >= blen || p !== b.p[i]) - return null; - } - - return alen; -}; - -// Returns true if an op can affect the given path -json.canOpAffectPath = function(op, path) { - return json.commonLengthForOps({p:path}, op) != null; -}; - // transform c so it applies to a document with otherC applied. json.transformComponent = function(dest, c, otherC, type) { c = clone(c); diff --git a/lib/utils.js b/lib/utils.js index 8090e2a..2a0f653 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -15,4 +15,34 @@ function convertToText(c) { delete c.o; } -module.exports = { convertFromText, convertToText }; +// Returns the common length of the paths of ops a and b +function commonLengthForOps(a, b) { + var alen = a.p.length; + var blen = b.p.length; + if (a.na != null || a.t) + alen++; + + if (b.na != null || b.t) + blen++; + + if (alen === 0) return -1; + if (blen === 0) return null; + + alen--; + blen--; + + for (var i = 0; i < alen; i++) { + var p = a.p[i]; + if (i >= blen || p !== b.p[i]) + return null; + } + + return alen; +}; + +// Returns true if an op can affect the given path +function canOpAffectPath(op, path) { + return commonLengthForOps({p:path}, op) != null; +}; + +module.exports = { convertFromText, convertToText, commonLengthForOps, canOpAffectPath }; diff --git a/test/json0-presence.js b/test/json0-presence.js index 3fff318..6abc2d3 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -92,6 +92,8 @@ describe('transformPresence', () => { }); it('nested string operations', () => { + + // Single level assert.deepEqual( transformPresence( { u: 'user', c: 8, s: [['content', 5, 7]] }, @@ -101,6 +103,7 @@ describe('transformPresence', () => { { u: 'user', c: 8, s: [['content', 6, 8]] } ); + // Multiple level assert.deepEqual( transformPresence( { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 5, 7]] }, @@ -109,6 +112,16 @@ describe('transformPresence', () => { ), { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 6, 8]] } ); + + // Op not matching path + assert.deepEqual( + transformPresence( + { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 5, 7]] }, + [{ p: ['content', 'somewhere', 'else', 0], si: 'a' }], // Insert the 'a' character at position 0. + true + ), + { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 5, 7]] } + ); }); //assert.deepEqual( From c672615822fbfe058a6bd080849625b32de788d5 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 10 Apr 2019 17:06:24 +0530 Subject: [PATCH 16/17] Add test for multiple selections --- test/json0-presence.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/json0-presence.js b/test/json0-presence.js index 6abc2d3..87fafa9 100644 --- a/test/json0-presence.js +++ b/test/json0-presence.js @@ -92,7 +92,6 @@ describe('transformPresence', () => { }); it('nested string operations', () => { - // Single level assert.deepEqual( transformPresence( @@ -122,6 +121,30 @@ describe('transformPresence', () => { ), { u: 'user', c: 8, s: [['content', 'deeply', 'nested', 5, 7]] } ); + + // Multiple selections + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [ + ['content', 'deeply', 'nested', 5, 7], + ['content', 'somewhere', 'else', 5, 7] + ] + }, + [{ p: ['content', 'somewhere', 'else', 0], si: 'a' }], // Insert the 'a' character at position 0. + true + ), + { + u: 'user', + c: 8, + s: [ + ['content', 'deeply', 'nested', 5, 7], + ['content', 'somewhere', 'else', 6, 8] + ] + } + ); }); //assert.deepEqual( From 65207d79d01a7732bf68799f08c1b997dc22611e Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 11 Apr 2019 07:20:33 +0530 Subject: [PATCH 17/17] Revise overall presence structure in README --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6314a10..08f2ae6 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,29 @@ The shape of our presence data is as follows: ``` { - u: '123', // user ID - c: 8, // number of changes made by this user - s: [ // list of selections - [ 1, 1 ], // collapsed selection - [ 5, 7 ], // forward selection - [ 9, 4 ] // backward selection - ] + user: '123', // User ID. + changes: 8, // Number of changes made by this user (for change detection). + presence: [ // List of sub-presence objects, per OT type. + { + type: 'rich-text', // The OT type for this presence object. + path: ['some', 'path'], // The path of this presence object. + subPresence: { // The type-specific presence object at this path. + u: '123', c: 8, // An example of an ot-rich-text presence object. + s: [ [ 1, 1 ], [ 5, 7 ], [ 9, 4 ] ] + } + }, + { + type: 'text0', // An example of a text0 presence object. + path: ['some', 'other', 'path'], + subPresence: { + u: '123', c: 8, + s: [ [ 1, 1 ], [ 5, 7 ], [ 9, 4 ] ] + } + } + ] } ``` -Each selection listed in `s` ends with a 2-element array containing the selection start index and the selection end index. The elements in the array preceeding the last two represent the path of a `text0` entry within the `json0` data structure. - -For example, the following entry in the `s` array represents the user's cursor position within the `content` field (`data.content`): - -``` -['content', 2, 2] -``` - -We can access deeply nested entries with this structure as well. For example, the following `s` entry represents a text selection in `data.files[3].text`: - -``` -['files', 3, 'text', 4, 7] -``` The rest of the README content is from the original repo https://github.com/ottypes/json0.