import * as Y from '../src/index.js' import * as t from 'lib0/testing' import { init } from './testHelper.js' /** * @param {t.TestCase} tc */ export const testBasicMap = tc => { const doc = new Y.Doc() const map = doc.getMap('map') const nested = new Y.Map() nested.set('a1', 'hello') map.set('a', nested) const link = map.link('a') map.set('b', link) const link2 = /** @type {Y.WeakLink} */ (map.get('b')) const expected = nested.toJSON() const actual = link2.deref().toJSON() t.compare(actual, expected) } /** * @param {t.TestCase} tc */ export const testBasicArray = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, 2, 3]) array0.insert(3, [array0.quote(Y.Range.only(1))]) t.compare(array0.get(0), 1) t.compare(array0.get(1), 2) t.compare(array0.get(2), 3) t.compare(array0.get(3).deref(), 2) testConnector.flushAllMessages() t.compare(array1.get(0), 1) t.compare(array1.get(1), 2) t.compare(array1.get(2), 3) t.compare(array1.get(3).deref(), 2) } /** * @param {t.TestCase} tc */ export const testArrayQuoteMultipleElements = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) const nested = new Y.Map([['key', 'value']]) array0.insert(0, [1, 2, nested, 3]) array0.insert(0, [array0.quote(Y.Range.bound(1, 3))]) const link0 = array0.get(0) t.compare(link0.unquote(), [2, nested, 3]) t.compare(array0.get(1), 1) t.compare(array0.get(2), 2) t.compare(array0.get(3), nested) t.compare(array0.get(4), 3) testConnector.flushAllMessages() const link1 = array1.get(0) let unquoted = link1.unquote() t.compare(unquoted[0], 2) t.compare(unquoted[1].toJSON(), { key: 'value' }) t.compare(unquoted[2], 3) t.compare(array1.get(1), 1) t.compare(array1.get(2), 2) t.compare(array1.get(3).toJSON(), { key: 'value' }) t.compare(array1.get(4), 3) array1.insert(3, ['A', 'B']) unquoted = link1.unquote() t.compare(unquoted[0], 2) t.compare(unquoted[1], 'A') t.compare(unquoted[2], 'B') t.compare(unquoted[3].toJSON(), { key: 'value' }) t.compare(unquoted[4], 3) testConnector.flushAllMessages() t.compare(array0.get(0).unquote(), [2, 'A', 'B', nested, 3]) } /** * @param {t.TestCase} tc */ export const testSelfQuotation = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, 2, 3, 4]) const link0 = array0.quote(Y.Range.bound(0, 3, false, true)) array0.insert(1, [link0]) // link is inserted into its own range t.compare(link0.unquote(), [1, link0, 2, 3]) t.compare(array0.get(0), 1) t.compare(array0.get(1), link0) t.compare(array0.get(2), 2) t.compare(array0.get(3), 3) t.compare(array0.get(4), 4) testConnector.flushAllMessages() const link1 = array1.get(1) const unquoted = link1.unquote() t.compare(unquoted, [1, link1, 2, 3]) t.compare(array1.get(0), 1) t.compare(array1.get(1), link1) t.compare(array1.get(2), 2) t.compare(array1.get(3), 3) t.compare(array1.get(4), 4) } /** * @param {t.TestCase} tc */ export const testUpdate = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', new Y.Map([['a1', 'hello']])) const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) map0.set('b', link0) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink>} */ (map1.get('b')) let l1 = /** @type {Y.Map} */ (link1.deref()) let l0 = /** @type {Y.Map} */ (link0.deref()) t.compare(l1.get('a1'), l0.get('a1')) map1.get('a').set('a2', 'world') testConnector.flushAllMessages() l1 = /** @type {Y.Map} */ (link1.deref()) l0 = /** @type {Y.Map} */ (link0.deref()) t.compare(l1.get('a2'), l0.get('a2')) } /** * @param {t.TestCase} tc */ export const testDeleteWeakLink = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', new Y.Map([['a1', 'hello']])) const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) map0.set('b', link0) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink} */ map1.get('b') const l1 = /** @type {Y.Map} */ (link1.deref()) const l0 = /** @type {Y.Map} */ (link0.deref()) t.compare(l1.get('a1'), l0.get('a1')) map1.delete('b') // delete links testConnector.flushAllMessages() // since links have been deleted, they no longer refer to any content t.compare(link0.deref(), undefined) t.compare(link1.deref(), undefined) } /** * @param {t.TestCase} tc */ export const testDeleteSource = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', new Y.Map([['a1', 'hello']])) const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) map0.set('b', link0) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink>} */ (map1.get('b')) const l1 = /** @type {Y.Map} */ (link1.deref()) const l0 = /** @type {Y.Map} */ (link0.deref()) t.compare(l1.get('a1'), l0.get('a1')) map1.delete('a') // delete source of the link testConnector.flushAllMessages() // since source have been deleted, links no longer refer to any content t.compare(link0.deref(), undefined) t.compare(link1.deref(), undefined) } /** * @param {t.TestCase} tc */ export const testObserveMapUpdate = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', 'value') const link0 = /** @type {Y.WeakLink} */ (map0.link('a')) map0.set('b', link0) /** * @type {any} */ let target0 link0.observe((e) => { target0 = e.target }) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink} */ (map1.get('b')) t.compare(link1.deref(), 'value') /** * @type {any} */ let target1 link1.observe((e) => { target1 = e.target }) map0.set('a', 'value2') t.compare(target0.deref(), 'value2') testConnector.flushAllMessages() t.compare(target1.deref(), 'value2') } /** * @param {t.TestCase} tc */ export const testObserveMapDelete = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', 'value') const link0 = /** @type {Y.WeakLink} */ (map0.link('a')) map0.set('b', link0) /** * @type {any} */ let target0 link0.observe((e) => { target0 = e.target }) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink} */ (map1.get('b')) t.compare(link1.deref(), 'value') /** * @type {any} */ let target1 link1.observe((e) => { target1 = e.target }) map0.delete('a') t.compare(target0.deref(), undefined) testConnector.flushAllMessages() t.compare(target1.deref(), undefined) } /** * @param {t.TestCase} tc */ export const testObserveArray = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['A', 'B', 'C']) const link0 = /** @type {Y.WeakLink} */ (array0.quote(Y.Range.bound(1, 2))) array0.insert(0, [link0]) /** * @type {any} */ let target0 link0.observe((e) => { target0 = e.target }) testConnector.flushAllMessages() const link1 = /** @type {Y.WeakLink} */ (array1.get(0)) t.compare(link1.unquote(), ['B', 'C']) /** * @type {any} */ let target1 link1.observe((e) => { target1 = e.target }) array0.delete(2) t.compare(target0.unquote(), ['C']) testConnector.flushAllMessages() t.compare(target1.unquote(), ['C']) array1.delete(2) t.compare(target1.unquote(), []) testConnector.flushAllMessages() t.compare(target0.unquote(), []) target0 = null array0.delete(1) t.compare(target0, null) } /** * @param {t.TestCase} tc */ export const testDeepObserveTransitive = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() /* Structure: - map1 - link-key: <=+-+ - map2: | | - key: value1-+ | - link-link: <--+ */ const map1 = doc.getMap('map1') const map2 = doc.getMap('map2') map2.set('key', 'value1') const link1 = /** @type {Y.WeakLink} */ (map2.link('key')) map1.set('link-key', link1) const link2 = /** @type {Y.WeakLink} */ (map1.link('link-key')) map2.set('link-link', link2) /** * @type {Array} */ let events = [] link2.observeDeep((e) => { events = e }) map2.set('key', 'value2') const values = events.map((e) => e.target.deref()) t.compare(values, ['value2']) } /** * @param {t.TestCase} tc */ export const testDeepObserveTransitive2 = tc => { // test observers in a face of multi-layer linked chains of values const doc = new Y.Doc() /* Structure: - map1 - link-key: <=+-+ - map2: | | - key: value1-+ | - link-link: <==+--+ - map3: | - link-link-link:<-+ */ const map1 = doc.getMap('map1') const map2 = doc.getMap('map2') const map3 = doc.getMap('map3') map2.set('key', 'value1') const link1 = /** @type {Y.WeakLink} */ (map2.link('key')) map1.set('link-key', link1) const link2 = /** @type {Y.WeakLink} */ (map1.link('link-key')) map2.set('link-link', link2) const link3 = /** @type {Y.WeakLink} */ (map2.link('link-link')) map3.set('link-link-link', link3) /** * @type {Array} */ let events = [] link3.observeDeep((e) => { events = e }) map2.set('key', 'value2') const values = events.map((e) => e.target.deref()) t.compare(values, ['value2']) } /** * @param {t.TestCase} tc */ export const testDeepObserveMap = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() /* Structure: - map (observed): - link:<----+ - array: | 0: nested:-+ - key: value */ const map = doc.getMap('map') const array = doc.getArray('array') /** * @type {Array} */ let events = [] map.observeDeep((es) => { events = es.map((e) => { return { target: e.target, keys: e.keys } }) }) const nested = new Y.Map() array.insert(0, [nested]) const link = array.quote(Y.Range.only(0)) map.set('link', link) // update entry in linked map events = [] nested.set('key', 'value') t.compare(events.length, 1) t.compare(events[0].target, nested) t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]])) // delete entry in linked map events = [] nested.delete('key') t.compare(events.length, 1) t.compare(events[0].target, nested) t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value' }]])) // delete linked map array.delete(0) t.compare(events.length, 1) t.compare(events[0].target, link) } /** * @param {t.TestCase} tc */ export const testDeepObserveArray = tc => { // FIXME // test observers in a face of linked chains of values const doc = new Y.Doc() /* Structure: - map: - nested: --------+ - key: value | - array (observed): | 0: <--------------+ */ const map = doc.getMap('map') const array = doc.getArray('array') const nested = new Y.Map() map.set('nested', nested) const link = map.link('nested') array.insert(0, [link]) /** * @type {Array} */ let events = [] array.observeDeep((evts) => { events = [] for (const e of evts) { switch (e.constructor) { case Y.YMapEvent: events.push({ target: e.target, keys: e.keys }) break case Y.YWeakLinkEvent: events.push({ target: e.target }) break default: throw new Error('unexpected event type ' + e.constructor) } } }) // update entry in linked map events = [] nested.set('key', 'value') t.compare(events.length, 1) t.compare(events[0].target, nested) t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]])) nested.set('key', 'value2') t.compare(events.length, 1) t.compare(events[0].target, nested) t.compare(events[0].keys, new Map([['key', { action: 'update', oldValue: 'value' }]])) // delete entry in linked map nested.delete('key') t.compare(events.length, 1) t.compare(events[0].target, nested) t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value2' }]])) // delete linked map map.delete('nested') t.compare(events.length, 1) t.compare(events[0].target, link) } /** * @param {t.TestCase} tc */ export const testDeepObserveNewElementWithinQuotedRange = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) const m1 = new Y.Map() const m3 = new Y.Map() array0.insert(0, [1, m1, m3, 2]) const link0 = array0.quote(Y.Range.bound(1, 2)) array0.insert(0, [link0]) testConnector.flushAllMessages() /** * @type {Array} */ let e0 = [] link0.observeDeep((evts) => { e0 = [] for (const e of evts) { switch (e.constructor) { case Y.YMapEvent: e0.push({ target: e.target, keys: e.keys }) break case Y.YWeakLinkEvent: e0.push({ target: e.target }) break default: throw new Error('unexpected event type ' + e.constructor) } } }) const link1 = /** @type {Y.WeakLink} */ (array1.get(0)) /** * @type {Array} */ let e1 = [] link1.observeDeep((evts) => { e1 = [] for (const e of evts) { switch (e.constructor) { case Y.YMapEvent: e1.push({ target: e.target, keys: e.keys }) break case Y.YWeakLinkEvent: e1.push({ target: e.target }) break default: throw new Error('unexpected event type ' + e.constructor) } } }) const m20 = new Y.Map() array0.insert(3, [m20]) m20.set('key', 'value') t.compare(e0.length, 1) t.compare(e0[0].target, m20) t.compare(e0[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]])) testConnector.flushAllMessages() const m21 = array1.get(3) t.compare(e1.length, 1) t.compare(e1[0].target, m21) t.compare(e1[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]])) } /** * @param {t.TestCase} tc */ export const testMapDeepObserve = tc => { // FIXME const doc = new Y.Doc() const outer = doc.getMap('outer') const inner = new Y.Map() outer.set('inner', inner) /** * @type {Array} */ let events = [] outer.observeDeep((evts) => { events = [] for (const e of evts) { switch (e.constructor) { case Y.YMapEvent: events.push({ target: e.target, keys: e.keys }) break case Y.YWeakLinkEvent: events.push({ target: e.target }) break default: throw new Error('unexpected event type ' + e.constructor) } } }) inner.set('key', 'value1') t.compare(events.length, 1) t.compare(events[0].target, inner) t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]])) events = [] inner.set('key', 'value2') t.compare(events.length, 1) t.compare(events[0].target, inner) t.compare(events[0].keys, new Map([['key', { action: 'update', oldValue: 'value1' }]])) events = [] inner.delete('key') t.compare(events.length, 1) t.compare(events[0].target, inner) t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value2' }]])) } /** * @param {t.TestCase} tc */ export const testDeepObserveRecursive = tc => { // test observers in a face of cycled chains of values const doc = new Y.Doc() /* Structure: array (observed): m0:--------+ - k1:<-+ | | | m1------+ | - k2:<-+ | | | m2------+ | - k0:<----+ */ const root = doc.getArray('array') const m0 = new Y.Map() const m1 = new Y.Map() const m2 = new Y.Map() root.insert(0, [m0]) root.insert(1, [m1]) root.insert(2, [m2]) const l0 = root.quote(Y.Range.only(0)) const l1 = root.quote(Y.Range.only(1)) const l2 = root.quote(Y.Range.only(2)) // create cyclic reference between links m0.set('k1', l1) m1.set('k2', l2) m2.set('k0', l0) /** * @type {Array} */ let events = [] m0.observeDeep((es) => { events = es.map((e) => { return { target: e.target, keys: e.keys } }) }) m1.set('test-key1', 'value1') t.compare(events.length, 1) t.compare(events[0].target, m1) t.compare(events[0].keys, new Map([['test-key1', { action: 'add', oldValue: undefined }]])) events = [] m2.set('test-key2', 'value2') t.compare(events.length, 1) t.compare(events[0].target, m2) t.compare(events[0].keys, new Map([['test-key2', { action: 'add', oldValue: undefined }]])) m1.delete('test-key1') t.compare(events.length, 1) t.compare(events[0].target, m1) t.compare(events[0].keys, new Map([['test-key1', { action: 'delete', oldValue: 'value1' }]])) } /** * @param {t.TestCase} tc */ export const testRemoteMapUpdate = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) map0.set('key', 1) testConnector.flushAllMessages() map1.set('link', map1.link('key')) map0.set('key', 2) map0.set('key', 3) // apply updated content first, link second Y.applyUpdate(users[2], Y.encodeStateAsUpdate(users[0])) Y.applyUpdate(users[2], Y.encodeStateAsUpdate(users[1])) // make sure that link can find the most recent block const link2 = map2.get('link') t.compare(link2.deref(), 3) testConnector.flushAllMessages() const link1 = map1.get('link') const link0 = map0.get('link') t.compare(link0.deref(), 3) t.compare(link1.deref(), 3) t.compare(link2.deref(), 3) } /** * @param {t.TestCase} tc */ export const testTextBasic = tc => { const { testConnector, text0, text1 } = init(tc, { users: 2 }) text0.insert(0, 'abcd') // 'abcd' const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc] t.compare(link0.toString(), 'bc') text0.insert(2, 'ef') // 'abefcd', quote: [befc] t.compare(link0.toString(), 'befc') text0.delete(3, 3) // 'abe', quote: [be] t.compare(link0.toString(), 'be') text0.insertEmbed(3, link0) // 'abe[be]' testConnector.flushAllMessages() const delta = text1.toDelta() const { insert } = delta[1] // YWeakLink t.compare(insert.toString(), 'be') } /** * @param {t.TestCase} tc */ export const testXmlTextBasic = tc => { const { testConnector, xml0, xml1 } = init(tc, { users: 2 }) const text0 = new Y.XmlText() xml0.insert(0, [text0]) text0.insert(0, 'abcd') // 'abcd' const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc] t.compare(link0.toString(), 'bc') text0.insert(2, 'ef') // 'abefcd', quote: [befc] t.compare(link0.toString(), 'befc') text0.delete(3, 3) // 'abe', quote: [be] t.compare(link0.toString(), 'be') text0.insertEmbed(3, link0) // 'abe[be]' testConnector.flushAllMessages() const text1 = /** @type {Y.XmlText} */ (xml1.get(0)) const delta = text1.toDelta() const { insert } = delta[1] // YWeakLink t.compare(insert.toString(), 'be') } /** * @param {t.TestCase} tc */ export const testQuoteFormattedText = tc => { const doc = new Y.Doc() const text = /** @type {Y.XmlText} */ (doc.get('text', Y.XmlText)) const text2 = /** @type {Y.XmlText} */ (doc.get('text2', Y.XmlText)) text.insert(0, 'abcde') text.format(0, 1, { b: true }) text.format(1, 3, { i: true }) // 'abcde' const l1 = text.quote(Y.Range.bound(0, 1)) t.compare(l1.toString(), 'ab') const l2 = text.quote(Y.Range.only(2)) // 'c' t.compare(l2.toString(), 'c') const l3 = text.quote(Y.Range.bound(3, 4)) // 'de' t.compare(l3.toString(), 'de') text2.insertEmbed(0, l1) text2.insertEmbed(1, l2) text2.insertEmbed(2, l3) const delta = text2.toDelta() t.compare(delta, [ { insert: l1 }, { insert: l2 }, { insert: l3 } ]) } /** * @param {t.TestCase} tc */ export const testTextLowerBoundary = tc => { const { testConnector, text0, text1, array0 } = init(tc, { users: 2 }) text0.insert(0, 'abcdef') testConnector.flushAllMessages() const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] const linkExclusive = text0.quote(Y.Range.bound(0, 4, true, false)) // (0..4] array0.insert(0, [linkInclusive, linkExclusive]) t.compare(linkInclusive.toString(), 'bcde') t.compare(linkExclusive.toString(), 'bcde') text1.insert(1, 'xyz') testConnector.flushAllMessages() t.compare(linkInclusive.toString(), 'bcde') t.compare(linkExclusive.toString(), 'xyzbcde') } /** * @param {t.TestCase} tc */ export const testTextUpperBoundary = tc => { const { testConnector, text0, text1, array0 } = init(tc, { users: 2 }) text0.insert(0, 'abcdef') testConnector.flushAllMessages() const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] const linkExclusive = text0.quote(Y.Range.bound(1, 5, false, true)) // [1..5) array0.insert(0, [linkInclusive, linkExclusive]) t.compare(linkInclusive.toString(), 'bcde') t.compare(linkExclusive.toString(), 'bcde') text1.insert(5, 'xyz') testConnector.flushAllMessages() t.compare(linkInclusive.toString(), 'bcde') t.compare(linkExclusive.toString(), 'bcdexyz') } /** * @param {t.TestCase} tc */ export const testArrayLowerBoundary = tc => { const { testConnector, array0, array1, map0 } = init(tc, { users: 2 }) array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f']) testConnector.flushAllMessages() const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] const linkExclusive = array0.quote(Y.Range.bound(0, 4, true, false)) // (0..4] map0.set('inclusive', linkInclusive) map0.set('exclusive', linkExclusive) t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e']) array1.insert(1, ['x', 'y', 'z']) testConnector.flushAllMessages() t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) t.compare(linkExclusive.unquote(), ['x', 'y', 'z', 'b', 'c', 'd', 'e']) } /** * @param {t.TestCase} tc */ export const testArrayUpperBoundary = tc => { const { testConnector, array0, array1, map0 } = init(tc, { users: 2 }) array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f']) testConnector.flushAllMessages() const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] const linkExclusive = array0.quote(Y.Range.bound(1, 5, false, true)) // [1..5) map0.set('inclusive', linkInclusive) map0.set('exclusive', linkExclusive) t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e']) array1.insert(5, ['x', 'y', 'z']) testConnector.flushAllMessages() t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e', 'x', 'y', 'z']) }