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

Inserting non text characters into Automerge.Text #194

Closed
Gozala opened this issue Jul 31, 2019 · 10 comments · Fixed by #238
Closed

Inserting non text characters into Automerge.Text #194

Gozala opened this issue Jul 31, 2019 · 10 comments · Fixed by #238

Comments

@Gozala
Copy link
Contributor

Gozala commented Jul 31, 2019

This is related to #193 as I attempt to model rich text as series of tokens of characters and markers:

{format:{italic:true}}Hello {format:{bold:true, italic:true}}beautiful{format:null} world\n

As I was trying to figure out a best approach to this I noticed that Automerge.Text would happily insert objects e.g. text.insertAt(0, { bold: true }) which than would appear as objectIds inside the text. (related thread on slack) It's also worth mentioning that Automerge.getObjectById(doc, objectId) would actually return inserted object.

I'm not sure what is the intended behavior here, but I suspect that text.join('') including objectId is highly unlikely. It seemed like a convenient accident for my use case, but I'm suspicious there might be deeper consequences so I chose to use list (or should I say array) of tokens instead. Either way I would like to propose:

  1. To document how Automerge.Text outperforms arrays (as suggested by the readme).
  2. Either throw if non-text is being inserted or omit objectIds in .join() and document non textual inserts bit more.
@ept
Copy link
Member

ept commented Jul 31, 2019

I think it would be good for Automerge.Text to support marker objects, and the current behaviour is broken. We should definitely change it so that text.insertAt(0, {bold: true}) works, and when you follow it with text.get(0) you get back the {bold: true} object.

Not sure what .join() should do in this case. Maybe simply skip over any marker objects?

@Gozala
Copy link
Contributor Author

Gozala commented Jul 31, 2019

I think it would be good for Automerge.Text to support marker objects, and the current behaviour is broken. We should definitely change it so that text.insertAt(0, {bold: true}) works, and when you follow it with text.get(0) you get back the {bold: true} object.

Not sure what .join() should do in this case. Maybe simply skip over any marker objects?

That is exactly what my RichText class that wraps list of tokens does. I'd love to let Automerge.Text do that.

@Gozala
Copy link
Contributor Author

Gozala commented Jul 31, 2019

Actually currently my RichText doesn't take just {bold:true} instead it looks more like this:

export type Resource = {[string]:string|number|boolean|null} 
export type Attributes = {[string]:string|number|boolean|null}
export type CharacterToken = string
export type MarkerToken = {|attributes:Attributes, length:0|}
export type EmbedToken = {|resource:Resource, attributes:?Attributes, length:1|}

export type Token =
  | CharacterToken
  | EmbedToken
  | MarkerToken

export type RichTextBuffer = MutableList<Token>

I think it might be more reasonable to add char: ?string field to MarkerToken and EmbedToken and let .join() use it if present or omit otherwise.

@Gozala
Copy link
Contributor Author

Gozala commented Aug 1, 2019

We should definitely change it so that text.insertAt(0, {bold: true}) works, and when you follow it with text.get(0) you get back the {bold: true} object.

I've tried to write a patch for this, but I got stuck - As far as I can tell Text instances only hold elems that don't contain any references to inserted objects nor nor to a document to be able to get corresponding objects from.

I suspect this is where change needs to occur but I'm not confident make those chanages
https://github.com/automerge/automerge/blob/d05b2fee907668ff10512fa2715e25e58e23ce2a/frontend/apply_patch.js#L344

@Gozala
Copy link
Contributor Author

Gozala commented Aug 6, 2019

Not sure what .join() should do in this case. Maybe simply skip over any marker objects?

@ept Is .join() on Text is even necessary, I realize it end up there because Text is list of characters, however conceptually it more like a JS String which does not have a .join method. I also don't really find it all that useful for Text.

@Gozala
Copy link
Contributor Author

Gozala commented Aug 6, 2019

think it would be good for Automerge.Text to support marker objects, and the current behaviour is broken. We should definitely change it so that text.insertAt(0, {bold: true}) works, and when you follow it with text.get(0) you get back the {bold: true} object.

I was thinking more about this & I'm starting to think that it would be better to have two distinct types Automerge.Text for plaint text (and no marker token) and a separate (maybe Automerge.Markup) for text and markers. That would reduce margin for error (I have discovered that non text was supported by mistake text.insertAt(offset, ["h", "e", "l", "l", "o"]) did not realize I had to pass chars as args). And more appropriate API could be chosen for text with markers.

@mattkrick
Copy link
Contributor

not sure if my mention was a mistake, but here's my .02

IIRC Text is just shorthand for an array of chars. if the text is rich, just use a doc.
i ran out of dev time for it, but an example is here: https://github.com/mattkrick/rich

see atlaskit by atlassian and prosemirror, which it's built on. if you wanted to build a CRDT plugin on top of prosemirror, it'd be revolutionary, although last time i checked it was too hard-wired for OTs.

as for type errors, i'd recommend just PRing the types for this package & using typescript.

@Gozala
Copy link
Contributor Author

Gozala commented Aug 6, 2019

not sure if my mention was a mistake, but here's my .02

My apologies I was trying to mention @ept by his name & have not noticed that github autocompleted with your handle instead.

@Gozala
Copy link
Contributor Author

Gozala commented Aug 6, 2019

IIRC Text is just shorthand for an array of chars. if the text is rich, just use a doc.
i ran out of dev time for it, but an example is here: https://github.com/mattkrick/rich

I don't think it's that simple as there are many cases where intention preservation will suffer, this is discussed in greater detail at #193

see atlaskit by atlassian and prosemirror, which it's built on. if you wanted to build a CRDT plugin on top of prosemirror, it'd be revolutionary, although last time i checked it was too hard-wired for OTs.

I intent to get to prosemirror eventually, but I chose to go with quill in first iteration as it's data model maps CRDTs much better. Problem with ProseMirror is that data model invariants are encoded into a document schema & with CRDTs you can converge to state that invalidates those invariants and there for ProseMirror will reject. This implies there needs to be a mechanism for incorporating changes from other replicas in a way that would comply with a schema.

as for type errors, i'd recommend just PRing the types for this package & using typescript.

My comment was less about type errors and more about type signatures. Currently it's insertAt(number, ...Array<object|string>) which means following type-checks text.insertAt(offset, ["h", "e", "l", "l", "o"]) even though it's not what was intended. That is also why I think having separate data types is good idea.

@ept ept closed this as completed in 2ff0009 Mar 23, 2020
ept added a commit that referenced this issue Mar 23, 2020
Allow non-string children in Text objects

Fixes #194
@ept
Copy link
Member

ept commented Mar 23, 2020

The original issue discussed here (support for objects inside Automerge.Text) is fixed in #238. The other problems around the type signature of the Text class can be dealt with in a separate issue.

philxia added a commit to philxia/automerge that referenced this issue Jun 27, 2020
* test/watchable_doc_test: move callback registration to beforeEach()

* copy automerge.d.ts to dist on build/publish

* use strict mode everywhere

The original code here suggests that in a browser,
modifying a frozen object will fail silently, whereas
in node it will throw an error. This test was failing
for me in browser testing, so I dug into it and it
turns out the difference isn't node vs browser
but strict mode vs not. (In mocha.opts we had
"use strict" so node tests were always running
in strict mode. TypeScript was also compiling in
strict mode.)

Since we have control over whether we're in strict 
mode or not, it makes more sense to just turn it on
everywhere and only test for that scenario.

screw it, just use strict mode everywhere

* test/types: add CounterList

* @types/automerge/index.d.ts: `message` can be undefined

* restore uuid factory, add typings

* @types/automerge/index.d: add uuid()

* @types/automerge/index.d: getElemId

* put all types in automerge namespace

* test/test: restore message type test

* make `Change` generic: `Change<T>`

* test/proxies_test: restore tests with string indices

* Action parameter is different for Ops vs Diffs

* properly type Table.add so it enforces property order when passing array

* npm: fix name of types file

* Automerge.Text no longer considered experimental

* Undo unnecessary formatting changes

* Revert back to plain JS for tests

* Nicer JSON serialization of Counter, Table, and Text

* Improve JSON serialization of Table

Put the rows in a separate object from the columns, so that applications
can iterate over the rows by calling `Object.values(doc.table.rows)`.

* Changelog for automerge#163 and 0.10.1

* 0.10.1

* actorId and elemId are not necessarily UUIDs

* Remove unused type definitions for tests

* setActorId is frontend-only

* Some corrections of the TypeScript types

* Initial set of tests for the TypeScript API

* Fix undo/redo when using separate frontend and backend

Bug: when using Automerge in a configuration with separate frontend and
backend, attempting to perform `Frontend.undo()` would result in the
following exception:

    TypeError: Cannot read property 'updated' of null
      at makeChange (frontend/index.js:113:43)

Fixed the bug and added tests that exercise undo/redo in the case where
frontend and backend are separate.

* Fix some bugs in type definitions

* Exception on modifying frozen object is optional

In Node v12.3.1 and v10.16.0, assigning or deleting a property of a
frozen object no longer throws an exception like it did in previous
versions. Rather than trying to split the test by version number, I
figured it was easier to just remove the assertion that an exception is
thrown (and just check that the object remains unmodified).

* Tests still to do

* Update caveats in readme

* Exception on modifying frozen object is optional

In Node v12.3.1 and v10.16.0, assigning or deleting a property of a
frozen object no longer throws an exception like it did in previous
versions. Rather than trying to split the test by version number, I
figured it was easier to just remove the assertion that an exception is
thrown (and just check that the object remains unmodified).

* Changelog for automerge#165

* Run Travis tests on Node 11 and 12 as well

* README: note correct immutable behavior

* index.d.ts: remove stray question mark

* typescript tests: Automerge.Text

* typescript tests: Automerge.Table

* no need to specify type twice

* expand on usage of types in initialization

* typescript tests: Automerge.Counter

* ignore .vscode

* add `Automerge.from`

Returns a new document object with the given initial state. See automerge#127

* Document created with `from` needs to have a backend

* Update the documentation for Automerge.from

* Changelog for automerge#127

Closes automerge#175

* Fix indentation and whitespace

* Remove unused code

* Add types for Automerge.from (automerge#127)

* WIP: experiment with readonly types returned outside of Automerge

* @types/automerge/index.d: move comment

* aha! Doc<T> = DeepFreezeObject<T>, not DeepFreeze<T>

* tweak `any` tests to work with DeepFreeze

* group types functionally

* upgrade typescript

* rename to Freeze, Proxy

* more rearranging

* simplify Table definition by inheriting from Array<T>

* getConflicts key is `keyof T`

* add documentation for Doc<T> and Proxy<D>

* formatting

* fix handler signature
automerge/automerge#155 (review)

* add tests for DocSet and WatchableDoc

* add DocSet tests

* ignore .vscode

* Revert "Update the documentation for Automerge.from"

This reverts commit c0ba4d2.

* Make object freezing optional

It turns out that in V8, copying frozen objects (especially arrays) is a
lot more expensive than copying non-frozen objects.
https://bugs.chromium.org/p/chromium/issues/detail?id=980227
https://jsperf.com/cloning-frozen-array/1
https://jsperf.com/cloning-frozen-object/1

Removing freezing entirely from skip_list.js since these objects are not
part of the user-facing API, and so we don't need to worry as much about
accidental mutation.

Retaining freezing in a few places in counter.js, table.js,
apply_patch.js and frontend/index.js where it has negligible performance
impact.

* Allow freeze option to be passed in to init() and load()

* Update README: we no longer freeze objects by default

* Slight README refresh

* Our own object-copying function is faster

Surprisingly, Object.assign() is not the fastest way of copying an
object: https://jsperf.com/cloning-large-objects/1

* Remote type parameter from Change interface

It is only needed for the "before" property, and I think that property
should not be considered part of the public API. (It is only used within
the frontend for internal bookkeeping; the Change objects produced and
consumed by the backend do not have this property.)

* Use opaque BackendState type in the backend

* Remove unused type parameter from Message interface

* Update the documentation for Automerge.from

* Changelog and README updates for automerge#155

* TypeScript support for freeze option

* Changelog for automerge#177 and automerge#179

Fixes: automerge#177

* Changelog for v0.11.0

* 0.11.0

* Allow Text instatiation with preset value

* docs: Add introduction to "Applying operations to the local state"

* `DocSet<T>` was missing `docIds` property

* `DocSet.getDoc` return value is `Doc<T>,` not `T`

# Conflicts:
#	@types/automerge/index.d.ts

* `Clock` is a `Map`, not an object

* allow passing `init` options to `from`

* index.d.ts: more precise InitOptions definition; add InitOptions to .from

* add `freeze` to `InitOptions`

* add tests for `from` with options

* refactor: Remove unused `local` list

* docs: Add local state overview

* Types for `Frontend.init()` and `Frontend.from()`

Addendum to automerge#183

* Changelog for automerge#183

* Some refactoring/tidying

Name `instantiateText` is for consistency with `instantiateTable`

* Allow modification of Text without assignment to document

Methods `.set()`, `.insertAt()` and `.deleteAt()` are now defined
directly on Automerge.Text, rather than going through the listProxy.
Therefore, these methods can now be called directly on a Text object
without having to first assign the Text object to the document.

Fixes automerge#180, automerge#166

* Changelog for automerge#180 and automerge#181

* List methods such as .filter() should use proxy objects

Fixes automerge#174

* Upgrade npm dependencies

- Remove `@types/sinon` and `@types/uuid` dependencies, which are unused.
- Update all version numbers in package.json to their latest version,
  except for `babel-loader`, which I'm keeping unchanged for now
  (upgrading it to 8.0.6 breaks `yarn build`, and I can't be bothered to
  figure that out now).
- Run `yarn upgrade` to bring packages up-to-date. The new version of
  `get-caller-file` dropped support for Node 9, and since that version
  of Node is now unsupported anyway, I dropped it from the Travis CI
  test matrix.
- Version 4.0.0-alpha.4 of `selenium-webdriver` requires Node 10 or
  higher. As Node 8 is still supported, I want to keep it in the CI text
  matrix. Thus, I had to add a resolution rule fixing `selenium-webdriver`
  to 4.0.0-alpha.1, which is compatible with Node 8.

* docs: Add description for determining causal readiness

* docs: Add missing operations

* .toString() of Text should return a string content

* test.js: add tests for concurrent deletion

* Fix stack overflow error on large changes

Fixes automerge#202. The stack overflow is not due to unbounded recursion, but
rather because of a method's varargs argument list being too long
(presumably each argument takes up some space on the stack). In
particular, there are several places where we are calling Array.push
with an argument list generated using the spread operator in order to
concatenate arrays. This commit replaces them with for loops, which also
happen to be faster: https://jsperf.com/pushing-large-arrays

* Changelog for 0.12.0

* 0.12.0

* test/typescript_test.ts: docSet.docIds

* Revert "`Clock` is a `Map`, not an object"

This reverts commit d8fd73f.

* Changelog for automerge#174, automerge#184, and automerge#199

* Readme updates

* Fix trap error by setting writable property descriptor to true

* add initialization tests for array, primitives

* fix typo in test name; make describe labels consistent

* readme: automatic formatting

* readme: initializing a document

* readme: updating a document

* readme: rearrange outline

* readme: actorId note

* readme: undo/redo edits

* readme: sending/receiving edits

* readme: conflicting changes edits

* readme: crdt datatypes edits

* readme: emoji

* readme: cross-references

* Tidy up description of network layers

* Text.toString()

* Add DocSet.removeDoc

* Changelog for automerge#210 and 0.12.1

* 0.12.1

* Remove unused parameter in SkipList node

Fixes automerge#213

* a test for round-tripping a control object

* split out tests a bit

* Allow objects to appear as elements in Automerge.Text

Fixes automerge#194

* toSpans() and a test set for it

* add to types

* okay, maybe not any

* quill delta experiment

* show that overlapping spans work

* basic apply-delta function

* account for control characters occupying space during delete & retain

* clean up the op application functions a little bit

* parse embeds from deltadocs

* make it possible to distinguish between attributes and embed control characters

* finish support for embeds

* explicitly account for control characters vs embeds

* Fix freezing of opaque string types

* Include link to repo

* docs(readme): add perge library to list under sending/receiving changes

* Update README.md

* Allow options object to be passed in to Automerge.change

Similarly in emptyChange(), undo(), and redo()

* canUndo should return false after Automerge.from()

* Bump handlebars from 4.1.2 to 4.5.3

Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](handlebars-lang/handlebars.js@v4.1.2...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

* fix deleteAt bug with input 0

* New `Automerge.getAllChanges()` API

* Bring internals documentation up-to-date

* Document frontend-backend protocol

* Bring the rest of the internals doc up-to-date

* Reword intro

* Fix indentation

* Changelog for v0.13.0

* 0.13.0

* Update copyright

* Add link to automerge-client-server

* Rephrase caveats

* remove tests for adding a Table row as an array of values

* Remove KeyOrder array from Table type

* remove KeyOrder array from types in tests

* clean up documentation

* remove undocumented ability to add table row from array of values

* remove test for API being removed

* Bump acorn from 6.2.1 to 6.4.1

Bumps [acorn](https://github.com/acornjs/acorn) from 6.2.1 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](acornjs/acorn@6.2.1...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>

* support more nesting & fix retain logic

* use string concat instead of join()

* Update dependencies (yarn upgrade)

* Updating dependencies

* Throw exception if people try to use the old API

* Remove obsolete Node 8 and 11 from testing matrix

* Changelog for automerge#236

* Get Sauce Labs tests working again

* Update Sauce Labs build badge

* Remove the list of columns from Automerge.Table

Since automerge#236 the Automerge.Table type no longer allows rows to be added as
an array of values, which are then mapped to column names. Now that this
feature is removed, nothing in Automerge is actually using the list of
columns that is currently required by the Automerge.Table constructor.
The columns are still saved, but since they do not enforce any schema,
they are purely advisory.

Hopefully one day we can have proper schema support in Automerge, but
the current half-hearted implementation is not really helping us get
there. Therefore I think it is best to just remove this list of columns
feature.

* Update Mocha configuration file format

mocha.opts is deprecated. This stops the annoying deprecation warning
when running the tests.

* Mocha should load test files

* Remove trailing whitespace

* Changelog for automerge#238

* Make table row's primary key available as `id` property

Currently, if you have a row object from an Automerge.Table, you can get
its primary key with `Automerge.getObjectId(row)`. This API is not very
discoverable; users who don't know about this API might be tempted to
generate their own IDs for table rows, which would miss the entire point
of Automerge.Table.

This patch makes the primary key more visible by making it available as
`row.id` instead. When a new row is added, we check that the row object
doesn't already have an `id` property. We also ensure that the `id`
property cannot be modified.

(Besides API usability, there is also a deeper reason for making this
change: on the `performance` branch, objectIds are backend-generated
Lamport timestamps rather than UUIDs to enable better compression; since
`Table.add` should synchronously return the primary key of the new row,
it must use a different ID from the objectId. Putting the row's primary
key in a separate property reinforces that distinction.)

* Fix description of Table in README

* Changelog for automerge#241 and automerge#242

* Remove set() method from Automerge.Table API

It is not needed, since add() and remove() handle changes to the set of
rows, and properties of row objects can be updated directly without
requiring any set() operation at the table level.

Looking at the code, I now realise that set() was not intended to be
part of the public API at all: it is called while applying a patch, and
if a user calls it, it does not generate any operations in the change
context. Hence I renamed it to start with an underscore (like `_clone()`
and `_freeze()`), and added a warning to the comment.

* Changelog for automerge#243

* Changelog for 0.14.0

* 0.14.0

* Link to new CRDT website

* Update table class type to reflect property getter

* Changelog and test for automerge#249

* Fix console inspection of proxy objects in Node

* Fix type signatures for WatchableDoc#get and WatchableDoc#set

* Use Slack's own invitation link instead of communityinviter

* Queued changes (whose dependencies are missing) should also be saved

Previously, load(save()) would discard any queued changes.

Bug reported by @KarenSarmiento, fixes automerge#258

* Make clearer that fine-grained updates are preferred

Add a README section as suggested by @johannesjo in automerge#260, and make a
more descriptive exception when users try to assign an object that is
already in the document.

Fixes automerge#260

* Changelog for 0.14.1

* 0.14.1

* Link to automerge#253 from README

* support calling indexOf with an object

* logo assets

* replace h1 with logo

* README.md: smaller logo

* images with wider spacing

* revert unintended reformatting

* Update name of main branch

fixes automerge#264

Co-authored-by: Herb Caudill <herb@devresults.com>
Co-authored-by: Martin Kleppmann <martin@kleppmann.com>
Co-authored-by: Herb Caudill <herb@caudillweb.com>
Co-authored-by: Harry Brundage <harry.brundage@gmail.com>
Co-authored-by: Irakli Gozalishvili <contact@gozala.io>
Co-authored-by: Eric Dahlseng <edahlseng@yahoo.com>
Co-authored-by: Peter van Hardenberg <pvh@pvh.ca>
Co-authored-by: Mikey Stengel <mikey.stengel96@gmail.com>
Co-authored-by: Brent Keller <brentkeller@gmail.com>
Co-authored-by: Jeff Peterson <jeff@yak.sh>
Co-authored-by: Max Gfeller <max.gfeller@gmail.com>
Co-authored-by: Sam McCord <sam.mccord@protonmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Speros Kokenes <speros@speross-mbp.lan>
Co-authored-by: Jeremy Apthorp <nornagon@nornagon.net>
Co-authored-by: Lauritz Hilsøe <mail+gh@lauritz.me>
Co-authored-by: vincent.capicotto <vincent.capicotto@hiptest.net>
Co-authored-by: Phil Schatz <253202+philschatz@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants