Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presence #31

Open
wants to merge 45 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7235c8f
Presence changes
houshuang Apr 4, 2019
7023ee3
WIP
houshuang Apr 4, 2019
22c5bd4
WIP
houshuang Apr 4, 2019
b25c3a7
WIP
houshuang Apr 6, 2019
beb947c
WIP
houshuang Apr 11, 2019
aadf1ef
WIP
houshuang Apr 11, 2019
957924d
Remove minor formatting changes to clean up PR diff.
curran Apr 12, 2019
ca187aa
Remove minor formatting changes to clean up PR diff.
curran Apr 12, 2019
44ca0a0
Remove minor formatting changes to clean up PR diff.
curran Apr 12, 2019
1b12811
Restore original package.json
curran Apr 12, 2019
d797551
Remove yarn.lock (as this was not already there in json0)
curran Apr 12, 2019
62f6acd
Fix undefined reference
curran Apr 12, 2019
bf02b8c
Comment unused code
curran Apr 12, 2019
73adc91
Simplify comparePresence
curran Apr 12, 2019
095967e
Simplify transformPresence
curran Apr 12, 2019
6435e13
Add stub for presence tests
curran Apr 12, 2019
a249e05
Add sketch for presence structure in README
curran Apr 12, 2019
151c6ea
Add first pass implementation from earlier
curran Apr 12, 2019
1a359af
Invoke subtype transformPresence
curran Apr 12, 2019
a2ab7e7
Revise proposed structure for presence
curran Apr 12, 2019
f3eb080
Revise structure in README
curran Apr 12, 2019
db5deeb
Fix invocation of subtype .transformPresence
curran Apr 12, 2019
00ecd57
Add first substantive test case for transformPresence
curran Apr 12, 2019
78b02ba
Clean transformPresence implementation. Introduce unpackPresence.
curran Apr 12, 2019
52e0fb8
Add test case for matching presence type
curran Apr 13, 2019
1d3d6da
Add test case for non-matching path
curran Apr 13, 2019
d5b7d0b
Match style of existing code
curran Apr 13, 2019
bc3a74f
Match style of existing code - use ES5
curran Apr 13, 2019
552794c
Start working on text0 presence
curran Apr 13, 2019
bcb60c4
Add text0 transformPresence
curran Apr 13, 2019
3c9c3d0
Add tests for isOwnPosition behavior
curran Apr 13, 2019
a92c376
Clean up PR diff
curran Apr 13, 2019
072d49b
Clean up PR diff
curran Apr 13, 2019
b8ca428
Revise shape of presence data structure
curran Apr 13, 2019
424c424
Simplify implementation
curran Apr 13, 2019
d1edf5a
Add test for transformPresence by op with multiple components
curran Apr 13, 2019
e7c6f03
Finalize tests for text0-presence
curran Apr 13, 2019
19431c7
Add transformPresence tests for embedded text0 ops
curran Apr 13, 2019
0f8290e
Pass through path-only presence objects
curran Apr 13, 2019
94540e1
Remove dead code, minor cleanup
curran Apr 15, 2019
d9d32a4
Namespace package and increment minor version
curran Apr 15, 2019
95f20d8
Add link to presence demo
curran Apr 15, 2019
90a06fe
Add usage instructions and context.
curran Apr 16, 2019
a3ecc44
Merge branch 'master' into presence-attempt-4
curran Apr 16, 2019
3cd4ef6
Reset package.json for upstream PR
curran Apr 16, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.swp
*.DS_Store
node_modules
package-lock.json
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

The JSON OT type can be used to edit arbitrary JSON documents.

Forked from [ottypes/json0](https://github.com/ottypes/json0) to implement [presence](https://github.com/ottypes/docs/issues/29).

Current status: Presence is basically working, but it's only transformed by subtype ops. Remaining work includes transforming presence by ops that are not text (`si`, `sd`) or subtype ops. Includes `li`, `ld`, `lm`, `oi`, `od`. The goal is that one day this fork will be merged into ottypes/json0 via this PR: [ottypes/json0: Presence](https://github.com/ottypes/json0/pull/31).

In the mean time, this fork is published on NPM as [@datavis-tech/ot-json0](https://www.npmjs.com/package/@datavis-tech/ot-json0). If you want to try it out:

```
npm install -S @datavis-tech/ot-json0
```

To [use it as the default ShareDB OT Type](https://github.com/share/sharedb/issues/284), you'll need to do the following (in both client and server):

```js
const json0 = require('fork-of-ot-json0');
const ShareDB = require('sharedb'); // or require('sharedb/lib/client');
ShareDB.types.register(json0.type);
ShareDB.types.defaultType = json0.type;
```

To use the presence feature, you'll need to use the [Teamwork fork of ShareDB](https://github.com/teamwork/sharedb#readme) until the [ShareDB Presence PR](https://github.com/share/sharedb/pull/207) is merged.

## Features

The JSON OT type supports the following operations:
Expand Down Expand Up @@ -294,6 +315,31 @@ offset in a string. `TEXT` must be contained at the location specified.

---

## Presence

(inspired by https://github.com/Teamwork/ot-rich-text#presence)

The shape of our presence data is as follows:

```js
{
p: ['some', 'path'], // Path of the presence.
t: 'ot-rich-text', // Subtype of the presence (a registered subtype).
s: { // Opaque presence object (subtype-specific structure).
u: '123', // An example of an ot-rich-text presence object.
c: 8,
s: [ [ 1, 1 ], [ 5, 7 ]]
}
}
```

Here's a demo https://github.com/datavis-tech/json0-presence-demo

![presence](https://user-images.githubusercontent.com/68416/56134824-ffac3400-5fac-11e9-89a1-c60064c3eb67.gif)

![selections](https://user-images.githubusercontent.com/68416/56134832-033fbb00-5fad-11e9-9274-a19b2287c5b1.gif)


# Commentary

This library was written a couple of years ago by [Jeremy Apthorp](https://github.com/nornagon). It was
Expand Down
37 changes: 37 additions & 0 deletions lib/json0.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,43 @@ json.transformComponent = function(dest, c, otherC, type) {
return dest;
};

json.createPresence = function(presenceData) {
return presenceData;
};

json.comparePresence = function(pres1, pres2) {
return JSON.stringify(pres1) === JSON.stringify(pres2);
};

json.transformPresence = function(presence, op, isOwnOp) {
// Don't transform path-only presence objects.
if(!presence.t) return presence;

for (var i = 0; i < op.length; i++) {
var c = op[i];

// convert old string ops to use subtype for backwards compatibility
if (c.si != null || c.sd != null) {
convertFromText(c);
}

// Transform against subtype ops.
if (c.t && c.t === presence.t && json.pathMatches(c.p, presence.p)) {
presence = Object.assign({}, presence, {
s: subtypes[presence.t].transformPresence(presence.s, c.o, isOwnOp)
});
}

// convert back to old string ops
if (c.t === 'text0') {
convertToText(c);
}

// TODO transform against non-subtype ops.
};
return presence;
};

require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp, json.append);

/**
Expand Down
30 changes: 30 additions & 0 deletions lib/text0.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,34 @@ text.invert = function(op) {
return op;
};

text.createPresence = function(presenceData) {
return presenceData;
};

// Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js
text.transformPresence = function(presence, operation, isOwnOperation) {
var user = presence.u;
var change = presence.c;
var selections = presence.s;
var side = isOwnOperation ? 'right' : 'left';
var newSelections = new Array(selections.length);

for (var i = 0, l = selections.length; i < l; ++i) {
newSelections[i] = [
text.transformCursor(selections[i][0], operation, side),
text.transformCursor(selections[i][1], operation, side)
];
}

return {
u: user,
c: change,
s: newSelections
}
}

text.comparePresence = function(pres1, pres2) {
return JSON.stringify(pres1) === JSON.stringify(pres2);
};

require('./bootstrapTransform')(text, transformComponent, checkValidOp, append);
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"name": "ot-json0",
"version": "1.1.0",
"version": "1.2.0",
"description": "JSON OT type",
"main": "lib/index.js",
"directories": {
"test": "test"
},
"dependencies": {},
"devDependencies": {
"ot-fuzzer": "^1.0.0",
"@teamwork/ot-rich-text": "^8.1.0",
"coffee-script": "^1.7.1",
"mocha": "^1.20.1",
"coffee-script": "^1.7.1"
"ot-fuzzer": "^1.0.0"
},
"scripts": {
"test": "mocha"
Expand Down
152 changes: 152 additions & 0 deletions test/json0-presence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
const assert = require('assert');
const json = require('../lib/json0');
const text = require('../lib/text0');
const otRichText = require('@teamwork/ot-rich-text')

const { createInsertText, createRetain, createDelete } = otRichText.Action

json.registerSubtype(otRichText.type);

const { createPresence, comparePresence, transformPresence } = json;

// Sample presence object using ot-rich-text sub-presence.
const samplePresence = {
p: ['some', 'path'], // Path of the presence.
t: 'ot-rich-text', // Subtype of the presence (a registered subtype).
s: { // Opaque presence object (subtype-specific structure).
u: '123', // An example of an ot-rich-text presence object.
c: 8,
s: [ [ 1, 1 ], [ 5, 7 ]]
}
}

// Sample presence object using text0 sub-presence.
const sampleTextPresence = Object.assign({}, samplePresence, {
t: 'text0'
});

// Sample presence object indicating only that
// the user has "joined" the document at the top level.
const samplePathOnlyPresence = { p: [] };

// These tests are inspired by the ones found here:
// https://github.com/Teamwork/ot-rich-text/blob/master/test/Operation.js
describe('json0 presence', () => {
describe('createPresence', () => {
it('should return the passed in presence object', () => {
assert.strictEqual(createPresence(samplePresence), samplePresence);
});
});

describe('comparePresence', () => {
it('should return true if equal', () => {
assert(comparePresence(samplePresence, samplePresence));
});

it('should return false if not equal', () => {
assert(!comparePresence(samplePresence, sampleTextPresence));
});
});

describe('transformPresence', () => {
it('should preserve original presence in case of no-op', () => {
assert.deepEqual(
transformPresence(samplePresence, [], true),
samplePresence
);
assert.deepEqual(
transformPresence(samplePresence, [], false),
samplePresence
);
});

it('should transform by subtype op with matching path and subtype', () => {
const o = [ createInsertText('a') ];
const op = [{ p: ['some', 'path'], t: otRichText.type.name, o }];
const isOwnOp = true;

assert.deepEqual(
transformPresence( samplePresence, op, isOwnOp),
Object.assign({}, samplePresence, {
s: otRichText.type.transformPresence(samplePresence.s, o, isOwnOp)
})
);
});

it('should transform by op with multiple components', () => {
const o1 = [ createInsertText('foo') ];
const o2 = [ createRetain(3), createDelete(2), createInsertText('a') ];

let s = samplePresence.s;
s = otRichText.type.transformPresence(s, o1);
s = otRichText.type.transformPresence(s, o2);

assert.deepEqual(
transformPresence(samplePresence, [
{ p: ['some', 'path'], t: otRichText.type.name, o: o1 },
{ p: ['some', 'path'], t: otRichText.type.name, o: o2 }
]),
Object.assign({}, samplePresence, { s })
);
});

it('should not transform by op with matching path and non-matching subtype', () => {
assert.deepEqual(
transformPresence(samplePresence, [{
p: ['some', 'path'],
t: 'some-invalid-name',
o: [ createInsertText('a') ]
}]),
samplePresence
);
});

it('should not transform by op with non-matching path and matching subtype', () => {
assert.deepEqual(
transformPresence(samplePresence, [{
p: ['some', 'other', 'path'],
t: otRichText.type.name,
o: [ createInsertText('a') ]
}]),
samplePresence
);
});

it('should transform by text0 op', () => {
const o = [{ p: 0, i: 'a' }];
const op = [{ p: ['some', 'path'], t: text.name, o }]; // text0 op
assert.deepEqual(
transformPresence(sampleTextPresence, op),
Object.assign({}, sampleTextPresence, {
s: text.transformPresence(sampleTextPresence.s, o)
})
);
});

it('should transform by text op (auto-convert to & from internal text0 type)', () => {
const o = [{ p: 0, i: 'a' }];
const op = [{ p: ['some', 'path', 0], si: 'a' }]; // json0 text op
const opClone = JSON.parse(JSON.stringify(op));

assert.deepEqual(
transformPresence(sampleTextPresence, op),
Object.assign({}, sampleTextPresence, {
s: text.transformPresence(sampleTextPresence.s, o)
})
);

// Ensure the original op survives.
assert.deepEqual(op, opClone);
});

it('should not break when given path-only presence', () => {
assert.deepEqual(
transformPresence(samplePathOnlyPresence, [{
p: ['some', 'path', 0],
si: 'a'
}]),
samplePathOnlyPresence
);
});
});
});
Loading