From 29894ab024233d7c30ef5d53029ea2a01310ce51 Mon Sep 17 00:00:00 2001 From: Charley DAVID Date: Fri, 16 Oct 2020 10:55:18 +0200 Subject: [PATCH 1/3] feat(presence): Add support for presences --- lib/json1.ts | 30 +++++++++++++++-- lib/types.ts | 4 +++ package-lock.json | 38 +++++++++++++++++++++ package.json | 1 + test/presence.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 test/presence.js diff --git a/lib/json1.ts b/lib/json1.ts index 940623b..204430b 100644 --- a/lib/json1.ts +++ b/lib/json1.ts @@ -20,8 +20,8 @@ // import log from './log' import deepEqual from './deepEqual.js' import deepClone from './deepClone.js' -import {ReadCursor, WriteCursor, readCursor, writeCursor, advancer, eachChildOf, isValidPathItem} from './cursor.js' -import { Doc, JSONOpComponent, Path, Key, JSONOp, JSONOpList, Conflict, ConflictType } from './types.js' +import { ReadCursor, WriteCursor, readCursor, writeCursor, advancer, eachChildOf, isValidPathItem } from './cursor.js' +import { Doc, JSONOpComponent, Path, Presence, Key, JSONOp, JSONOpList, Conflict, ConflictType } from './types.js' const RELEASE_MODE = process.env.JSON1_RELEASE_MODE const log: (...args: any) => void = RELEASE_MODE ? (() => {}) : require('./log').default @@ -57,6 +57,7 @@ export const type = { apply, transformPosition, + transformPresence, compose, tryTransform, transform, @@ -688,7 +689,11 @@ function transformPosition(path: Path, op: JSONOp): Path | null { if (et) { const e = getEdit(c!) log('Embedded edit', e, et) - if (et.transformPosition) path[i] = et.transformPosition(path[i], e) + if (et.transformPosition) { + path[i] = et.transformPosition(path[i], e) + } else if (et.transformCursor) { + path[i] = et.transformCursor(path[i], e) + } // Its invalid for the operation to have drops inside the embedded edit here. break } @@ -721,6 +726,25 @@ function transformPosition(path: Path, op: JSONOp): Path | null { return path } +function transformPresence(presence: Presence | null, op: JSONOp, isOwnOperation: boolean): Presence | null { + if (!presence || !presence.start || !presence.end) { + return null + } + + const start = transformPosition(presence.start, op) + const end = transformPosition(presence.end, op) + + if (start && end) { + return { start, end } + } else if (start) { + return { start, end: start } + } else if (end) { + return { start: end, end } + } + + return null +} + // ****** Compose diff --git a/lib/types.ts b/lib/types.ts index 30b4bad..09979c1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -57,6 +57,10 @@ export type JSONOp = null | JSONOpList export type Key = number | string export type Path = Key[] +export type Presence = { + start: Path, + end: Path, +} /** * JSON documents must be able to round-trip through JSON.stringify / diff --git a/package-lock.json b/package-lock.json index d4b1f80..7a0735d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -297,6 +297,12 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -525,6 +531,18 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -721,6 +739,17 @@ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, + "quill-delta": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz", + "integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==", + "dev": true, + "requires": { + "fast-diff": "1.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + } + }, "readdirp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", @@ -742,6 +771,15 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "rich-text": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rich-text/-/rich-text-4.1.0.tgz", + "integrity": "sha512-zQg80us6AfopS7s2YPsU+jfLX1RJaOdd6w26Jz4Al1G73AMzqDLTIZSnr0I7HTdCIU1gcGSMDlhq2v4IfoKfbg==", + "dev": true, + "requires": { + "quill-delta": "^4.2.1" + } + }, "seedrandom": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", diff --git a/package.json b/package.json index 43e2333..5d091e5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "mocha": "^7.1.1", "ot-fuzzer": "1.3", "ot-simple": "^1.0.0", + "rich-text": "^4.1.0", "terser": "^4.6.7", "typescript": "^3.9.5" }, diff --git a/test/presence.js b/test/presence.js new file mode 100644 index 0000000..3949ea8 --- /dev/null +++ b/test/presence.js @@ -0,0 +1,86 @@ +const assert = require("assert"); +const Delta = require("quill-delta"); +const { type } = require("../dist/json1.release"); + +type.registerSubtype(require("rich-text")); + +describe("presences", () => { + it("transforms json1 only presences", () => { + const op = ["z", 0, { i: "hello" }]; + const presenceBefore = { start: ["z", 0], end: ["z", 1] }; + const presenceAfter = { start: ["z", 1], end: ["z", 2] }; + + assert.deepStrictEqual( + type.transformPresence(presenceBefore, op), + presenceAfter + ); + }); + + it("transforms json1 + text-unicode presences", () => { + const op = ["z", [0, { i: "hello" }], [2, { es: ["hi"] }]]; + const presenceBefore = { start: ["z", 1, 1], end: ["z", 2, 0] }; + const presenceAfter = { start: ["z", 2, 3], end: ["z", 3, 0] }; + + assert.deepStrictEqual( + type.transformPresence(presenceBefore, op), + presenceAfter + ); + }); + + it("transforms json1 + rich-text presences", () => { + const op = [ + "z", + [0, { i: "hello" }], + [4, { et: "rich-text", e: new Delta([{ insert: "bye" }]) }], + ]; + const presenceBefore = { start: ["z", 3, 1], end: ["z", 4, 0] }; + const presenceAfter = { start: ["z", 4, 4], end: ["z", 5, 0] }; + + assert.deepStrictEqual( + type.transformPresence(presenceBefore, op), + presenceAfter + ); + }); + + it("does nothing on null presences", () => { + const op = ["z", 0, { i: "hello" }]; + + assert.deepStrictEqual(type.transformPresence(null, op), null); + }); + + it("returns null on missing start or end", () => { + const op = ["z", 0, { i: "hello" }]; + + assert.deepStrictEqual( + type.transformPresence({ start: null, end: [] }, op), + null + ); + + assert.deepStrictEqual( + type.transformPresence({ start: [], end: null }, op), + null + ); + }); + + it("returns collapsed presence when start container is removed", () => { + const op = ["z", 0, { r: true }]; + const presenceBefore = { start: ["z", 0], end: ["z", 1] }; + const presenceAfter = { start: ["z", 0], end: ["z", 0] }; + + assert.deepStrictEqual( + type.transformPresence(presenceBefore, op), + presenceAfter + ); + }); + + it("returns collapsed presence when end container is removed", () => { + const op = ["z", 1, { r: true }]; + const presenceBefore = { start: ["z", 0], end: ["z", 1] }; + const presenceAfter = { start: ["z", 0], end: ["z", 0] }; + + assert.deepStrictEqual( + type.transformPresence(presenceBefore, op), + presenceAfter + ); + }); +}); From f01b8b6c2e1caeb1828ccc57d2acc1794860eb1b Mon Sep 17 00:00:00 2001 From: Charley DAVID Date: Fri, 16 Oct 2020 11:46:01 +0200 Subject: [PATCH 2/3] fix(packages.json): Fix NPM vs yarn lock files --- yarn.lock | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/yarn.lock b/yarn.lock index 1046d08..64c7fc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -217,6 +217,11 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +fast-diff@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -396,6 +401,16 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash@^4.17.15: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -569,6 +584,15 @@ picomatch@^2.0.4: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +quill-delta@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.2.2.tgz#015397d046e0a3bed087cd8a51f98c11a1b8f351" + integrity sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg== + dependencies: + fast-diff "1.2.0" + lodash.clonedeep "^4.5.0" + lodash.isequal "^4.5.0" + readdirp@~3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" @@ -586,6 +610,13 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +rich-text@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rich-text/-/rich-text-4.1.0.tgz#8b4c0e48a84fbb535dd692998f1e39eaaf0451a8" + integrity sha512-zQg80us6AfopS7s2YPsU+jfLX1RJaOdd6w26Jz4Al1G73AMzqDLTIZSnr0I7HTdCIU1gcGSMDlhq2v4IfoKfbg== + dependencies: + quill-delta "^4.2.1" + seedrandom@^2.4.4: version "2.4.4" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba" From 70dabfd6075588b5ecfee37056f9be4f69718c56 Mon Sep 17 00:00:00 2001 From: Charley DAVID Date: Thu, 22 Oct 2020 18:02:38 +0200 Subject: [PATCH 3/3] feat(presence): Support for arbitrary data --- lib/json1.ts | 6 +++--- lib/types.ts | 1 + test/presence.js | 9 +++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/json1.ts b/lib/json1.ts index 204430b..733ea9e 100644 --- a/lib/json1.ts +++ b/lib/json1.ts @@ -735,11 +735,11 @@ function transformPresence(presence: Presence | null, op: JSONOp, isOwnOperation const end = transformPosition(presence.end, op) if (start && end) { - return { start, end } + return { ...presence, start, end } } else if (start) { - return { start, end: start } + return { ...presence, start, end: start } } else if (end) { - return { start: end, end } + return { ...presence, start: end, end } } return null diff --git a/lib/types.ts b/lib/types.ts index 09979c1..de7412d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -60,6 +60,7 @@ export type Path = Key[] export type Presence = { start: Path, end: Path, + [key: string]: any } /** diff --git a/test/presence.js b/test/presence.js index 3949ea8..e74e54b 100644 --- a/test/presence.js +++ b/test/presence.js @@ -83,4 +83,13 @@ describe("presences", () => { presenceAfter ); }); + + it("allows arbitrary data in presences", () => { + const op = ["z", 0, { i: "hello" }]; + const data = { user: "John Doe", color: "blue" }; + const presenceBefore = { start: ["z", 0], end: ["z", 1], data }; + const presenceAfter = type.transformPresence(presenceBefore, op); + + assert.ok(presenceBefore.data === presenceAfter.data); + }); });