From 77e479c03b589544d5675881fac7d4be9c405646 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 9 Jan 2019 23:54:36 +0100 Subject: [PATCH] working on snapshotting and version history --- bindings/prosemirror.js | 85 +++++++++---- examples/prosemirror-history.js | 68 +++++++++++ examples/prosemirror.js | 3 +- index.js | 3 + package-lock.json | 191 ++++++++++++----------------- package.json | 3 +- protocols/history.js | 33 +++++ protocols/sync.js | 208 ++------------------------------ structs/ItemBinary.js | 48 ++++++++ types/YArray.js | 51 ++++++-- types/YMap.js | 63 +++++++--- types/YText.js | 6 +- types/YXmlElement.js | 20 ++- utils/DeleteStore.js | 170 +++++++++++++++++++++++++- utils/StateStore.js | 57 ++++++++- utils/Y.js | 9 +- utils/snapshot.js | 8 ++ 17 files changed, 648 insertions(+), 378 deletions(-) create mode 100644 examples/prosemirror-history.js create mode 100644 protocols/history.js create mode 100644 structs/ItemBinary.js create mode 100644 utils/snapshot.js diff --git a/bindings/prosemirror.js b/bindings/prosemirror.js index bc8c57e8..984a7dcc 100644 --- a/bindings/prosemirror.js +++ b/bindings/prosemirror.js @@ -31,35 +31,55 @@ export const prosemirrorPluginKey = new PluginKey('yjs') * @return {Plugin} Returns a prosemirror plugin that binds to this type */ export const prosemirrorPlugin = yXmlFragment => { - const pluginState = { - type: yXmlFragment, - y: yXmlFragment._y, - binding: null - } let changedInitialContent = false const plugin = new Plugin({ key: prosemirrorPluginKey, state: { init: (initargs, state) => { - return pluginState + return { + type: yXmlFragment, + y: yXmlFragment._y, + binding: null, + snapshot: null + } }, apply: (tr, pluginState) => { - // update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin - if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) { - changedInitialContent = true - pluginState.binding._prosemirrorChanged(tr.doc) + const change = tr.getMeta(prosemirrorPluginKey) + if (change !== undefined) { + pluginState = Object.assign({}, pluginState) + for (let key in change) { + pluginState[key] = change[key] + } + } + if (pluginState.binding !== null) { + if (change !== undefined && change.snapshot !== undefined) { + // snapshot changed, rerender next + setTimeout(() => { + pluginState.binding._renderSnapshot(change.snapshot) + }, 0) + } else if (pluginState.snapshot == null) { + // only apply if no snapshot active + // update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin + if (changedInitialContent || tr.doc.content.size > 4) { + changedInitialContent = true + pluginState.binding._prosemirrorChanged(tr.doc) + } + } } return pluginState } }, view: view => { const binding = new ProsemirrorBinding(yXmlFragment, view) - pluginState.binding = binding + view.dispatch(view.state.tr.setMeta(prosemirrorPluginKey, { binding })) return { update: () => { - if (changedInitialContent || view.state.doc.content.size > 4) { - changedInitialContent = true - binding._prosemirrorChanged(view.state.doc) + const pluginState = plugin.getState(view.state) + if (pluginState.snapshot == null) { + if (changedInitialContent || view.state.doc.content.size > 4) { + changedInitialContent = true + binding._prosemirrorChanged(view.state.doc) + } } }, destroy: () => { @@ -301,8 +321,18 @@ export class ProsemirrorBinding { }) yXmlFragment.observeDeep(this._observeFunction) } + _renderSnapshot (snapshot) { + // clear mapping because we are going to rerender + this.mapping = new Map() + this.mux(() => { + const fragmentContent = this.type.toArray(snapshot).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot)).filter(n => n !== null) + const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0)) + this.prosemirrorView.dispatch(tr) + }) + } _typeChanged (events, transaction) { - if (events.length === 0) { + if (events.length === 0 || prosemirrorPluginKey.getState(this.prosemirrorView.state).snapshot != null) { + // drop out if snapshot is active return } console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs) @@ -321,7 +351,7 @@ export class ProsemirrorBinding { tr.setSelection(TextSelection.create(tr.doc, anchor, head)) } } - this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr)) + this.prosemirrorView.dispatch(tr) }) } _prosemirrorChanged (doc) { @@ -335,16 +365,17 @@ export class ProsemirrorBinding { } /** - * @privateMapping + * @private * @param {YXmlElement} el * @param {PModel.Schema} schema * @param {ProsemirrorMapping} mapping + * @param {HistorySnapshot} [snapshot] * @return {PModel.Node} */ -export const createNodeIfNotExists = (el, schema, mapping) => { +export const createNodeIfNotExists = (el, schema, mapping, snapshot) => { const node = mapping.get(el) if (node === undefined) { - return createNodeFromYElement(el, schema, mapping) + return createNodeFromYElement(el, schema, mapping, snapshot) } return node } @@ -354,18 +385,19 @@ export const createNodeIfNotExists = (el, schema, mapping) => { * @param {YXmlElement} el * @param {PModel.Schema} schema * @param {ProsemirrorMapping} mapping + * @param {import('../protocols/history.js').HistorySnapshot} snapshot * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null */ -export const createNodeFromYElement = (el, schema, mapping) => { +export const createNodeFromYElement = (el, schema, mapping, snapshot) => { const children = [] - el.toArray().forEach(type => { + el.toArray(snapshot).forEach(type => { if (type.constructor === YXmlElement) { - const n = createNodeIfNotExists(type, schema, mapping) + const n = createNodeIfNotExists(type, schema, mapping, snapshot) if (n !== null) { children.push(n) } } else { - const ns = createTextNodesFromYText(type, schema, mapping) + const ns = createTextNodesFromYText(type, schema, mapping, snapshot) if (ns !== null) { ns.forEach(textchild => { if (textchild !== null) { @@ -377,7 +409,7 @@ export const createNodeFromYElement = (el, schema, mapping) => { }) let node try { - node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children) + node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(snapshot), children) } catch (e) { // an error occured while creating the node. This is probably a result because of a concurrent action. // delete the node and do not push to children @@ -395,11 +427,12 @@ export const createNodeFromYElement = (el, schema, mapping) => { * @param {YText} text * @param {PModel.Schema} schema * @param {ProsemirrorMapping} mapping + * @param {HistorySnapshot} [snapshot] * @return {Array} */ -export const createTextNodesFromYText = (text, schema, mapping) => { +export const createTextNodesFromYText = (text, schema, mapping, snapshot) => { const nodes = [] - const deltas = text.toDelta() + const deltas = text.toDelta(snapshot) try { for (let i = 0; i < deltas.length; i++) { const delta = deltas[i] diff --git a/examples/prosemirror-history.js b/examples/prosemirror-history.js new file mode 100644 index 00000000..c3da82a9 --- /dev/null +++ b/examples/prosemirror-history.js @@ -0,0 +1,68 @@ + +import {Plugin} from "prosemirror-state" +import crel from 'crel' +import * as Y from '../index.js' +import { prosemirrorPluginKey } from '../bindings/prosemirror.js' +import * as encoding from '../lib/encoding.js' +import * as decoding from '../lib/decoding.js' +import * as historyProtocol from '../protocols/history.js' + +export const noteHistoryPlugin = new Plugin({ + view (editorView) { return new NoteHistoryPlugin(editorView) } +}) + +const createWrapper = () => { + const wrapper = crel('div', { style: 'display: flex' }) + const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' }) + wrapper.insertBefore(historyContainer, null) + return { wrapper, historyContainer } +} + +class NoteHistoryPlugin { + constructor(editorView) { + this.editorView = editorView + const { historyContainer, wrapper } = createWrapper() + this.wrapper = wrapper + this.historyContainer = historyContainer + const n = editorView.dom.parentNode.parentNode + n.parentNode.replaceChild(this.wrapper, n) + n.style['flex-grow'] = '1' + wrapper.insertBefore(n, this.wrapper.firstChild) + this.render() + const y = prosemirrorPluginKey.getState(this.editorView.state).y + const history = y.define('history', Y.Array) + history.observe(this.render.bind(this)) + } + render () { + const y = prosemirrorPluginKey.getState(this.editorView.state).y + const history = y.define('history', Y.Array).toArray() + const fragment = document.createDocumentFragment() + const snapshotBtn = crel('button', { type: 'button' }, ['snapshot']) + snapshotBtn.addEventListener('click', this.snapshot.bind(this)) + fragment.insertBefore(snapshotBtn, null) + history.forEach(buf => { + const decoder = decoding.createDecoder(buf) + const snapshot = historyProtocol.readHistorySnapshot(decoder) + const date = new Date(decoding.readUint32(decoder) * 1000) + const a = crel('a', [ + '• '+ date.toUTCString() + ]) + const el = crel('div', [ a ]) + a.addEventListener('click', () => { + console.log('setting snapshot') + this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot })) + }) + fragment.insertBefore(el, null) + }) + this.historyContainer.innerHTML = '' + this.historyContainer.insertBefore(fragment, null) + } + snapshot () { + const y = prosemirrorPluginKey.getState(this.editorView.state).y + const history = y.define('history', Y.Array) + const encoder = encoding.createEncoder() + historyProtocol.writeHistorySnapshot(encoder, y) + encoding.writeUint32(encoder, Math.floor(Date.now() / 1000)) + history.push([encoding.toBuffer(encoder)]) + } +} diff --git a/examples/prosemirror.js b/examples/prosemirror.js index d97eebfc..f546b4a4 100644 --- a/examples/prosemirror.js +++ b/examples/prosemirror.js @@ -9,6 +9,7 @@ import { EditorView } from 'prosemirror-view' import { DOMParser } from 'prosemirror-model' import { schema } from 'prosemirror-schema-basic' import { exampleSetup } from 'prosemirror-example-setup' +import { noteHistoryPlugin } from './prosemirror-history.js' const provider = new WebsocketProvider(conf.serverAddress) const ydocument = provider.get('prosemirror') @@ -17,7 +18,7 @@ const type = ydocument.define('prosemirror', Y.XmlFragment) const prosemirrorView = new EditorView(document.querySelector('#editor'), { state: EditorState.create({ doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')), - plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin]) + plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin, noteHistoryPlugin]) }) }) diff --git a/index.js b/index.js index 78375df2..09d56b2b 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ import { ItemJSON } from './structs/ItemJSON.js' import { ItemString } from './structs/ItemString.js' import { ItemFormat } from './structs/ItemFormat.js' import { ItemEmbed } from './structs/ItemEmbed.js' +import { ItemBinary } from './structs/ItemBinary.js' import { GC } from './structs/GC.js' import { YArray } from './types/YArray.js' @@ -53,3 +54,5 @@ registerStruct(9, YXmlElement) registerStruct(10, YXmlText) registerStruct(11, YXmlHook) registerStruct(12, ItemEmbed) +registerStruct(13, ItemBinary) + diff --git a/package-lock.json b/package-lock.json index e28ca4fe..9e8bd726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-5.0.0.tgz", "integrity": "sha512-5mU5P1gXtsMIXg65/rsYGsi93+MlogXZ9FA8JnwKurHQg64bfXwGYVdVdijNTVNOlAsuIiOwHdvFFD5JqCJQ7A==", - "dev": true, "requires": { "xtend": "~4.0.0" } @@ -201,8 +200,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", @@ -239,13 +237,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "optional": true }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -1669,13 +1667,13 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", - "dev": true + "optional": true }, "bl": { "version": "1.2.2", "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "dev": true, + "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -1685,13 +1683,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1706,7 +1704,7 @@ "version": "1.1.1", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -1750,7 +1748,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, + "optional": true, "requires": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -1760,13 +1758,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "optional": true }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true + "optional": true }, "buffer-from": { "version": "1.0.0", @@ -1893,7 +1891,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true + "optional": true }, "circular-json": { "version": "0.3.3", @@ -1932,12 +1930,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "optional": true }, "codemirror": { "version": "5.42.0", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.42.0.tgz", - "integrity": "sha512-pbApC8zDzItP3HRphD6kQVwS976qB5Qi0hU3MZMixLk+AyugOW1RF+8XJEjeyl5yWsHNe88tDUxzeRh5AOxPRw==" + "integrity": "sha512-pbApC8zDzItP3HRphD6kQVwS976qB5Qi0hU3MZMixLk+AyugOW1RF+8XJEjeyl5yWsHNe88tDUxzeRh5AOxPRw==", + "dev": true }, "color-convert": { "version": "1.9.1", @@ -2146,8 +2145,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "contains-path": { "version": "0.1.0", @@ -2170,14 +2168,12 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "crel": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz", - "integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==", - "dev": true + "integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==" }, "cross-spawn": { "version": "5.1.0", @@ -2285,7 +2281,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, + "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -2300,7 +2296,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "optional": true }, "deep-is": { "version": "0.1.3", @@ -2312,7 +2308,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-4.0.2.tgz", "integrity": "sha512-5fMC8ek8alH16QiV0lTCis610D1Zt1+LA4MS4d63JgS32lrCjTFDUFz2ao09/j2I4Bqb5jL4FZYwu7Jz0XO1ww==", - "dev": true, + "optional": true, "requires": { "abstract-leveldown": "~5.0.0", "inherits": "^2.0.3" @@ -2367,7 +2363,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "optional": true }, "depd": { "version": "1.1.2", @@ -2394,7 +2390,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true + "optional": true }, "doctrine": { "version": "2.1.0", @@ -2490,7 +2486,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-5.0.4.tgz", "integrity": "sha512-8CIZLDcSKxgzT+zX8ZVfgNbu8Md2wq/iqa1Y7zyVR18QBEAc0Nmzuvj/N5ykSKpfGzjM8qxbaFntLPwnVoUhZw==", - "dev": true, + "optional": true, "requires": { "abstract-leveldown": "^5.0.0", "inherits": "^2.0.3", @@ -2503,7 +2499,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -2518,7 +2513,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, "requires": { "prr": "~1.0.1" } @@ -3302,7 +3296,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz", "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg==", - "dev": true + "optional": true }, "expand-tilde": { "version": "1.2.2", @@ -3370,7 +3364,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/fast-future/-/fast-future-1.0.2.tgz", "integrity": "sha1-hDWpqqAteSSNF9cE52JZMB2ZKAo=", - "dev": true + "optional": true }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -3562,7 +3556,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "optional": true }, "fs-exists-sync": { "version": "0.1.0", @@ -4138,7 +4132,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4154,7 +4148,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4163,7 +4157,7 @@ "version": "1.0.2", "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4192,7 +4186,7 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "dev": true + "optional": true }, "glob": { "version": "7.1.2", @@ -4336,7 +4330,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "optional": true }, "home-or-tmp": { "version": "2.0.0", @@ -4550,14 +4544,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "version": "3.3.0", @@ -4723,8 +4715,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "2.0.1", @@ -4853,8 +4844,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -5118,7 +5108,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/level/-/level-4.0.0.tgz", "integrity": "sha512-4epzCOlEcJ529NOdlAYiuiakS/kZTDdiKSBNJmE1B8bsmA+zEVwcpxyH86qJSQTpOu7SODrlaD9WgPRHLkGutA==", - "dev": true, + "optional": true, "requires": { "level-packager": "^3.0.0", "leveldown": "^4.0.0", @@ -5129,13 +5119,12 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.0.tgz", "integrity": "sha512-OIpVvjCcZNP5SdhcNupnsI1zo5Y9Vpm+k/F1gfG5kXrtctlrwanisakweJtE0uA0OpLukRfOQae+Fg0M5Debhg==", - "dev": true + "optional": true }, "level-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.0.tgz", "integrity": "sha512-AmY4HCp9h3OiU19uG+3YWkdELgy05OTP/r23aNHaQKWv8DO787yZgsEuGVkoph40uwN+YdUKnANlrxSsoOaaxg==", - "dev": true, "requires": { "errno": "~0.1.1" } @@ -5144,7 +5133,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-3.0.1.tgz", "integrity": "sha512-nEIQvxEED9yRThxvOrq8Aqziy4EGzrxSZK+QzEFAVuJvQ8glfyZ96GB6BoI4sBbLfjMXm2w4vu3Tkcm9obcY0g==", - "dev": true, + "optional": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.3.6", @@ -5155,13 +5144,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5176,7 +5165,7 @@ "version": "1.1.1", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -5187,7 +5176,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-3.1.0.tgz", "integrity": "sha512-UxVEfK5WH0u0InR3WxTCSAroiorAGKzXWZT6i+nBjambmvINuXFUsFx2Ai3UIjUUtnyWhluv42jMlzUZCsAk9A==", - "dev": true, + "optional": true, "requires": { "encoding-down": "~5.0.0", "levelup": "^3.0.0" @@ -5197,7 +5186,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-4.0.1.tgz", "integrity": "sha512-ZlBKVSsglPIPJnz4ggB8o2R0bxDxbsMzuQohbfgoFMVApyTE118DK5LNRG0cRju6rt3OkGxe0V6UYACGlq/byg==", - "dev": true, + "optional": true, "requires": { "abstract-leveldown": "~5.0.0", "bindings": "~1.3.0", @@ -5210,7 +5199,7 @@ "version": "2.10.0", "resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true + "optional": true } } }, @@ -5218,7 +5207,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/levelup/-/levelup-3.1.1.tgz", "integrity": "sha512-9N10xRkUU4dShSRRFTBdNaBxofz+PGaIZO962ckboJZiNmLuhVT6FZ6ZKAsICKfUBO76ySaYU6fJWX/jnj3Lcg==", - "dev": true, + "optional": true, "requires": { "deferred-leveldown": "~4.0.0", "level-errors": "~2.0.0", @@ -5647,7 +5636,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "optional": true }, "minimatch": { "version": "3.0.4", @@ -5661,14 +5650,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -5721,7 +5708,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.0.tgz", "integrity": "sha512-9g2twBGSP6wIR5PW7tXvAWnEWKJDH/VskdXp168xsw9VVxpEGov8K4jsP4/VeoC7b2ZAyzckvMCuQuQlw44lXg==", - "dev": true, + "optional": true, "requires": { "semver": "^5.4.1" }, @@ -5730,7 +5717,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true + "optional": true } } }, @@ -5748,7 +5735,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", - "dev": true + "optional": true }, "normalize-package-data": { "version": "2.4.0", @@ -5775,7 +5762,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -5795,8 +5782,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nwmatcher": { "version": "1.4.3", @@ -5815,8 +5801,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-keys": { "version": "1.0.11", @@ -5853,7 +5838,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -5871,7 +5855,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.1.tgz", "integrity": "sha512-saQQ9hjLwu/oS0492eyYotoh+bra1819cfAT5rjY/e4REWwuc8IgZ844Oo44SiftWcJuBiqp0SA0BFVbmLX0IQ==", - "dev": true + "optional": true }, "optionator": { "version": "0.8.2", @@ -5904,8 +5888,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", @@ -6148,7 +6131,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-4.0.0.tgz", "integrity": "sha512-7tayxeYboJX0RbVzdnKyGl2vhQRWr6qfClEXDhOkXjuaOKCw2q8aiuFhONRYVsG/czia7KhpykIlI2S2VaPunA==", - "dev": true, + "optional": true, "requires": { "detect-libc": "^1.0.3", "expand-template": "^1.0.2", @@ -6171,7 +6154,7 @@ "version": "1.2.0", "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "optional": true } } }, @@ -6196,8 +6179,7 @@ "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "progress": { "version": "2.0.0", @@ -6380,8 +6362,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", @@ -6400,7 +6381,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6495,7 +6476,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -6507,7 +6488,7 @@ "version": "1.2.0", "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "optional": true } } }, @@ -6560,7 +6541,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7094,8 +7074,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", @@ -7164,7 +7143,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "optional": true }, "set-getter": { "version": "0.1.0", @@ -7211,20 +7190,19 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simple-concat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", - "dev": true + "optional": true }, "simple-get": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", - "dev": true, + "optional": true, "requires": { "decompress-response": "^3.3.0", "once": "^1.3.1", @@ -7419,7 +7397,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -7428,14 +7405,12 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -7446,7 +7421,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -7455,7 +7429,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7486,8 +7459,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { "version": "2.0.0", @@ -7563,7 +7535,7 @@ "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "dev": true, + "optional": true, "requires": { "chownr": "^1.0.1", "mkdirp": "^0.5.1", @@ -7575,7 +7547,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7587,7 +7559,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dev": true, + "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -7629,7 +7601,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "optional": true }, "to-fast-properties": { "version": "1.0.3", @@ -7694,7 +7666,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -7813,8 +7785,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.0", @@ -7928,13 +7899,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "dev": true + "optional": true }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, + "optional": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -7942,8 +7913,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", @@ -7979,8 +7949,7 @@ "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index 2423821f..96336688 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "rollup-watch": "^3.2.2", "standard": "^11.0.1", "tui-jsdoc-template": "^1.2.2", - "codemirror": "^5.42.0" + "codemirror": "^5.42.0", + "crel": "^3.1.0" }, "optionalDependencies": { "level": "^4.0.0", diff --git a/protocols/history.js b/protocols/history.js new file mode 100644 index 00000000..c74e8531 --- /dev/null +++ b/protocols/history.js @@ -0,0 +1,33 @@ + + +import * as encoding from '../lib/encoding.js' +import * as decoding from '../lib/decoding.js' +import { Y } from '../utils/Y.js' // eslint-disable-line +import { writeDeleteStore, readFreshDeleteStore, DeleteStore } from '../utils/DeleteStore.js' // eslint-disable-line +import { writeStateMap, readStateMap } from '../utils/StateStore.js' + +/** + * @typedef {Object} HistorySnapshot + * @property {DeleteStore} HistorySnapshot.ds + * @property {Map} HistorySnapshot.sm + */ + +/** + * @param {encoding.Encoder} encoder + * @param {Y} y + */ +export const writeHistorySnapshot = (encoder, y) => { + writeDeleteStore(encoder, y.ds) + writeStateMap(encoder, y.ss.state) +} + +/** + * + * @param {decoding.Decoder} decoder + * @return {HistorySnapshot} + */ +export const readHistorySnapshot = (decoder) => { + const ds = readFreshDeleteStore(decoder) + const sm = readStateMap(decoder) + return { ds, sm } +} \ No newline at end of file diff --git a/protocols/sync.js b/protocols/sync.js index 42d2abe1..5600555e 100644 --- a/protocols/sync.js +++ b/protocols/sync.js @@ -10,9 +10,11 @@ import { deleteItemRange } from '../utils/structManipulation.js' import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js' import { Y } from '../utils/Y.js' // eslint-disable-line import * as stringify from '../utils/structStringify.js' +import { readStateMap, writeStateMap } from '../utils/StateStore.js' +import { writeDeleteStore, readDeleteStore, stringifyDeleteStore } from '../utils/DeleteStore.js' /** - * @typedef {Map} StateSet + * @typedef {Map} StateMap */ /** @@ -44,196 +46,6 @@ export const messageYjsSyncStep1 = 0 export const messageYjsSyncStep2 = 1 export const messageYjsUpdate = 2 -/** - * Stringifies a message-encoded Delete Set. - * - * @param {decoding.Decoder} decoder - * @return {string} - */ -export const stringifyDeleteSet = (decoder) => { - let str = '' - const dsLength = decoding.readUint32(decoder) - for (let i = 0; i < dsLength; i++) { - str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user - const dvLength = decoding.readUint32(decoder) - for (let j = 0; j < dvLength; j++) { - str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n` - } - } - return str -} - -/** - * Write the DeleteSet of a shared document to an Encoder. - * - * @param {encoding.Encoder} encoder - * @param {Y} y - */ -export const writeDeleteSet = (encoder, y) => { - let currentUser = null - let currentLength - let lastLenPos - let numberOfUsers = 0 - const laterDSLenPus = encoding.length(encoder) - encoding.writeUint32(encoder, 0) - y.ds.iterate(null, null, n => { - const user = n._id.user - const clock = n._id.clock - const len = n.len - const gc = n.gc - if (currentUser !== user) { - numberOfUsers++ - // a new user was foundimport { StateSet } from '../Store/StateStore.js' // eslint-disable-line - - if (currentUser !== null) { // happens on first iteration - encoding.setUint32(encoder, lastLenPos, currentLength) - } - currentUser = user - encoding.writeVarUint(encoder, user) - // pseudo-fill pos - lastLenPos = encoding.length(encoder) - encoding.writeUint32(encoder, 0) - currentLength = 0 - } - encoding.writeVarUint(encoder, clock) - encoding.writeVarUint(encoder, len) - encoding.writeUint8(encoder, gc ? 1 : 0) - currentLength++ - }) - if (currentUser !== null) { // happens on first iteration - encoding.setUint32(encoder, lastLenPos, currentLength) - } - encoding.setUint32(encoder, laterDSLenPus, numberOfUsers) -} - -/** - * Read delete set from Decoder and apply it to a shared document. - * - * @param {decoding.Decoder} decoder - * @param {Y} y - */ -export const readDeleteSet = (decoder, y) => { - const dsLength = decoding.readUint32(decoder) - for (let i = 0; i < dsLength; i++) { - const user = decoding.readVarUint(decoder) - const dv = [] - const dvLength = decoding.readUint32(decoder) - for (let j = 0; j < dvLength; j++) { - const from = decoding.readVarUint(decoder) - const len = decoding.readVarUint(decoder) - const gc = decoding.readUint8(decoder) === 1 - dv.push({from, len, gc}) - } - if (dvLength > 0) { - const deletions = [] - let pos = 0 - let d = dv[pos] - y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => { - // cases: - // 1. d deletes something to the right of n - // => go to next n (break) - // 2. d deletes something to the left of n - // => create deletions - // => reset d accordingly - // *)=> if d doesn't delete anything anymore, go to next d (continue) - // 3. not 2) and d deletes something that also n deletes - // => reset d so that it doesn't contain n's deletion - // *)=> if d does not delete anything anymore, go to next d (continue) - while (d != null) { - var diff = 0 // describe the diff of length in 1) and 2) - if (n._id.clock + n.len <= d.from) { - // 1) - break - } else if (d.from < n._id.clock) { - // 2) - // delete maximum the len of d - // else delete as much as possible - diff = Math.min(n._id.clock - d.from, d.len) - // deleteItemRange(y, user, d.from, diff, true) - deletions.push([user, d.from, diff]) - } else { - // 3) - diff = n._id.clock + n.len - d.from // never null (see 1) - if (d.gc && !n.gc) { - // d marks as gc'd but n does not - // then delete either way - // deleteItemRange(y, user, d.from, Math.min(diff, d.len), true) - deletions.push([user, d.from, Math.min(diff, d.len)]) - } - } - if (d.len <= diff) { - // d doesn't delete anything anymore - d = dv[++pos] - } else { - d.from = d.from + diff // reset pos - d.len = d.len - diff // reset length - } - } - }) - // TODO: It would be more performant to apply the deletes in the above loop - // Adapt the Tree implementation to support delete while iterating - for (let i = deletions.length - 1; i >= 0; i--) { - const del = deletions[i] - deleteItemRange(y, del[0], del[1], del[2], true) - } - // for the rest.. just apply it - for (; pos < dv.length; pos++) { - d = dv[pos] - deleteItemRange(y, user, d.from, d.len, true) - // deletions.push([user, d.from, d.len, d.gc) - } - } - } -} - -/** - * Read a StateSet from Decoder and return it as string. - * - * @param {decoding.Decoder} decoder - * @return {string} - */ -export const stringifyStateSet = decoder => { - let s = 'State Set: ' - readStateSet(decoder).forEach((clock, user) => { - s += `(${user}: ${clock}), ` - }) - return s -} - -/** - * Write StateSet to Encoder - * - * @param {encoding.Encoder} encoder - * @param {Y} y - */ -export const writeStateSet = (encoder, y) => { - const state = y.ss.state - // write as fixed-size number to stay consistent with the other encode functions. - // => anytime we write the number of objects that follow, encode as fixed-size number. - encoding.writeUint32(encoder, state.size) - state.forEach((clock, user) => { - encoding.writeVarUint(encoder, user) - encoding.writeVarUint(encoder, clock) - }) -} - -/** - * Read StateSet from Decoder and return as Map - * - * @param {decoding.Decoder} decoder - * @return {StateSet} - */ -export const readStateSet = decoder => { - const ss = new Map() - const ssLength = decoding.readUint32(decoder) - for (let i = 0; i < ssLength; i++) { - const user = decoding.readVarUint(decoder) - const clock = decoding.readVarUint(decoder) - ss.set(user, clock) - } - return ss -} - /** * @param {decoding.Decoder} decoder * @param {Y} y @@ -262,7 +74,7 @@ export const stringifyStructs = (decoder, y) => { * * @param {encoding.Encoder} encoder * @param {Y} y - * @param {StateSet} ss State Set received from a remote client. Maps from client id to number of created operations by client id. + * @param {StateMap} ss State Set received from a remote client. Maps from client id to number of created operations by client id. */ export const writeStructs = (encoder, y, ss) => { const lenPos = encoding.length(encoder) @@ -328,7 +140,7 @@ export const stringifySyncStep1 = (decoder) => { */ export const writeSyncStep1 = (encoder, y) => { encoding.writeVarUint(encoder, messageYjsSyncStep1) - writeStateSet(encoder, y) + writeStateMap(encoder, y.ss.state) } /** @@ -339,7 +151,7 @@ export const writeSyncStep1 = (encoder, y) => { export const writeSyncStep2 = (encoder, y, ss) => { encoding.writeVarUint(encoder, messageYjsSyncStep2) writeStructs(encoder, y, ss) - writeDeleteSet(encoder, y) + writeDeleteStore(encoder, y.ds) } /** @@ -350,7 +162,7 @@ export const writeSyncStep2 = (encoder, y, ss) => { * @param {Y} y */ export const readSyncStep1 = (decoder, encoder, y) => - writeSyncStep2(encoder, y, readStateSet(decoder)) + writeSyncStep2(encoder, y, readStateMap(decoder)) /** * @param {decoding.Decoder} decoder @@ -363,19 +175,19 @@ export const stringifySyncStep2 = (decoder, y) => { str += stringifyStructs(decoder, y) // write DS to string str += ' + Delete Set:\n' - str += stringifyDeleteSet(decoder) + str += stringifyDeleteStore(decoder) return str } /** - * Read and apply Structs and then DeleteSet to a y instance. + * Read and apply Structs and then DeleteStore to a y instance. * * @param {decoding.Decoder} decoder * @param {Y} y */ export const readSyncStep2 = (decoder, y) => { readStructs(decoder, y) - readDeleteSet(decoder, y) + readDeleteStore(decoder, y) } /** diff --git a/structs/ItemBinary.js b/structs/ItemBinary.js new file mode 100644 index 00000000..b3347a59 --- /dev/null +++ b/structs/ItemBinary.js @@ -0,0 +1,48 @@ +/** + * @module structs + */ + +// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency + +import { Item, splitHelper } from './Item.js' +import * as stringify from '../utils/structStringify.js' +import * as encoding from '../lib/encoding.js' +import * as decoding from '../lib/decoding.js' +import { Y } from '../utils/Y.js' // eslint-disable-line + +export class ItemBinary extends Item { + constructor () { + super() + this._content = null + } + _copy () { + let struct = super._copy() + struct._content = this._content + return struct + } + /** + * @param {Y} y + * @param {decoding.Decoder} decoder + */ + _fromBinary (y, decoder) { + const missing = super._fromBinary(y, decoder) + this._content = decoding.readPayload(decoder) + return missing + } + /** + * @param {encoding.Encoder} encoder + */ + _toBinary (encoder) { + super._toBinary(encoder) + encoding.writePayload(encoder, this._content) + } + /** + * Transform this YXml Type to a readable format. + * Useful for logging as all Items and Delete implement this method. + * + * @private + */ + _logString () { + return stringify.logItemHelper('ItemBinary', this) + } +} diff --git a/types/YArray.js b/types/YArray.js index 82dc2a3d..48b63a27 100644 --- a/types/YArray.js +++ b/types/YArray.js @@ -9,6 +9,8 @@ import * as stringify from '../utils/structStringify.js' import { YEvent } from '../utils/YEvent.js' import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import { Item } from '../structs/Item.js' // eslint-disable-line +import { ItemBinary } from '../structs/ItemBinary.js' +import { isVisible } from '../utils/snapshot.js' /** * Event that describes the changes on a YArray @@ -89,6 +91,7 @@ export class YArray extends Type { * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray + * @return {any} */ get (index) { let n = this._start @@ -112,10 +115,11 @@ export class YArray extends Type { /** * Transforms this YArray to a JavaScript Array. * + * @param {Object} [snapshot] * @return {Array} */ - toArray () { - return this.map(c => c) + toArray (snapshot) { + return this.map(c => c, snapshot) } /** @@ -137,14 +141,15 @@ export class YArray extends Type { * element of this YArray. * * @param {Function} f Function that produces an element of the new Array + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @return {Array} A new array with each element being the result of the * callback function */ - map (f) { + map (f, snapshot) { const res = [] this.forEach((c, i) => { res.push(f(c, i, this)) - }) + }, snapshot) return res } @@ -152,14 +157,17 @@ export class YArray extends Type { * Executes a provided function on once on overy element of this YArray. * * @param {Function} f A function to execute on every element of this YArray. + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] */ - forEach (f) { + forEach (f, snapshot) { let index = 0 let n = this._start while (n !== null) { - if (!n._deleted && n._countable) { + if (isVisible(n, snapshot) && n._countable) { if (n instanceof Type) { f(n, index++, this) + } else if (n.constructor === ItemBinary) { + f(n._content, index++, this) } else { const content = n._content const contentLen = content.length @@ -239,7 +247,7 @@ export class YArray extends Type { * * @private * @param {Item} left The element container to use as a reference. - * @param {Array} content The Array of content to insert (see {@see insert}) + * @param {Array} content The Array of content to insert (see {@see insert}) */ insertAfter (left, content) { this._transact(y => { @@ -276,6 +284,29 @@ export class YArray extends Type { left._right = c } left = c + } else if (c.constructor === ArrayBuffer) { + if (prevJsonIns !== null) { + if (y !== null) { + prevJsonIns._integrate(y) + } + left = prevJsonIns + prevJsonIns = null + } + const itemBinary = new ItemBinary() + itemBinary._origin = left + itemBinary._left = left + itemBinary._right = right + itemBinary._right_origin = right + itemBinary._parent = this + itemBinary._content = c + if (y !== null) { + itemBinary._integrate(y) + } else if (left === null) { + this._start = itemBinary + } else { + left._right = itemBinary + } + left = itemBinary } else { if (prevJsonIns === null) { prevJsonIns = new ItemJSON() @@ -294,6 +325,8 @@ export class YArray extends Type { prevJsonIns._integrate(y) } else if (prevJsonIns._left === null) { this._start = prevJsonIns + } else { + left._right = prevJsonIns } } }) @@ -314,7 +347,7 @@ export class YArray extends Type { * yarray.insert(2, [1, 2]) * * @param {number} index The index to insert content at. - * @param {Array} content The array of content + * @param {Array} content The array of content */ insert (index, content) { this._transact(() => { @@ -347,7 +380,7 @@ export class YArray extends Type { /** * Appends content to this YArray. * - * @param {Array} content Array of content to append. + * @param {Array} content Array of content to append. */ push (content) { let n = this._start diff --git a/types/YMap.js b/types/YMap.js index 813d8154..c05fcb3d 100644 --- a/types/YMap.js +++ b/types/YMap.js @@ -7,6 +7,8 @@ import { Type } from '../structs/Type.js' import { ItemJSON } from '../structs/ItemJSON.js' import * as stringify from '../utils/structStringify.js' import { YEvent } from '../utils/YEvent.js' +import { ItemBinary } from '../structs/ItemBinary.js' +import { isVisible } from '../utils/snapshot.js'; /** * Event that describes the changes on a YMap. @@ -53,6 +55,8 @@ export class YMap extends Type { } else { res = item.toString() } + } else if (item.constructor === ItemBinary) { + res = item._content } else { res = item._content[0] } @@ -65,14 +69,23 @@ export class YMap extends Type { /** * Returns the keys for each element in the YMap Type. * + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @return {Array} */ - keys () { + keys (snapshot) { // TODO: Should return either Iterator or Set! let keys = [] - for (let [key, value] of this._map) { - if (!value._deleted) { - keys.push(key) + if (snapshot === undefined) { + for (let [key, value] of this._map) { + if (value._deleted) { + keys.push(key) + } + } + } else { + for (let key in this._map) { + if (this.has(key, snapshot)) { + keys.push(key) + } } } return keys @@ -96,7 +109,7 @@ export class YMap extends Type { * Adds or updates an element with a specified key and value. * * @param {string} key The key of the element to add to this YMap - * @param {Object | string | number | Type} value The value of the element to add + * @param {Object | string | number | Type | ArrayBuffer } value The value of the element to add */ set (key, value) { this._transact(y => { @@ -120,6 +133,9 @@ export class YMap extends Type { value = v } else if (value instanceof Item) { v = value + } else if (value.constructor === ArrayBuffer) { + v = new ItemBinary() + v._content = value } else { v = new ItemJSON() v._content = [value] @@ -141,16 +157,27 @@ export class YMap extends Type { * Returns a specified element from this YMap. * * @param {string} key The key of the element to return. + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] */ - get (key) { + get (key, snapshot) { let v = this._map.get(key) - if (v === undefined || v._deleted) { + if (v === undefined) { return undefined } - if (v instanceof Type) { - return v - } else { - return v._content[v._content.length - 1] + if (snapshot !== undefined) { + // iterate until found element that exists + while (!snapshot.sm.has(v._id.user) || v._id.clock >= snapshot.sm.get(v._id.user)) { + v = v._right + } + } + if (isVisible(v, snapshot)) { + if (v instanceof Type) { + return v + } else if (v.constructor === ItemBinary) { + return v._content + } else { + return v._content[v._content.length - 1] + } } } @@ -158,14 +185,20 @@ export class YMap extends Type { * Returns a boolean indicating whether the specified key exists or not. * * @param {string} key The key to test. + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] */ - has (key) { + has (key, snapshot) { let v = this._map.get(key) - if (v === undefined || v._deleted) { + if (v === undefined) { return false - } else { - return true } + if (snapshot !== undefined) { + // iterate until found element that exists + while (!snapshot.sm.has(v._id.user) || v._id.clock >= snapshot.sm.get(v._id.user)) { + v = v._right + } + } + return isVisible(v, snapshot) } /** diff --git a/types/YText.js b/types/YText.js index b1bc160c..d5f32ddc 100644 --- a/types/YText.js +++ b/types/YText.js @@ -7,6 +7,7 @@ import { ItemString } from '../structs/ItemString.js' import { ItemFormat } from '../structs/ItemFormat.js' import * as stringify from '../utils/structStringify.js' import { YArrayEvent, YArray } from './YArray.js' +import { isVisible } from '../utils/snapshot.js' /** * @private @@ -570,11 +571,12 @@ export class YText extends YArray { /** * Returns the Delta representation of this YText type. * + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @return {Delta} The Delta representation of this type. * * @public */ - toDelta () { + toDelta (snapshot) { let ops = [] let currentAttributes = new Map() let str = '' @@ -600,7 +602,7 @@ export class YText extends YArray { } } while (n !== null) { - if (!n._deleted) { + if (isVisible(n, snapshot)) { switch (n.constructor) { case ItemString: str += n._content diff --git a/types/YXmlElement.js b/types/YXmlElement.js index 0111b915..72131b9f 100644 --- a/types/YXmlElement.js +++ b/types/YXmlElement.js @@ -300,26 +300,34 @@ export class YXmlElement extends YXmlFragment { * * @param {String} attributeName The attribute name that identifies the * queried value. + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @return {String} The queried attribute value. * * @public */ - getAttribute (attributeName) { - return YMap.prototype.get.call(this, attributeName) + getAttribute (attributeName, snapshot) { + return YMap.prototype.get.call(this, attributeName, snapshot) } /** * Returns all attribute name/value pairs in a JSON Object. * + * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @return {Object} A JSON Object that describes the attributes. * * @public */ - getAttributes () { + getAttributes (snapshot) { const obj = {} - for (let [key, value] of this._map) { - if (!value._deleted) { - obj[key] = value._content[0] + if (snapshot === undefined) { + for (let [key, value] of this._map) { + if (!value._deleted) { + obj[key] = value._content[0] + } + } + } else { + for (let key in this._map) { + return YMap.prototype.get.call(this, key, snapshot) } } return obj diff --git a/utils/DeleteStore.js b/utils/DeleteStore.js index 00329b89..9b44bc2f 100644 --- a/utils/DeleteStore.js +++ b/utils/DeleteStore.js @@ -5,6 +5,10 @@ import { Tree } from '../lib/Tree.js' import * as ID from './ID.js' +import * as encoding from '../lib/encoding.js' +import * as decoding from '../lib/decoding.js' +import { deleteItemRange } from '../utils/structManipulation.js' + class DSNode { constructor (id, len, gc) { this._id = id @@ -86,8 +90,168 @@ export class DeleteStore extends Tree { this.put(newMark) } } - // TODO: exchange markDeleted for mark() - markDeleted (id, length) { - this.mark(id, length, false) +} + + +/** + * Stringifies a message-encoded Delete Set. + * + * @param {decoding.Decoder} decoder + * @return {string} + */ +export const stringifyDeleteStore = (decoder) => { + let str = '' + const dsLength = decoding.readUint32(decoder) + for (let i = 0; i < dsLength; i++) { + str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user + const dvLength = decoding.readUint32(decoder) + for (let j = 0; j < dvLength; j++) { + str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n` + } + } + return str +} + +/** + * Write the DeleteSet of a shared document to an Encoder. + * + * @param {encoding.Encoder} encoder + * @param {DeleteStore} ds + */ +export const writeDeleteStore = (encoder, ds) => { + let currentUser = null + let currentLength + let lastLenPos + let numberOfUsers = 0 + const laterDSLenPus = encoding.length(encoder) + encoding.writeUint32(encoder, 0) + ds.iterate(null, null, n => { + const user = n._id.user + const clock = n._id.clock + const len = n.len + const gc = n.gc + if (currentUser !== user) { + numberOfUsers++ + // a new user was found + if (currentUser !== null) { // happens on first iteration + encoding.setUint32(encoder, lastLenPos, currentLength) + } + currentUser = user + encoding.writeVarUint(encoder, user) + // pseudo-fill pos + lastLenPos = encoding.length(encoder) + encoding.writeUint32(encoder, 0) + currentLength = 0 + } + encoding.writeVarUint(encoder, clock) + encoding.writeVarUint(encoder, len) + encoding.writeUint8(encoder, gc ? 1 : 0) + currentLength++ + }) + if (currentUser !== null) { // happens on first iteration + encoding.setUint32(encoder, lastLenPos, currentLength) + } + encoding.setUint32(encoder, laterDSLenPus, numberOfUsers) +} + +/** + * Read delete store from Decoder and create a fresh DeleteStore + * + * @param {decoding.Decoder} decoder + * @return {DeleteStore} + */ +export const readFreshDeleteStore = decoder => { + const ds = new DeleteStore() + const dsLength = decoding.readUint32(decoder) + for (let i = 0; i < dsLength; i++) { + const user = decoding.readVarUint(decoder) + const dvLength = decoding.readUint32(decoder) + for (let j = 0; j < dvLength; j++) { + const from = decoding.readVarUint(decoder) + const len = decoding.readVarUint(decoder) + const gc = decoding.readUint8(decoder) + ds.put(new DSNode(ID.createID(user, from), len, gc)) + } + } + return ds +} + +/** + * Read delete set from Decoder and apply it to a shared document. + * + * @param {decoding.Decoder} decoder + * @param {Y} y + */ +export const readDeleteStore = (decoder, y) => { + const dsLength = decoding.readUint32(decoder) + for (let i = 0; i < dsLength; i++) { + const user = decoding.readVarUint(decoder) + const dv = [] + const dvLength = decoding.readUint32(decoder) + for (let j = 0; j < dvLength; j++) { + const from = decoding.readVarUint(decoder) + const len = decoding.readVarUint(decoder) + const gc = decoding.readUint8(decoder) === 1 + dv.push({from, len, gc}) + } + if (dvLength > 0) { + const deletions = [] + let pos = 0 + let d = dv[pos] + y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => { + // cases: + // 1. d deletes something to the right of n + // => go to next n (break) + // 2. d deletes something to the left of n + // => create deletions + // => reset d accordingly + // *)=> if d doesn't delete anything anymore, go to next d (continue) + // 3. not 2) and d deletes something that also n deletes + // => reset d so that it doesn't contain n's deletion + // *)=> if d does not delete anything anymore, go to next d (continue) + while (d != null) { + var diff = 0 // describe the diff of length in 1) and 2) + if (n._id.clock + n.len <= d.from) { + // 1) + break + } else if (d.from < n._id.clock) { + // 2) + // delete maximum the len of d + // else delete as much as possible + diff = Math.min(n._id.clock - d.from, d.len) + // deleteItemRange(y, user, d.from, diff, true) + deletions.push([user, d.from, diff]) + } else { + // 3) + diff = n._id.clock + n.len - d.from // never null (see 1) + if (d.gc && !n.gc) { + // d marks as gc'd but n does not + // then delete either way + // deleteItemRange(y, user, d.from, Math.min(diff, d.len), true) + deletions.push([user, d.from, Math.min(diff, d.len)]) + } + } + if (d.len <= diff) { + // d doesn't delete anything anymore + d = dv[++pos] + } else { + d.from = d.from + diff // reset pos + d.len = d.len - diff // reset length + } + } + }) + // TODO: It would be more performant to apply the deletes in the above loop + // Adapt the Tree implementation to support delete while iterating + for (let i = deletions.length - 1; i >= 0; i--) { + const del = deletions[i] + deleteItemRange(y, del[0], del[1], del[2], true) + } + // for the rest.. just apply it + for (; pos < dv.length; pos++) { + d = dv[pos] + deleteItemRange(y, user, d.from, d.len, true) + // deletions.push([user, d.from, d.len, d.gc) + } + } } } diff --git a/utils/StateStore.js b/utils/StateStore.js index f739b5f5..2c201f6f 100644 --- a/utils/StateStore.js +++ b/utils/StateStore.js @@ -4,12 +4,65 @@ import * as ID from '../utils/ID.js' +import * as encoding from '../lib/encoding.js' +import * as decoding from '../lib/decoding.js' + +const writeStateStore = (encoder, ss) => { + +} + /** - * @typedef {Map} StateSet + * @typedef {Map} StateMap */ /** - * @private + * Read StateMap from Decoder and return as Map + * + * @param {decoding.Decoder} decoder + * @return {StateMap} + */ +export const readStateMap = decoder => { + const ss = new Map() + const ssLength = decoding.readUint32(decoder) + for (let i = 0; i < ssLength; i++) { + const user = decoding.readVarUint(decoder) + const clock = decoding.readVarUint(decoder) + ss.set(user, clock) + } + return ss +} + +/** + * Write StateMap to Encoder + * + * @param {encoding.Encoder} encoder + * @param {StateMap} state + */ +export const writeStateMap = (encoder, state) => { + // write as fixed-size number to stay consistent with the other encode functions. + // => anytime we write the number of objects that follow, encode as fixed-size number. + encoding.writeUint32(encoder, state.size) + state.forEach((clock, user) => { + encoding.writeVarUint(encoder, user) + encoding.writeVarUint(encoder, clock) + }) +} + +/** + * Read a StateMap from Decoder and return it as string. + * + * @param {decoding.Decoder} decoder + * @return {string} + */ +export const stringifyStateMap = decoder => { + let s = 'State Set: ' + readStateMap(decoder).forEach((clock, user) => { + s += `(${user}: ${clock}), ` + }) + return s +} + +/** */ export class StateStore { constructor (y) { diff --git a/utils/Y.js b/utils/Y.js index 522079ee..937ef47c 100644 --- a/utils/Y.js +++ b/utils/Y.js @@ -1,4 +1,4 @@ -import { DeleteStore } from './DeleteStore.js' +import { DeleteStore, readDeleteStore, writeDeleteStore } from './DeleteStore.js' import { OperationStore } from './OperationStore.js' import { StateStore } from './StateStore.js' import { generateRandomUint32 } from './generateRandomUint32.js' @@ -59,7 +59,7 @@ export class Y extends NamedEventHandler { importModel (decoder) { this.transact(() => { integrateRemoteStructs(decoder, this) - message.readDeleteSet(decoder, this) + readDeleteStore(decoder, this) }) } @@ -71,7 +71,7 @@ export class Y extends NamedEventHandler { exportModel () { const encoder = encoding.createEncoder() message.writeStructs(encoder, this, new Map()) - message.writeDeleteSet(encoder, this) + writeDeleteStore(encoder, this.ds) return encoding.toBuffer(encoder) } _beforeChange () {} @@ -174,7 +174,7 @@ export class Y extends NamedEventHandler { * * @param {String} name * @param {Function} TypeConstructor The constructor of the type definition - * @returns {Type} The created type. Constructed with TypeConstructor + * @returns {any} The created type. Constructed with TypeConstructor */ define (name, TypeConstructor) { let id = createRootID(name, TypeConstructor) @@ -194,6 +194,7 @@ export class Y extends NamedEventHandler { * This returns the same value as `y.share[name]` * * @param {String} name The typename + * @return {any} */ get (name) { return this._map.get(name) diff --git a/utils/snapshot.js b/utils/snapshot.js new file mode 100644 index 00000000..08b5c7a6 --- /dev/null +++ b/utils/snapshot.js @@ -0,0 +1,8 @@ + + +/** + * + * @param {Item} item + * @param {import("../protocols/history").HistorySnapshot} [snapshot] + */ +export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id)) \ No newline at end of file