refactoring: removed default connector and persistence, new code style, proper jsdocs, enabled typechecking
This commit is contained in:
parent
fe038822a3
commit
e1ece6dc66
@ -248,7 +248,7 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
|
||||
* y-websockets-client aways waits to sync with the server
|
||||
* y.connector.disconnect()
|
||||
* Force to disconnect this instance from the other instances
|
||||
* y.connector.reconnect()
|
||||
* y.connector.connect()
|
||||
* Try to reconnect to the other instances (needs to be supported by the
|
||||
connector)
|
||||
* Not supported by y-xmpp
|
||||
|
7
lib/binary.js
Normal file
7
lib/binary.js
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
export const BITS32 = 0xFFFFFFFF
|
||||
export const BITS21 = (1 << 21) - 1
|
||||
export const BITS16 = (1 << 16) - 1
|
||||
|
||||
export const BIT26 = 1 << 26
|
||||
export const BIT32 = 1 << 32
|
@ -6,7 +6,7 @@ import * as globals from './globals.js'
|
||||
/**
|
||||
* A Decoder handles the decoding of an ArrayBuffer.
|
||||
*/
|
||||
class Decoder {
|
||||
export class Decoder {
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer Binary data to decode
|
||||
*/
|
||||
@ -166,23 +166,3 @@ export const peekVarString = decoder => {
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Read ID.
|
||||
* * If first varUint read is 0xFFFFFF a RootID is returned.
|
||||
* * Otherwise an ID is returned
|
||||
*
|
||||
* @param {Decoder} decoder
|
||||
* @return {ID}
|
||||
*
|
||||
export const readID = decoder => {
|
||||
let user = decoder.readVarUint()
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = new RootID(decoder.readVarString(), null)
|
||||
rid.type = decoder.readVarUint()
|
||||
return rid
|
||||
}
|
||||
return new ID(user, decoder.readVarUint())
|
||||
}
|
||||
*/
|
||||
|
@ -21,16 +21,15 @@ export const createEncoder = () => new Encoder()
|
||||
* The current length of the encoded data.
|
||||
*/
|
||||
export const length = encoder => {
|
||||
let len = 0
|
||||
let len = encoder.cpos
|
||||
for (let i = 0; i < encoder.bufs.length; i++) {
|
||||
len += encoder.bufs[i].length
|
||||
}
|
||||
len += encoder.cpos
|
||||
return len
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ArrayBuffer.
|
||||
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
||||
* @param {Encoder} encoder
|
||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
||||
*/
|
||||
@ -185,12 +184,14 @@ export const writeVarString = (encoder, str) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the content of another biUint8Arr
|
||||
* Write the content of another Encoder.
|
||||
*
|
||||
* TODO: can be improved!
|
||||
*
|
||||
* @param {Encoder} encoder The enUint8Arr
|
||||
* @param encoderToAppend The BinaryEncoder to be written.
|
||||
* @param {Encoder} append The BinaryEncoder to be written.
|
||||
*/
|
||||
export const writeBinaryEncoder = (encoder, encoderToAppend) => writeArrayBuffer(encoder, toBuffer(encoder))
|
||||
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))
|
||||
|
||||
/**
|
||||
* Append an arrayBuffer to the encoder.
|
||||
@ -215,20 +216,3 @@ export const writePayload = (encoder, arrayBuffer) => {
|
||||
writeVarUint(encoder, arrayBuffer.byteLength)
|
||||
writeArrayBuffer(encoder, arrayBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an ID at the current position.
|
||||
*
|
||||
* @param {ID} id The ID that is to be written.
|
||||
*
|
||||
export const writeID = (encoder, id) => {
|
||||
const user = id.user
|
||||
writeVarUint(encoder, user)
|
||||
if (user !== RootFakeUserID) {
|
||||
writeVarUint(encoder, id.clock)
|
||||
} else {
|
||||
writeVarString(encoder, id.name)
|
||||
writeVarUint(encoder, id.type)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
2
lib/math.js
Normal file
2
lib/math.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export const floor = Math.floor
|
@ -1,32 +1,30 @@
|
||||
// TODO: rename mutex
|
||||
|
||||
/**
|
||||
* Creates a mutual exclude function with the following property:
|
||||
*
|
||||
* @example
|
||||
* const mutualExclude = createMutualExclude()
|
||||
* mutualExclude(function () {
|
||||
* const mutex = createMutex()
|
||||
* mutex(function () {
|
||||
* // This function is immediately executed
|
||||
* mutualExclude(function () {
|
||||
* mutex(function () {
|
||||
* // This function is never executed, as it is called with the same
|
||||
* // mutualExclude
|
||||
* // mutex function
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* @return {Function} A mutual exclude function
|
||||
* @public
|
||||
*/
|
||||
export function createMutualExclude () {
|
||||
var token = true
|
||||
return function mutualExclude (f, g) {
|
||||
export const createMutex = () => {
|
||||
let token = true
|
||||
return (f, g) => {
|
||||
if (token) {
|
||||
token = false
|
||||
try {
|
||||
f()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
token = true
|
||||
}
|
||||
token = true
|
||||
} else if (g !== undefined) {
|
||||
g()
|
||||
}
|
2
lib/number.js
Normal file
2
lib/number.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
66
lib/random/PRNG/Mt19937.js
Normal file
66
lib/random/PRNG/Mt19937.js
Normal file
@ -0,0 +1,66 @@
|
||||
const N = 624
|
||||
const M = 397
|
||||
|
||||
function twist (u, v) {
|
||||
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
||||
}
|
||||
|
||||
function nextState (state) {
|
||||
let p = 0
|
||||
let j
|
||||
for (j = N - M + 1; --j; p++) {
|
||||
state[p] = state[p + M] ^ twist(state[p], state[p + 1])
|
||||
}
|
||||
for (j = M; --j; p++) {
|
||||
state[p] = state[p + M - N] ^ twist(state[p], state[p + 1])
|
||||
}
|
||||
state[p] = state[p + M - N] ^ twist(state[p], state[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c).
|
||||
* MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not
|
||||
* very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and
|
||||
* needs to recompute its state after generating 624 numbers.
|
||||
*
|
||||
* @example
|
||||
* const gen = new Mt19937(new Date().getTime())
|
||||
* console.log(gen.next())
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default class Mt19937 {
|
||||
/**
|
||||
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||
*/
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
const state = new Uint32Array(N)
|
||||
state[0] = seed
|
||||
for (let i = 1; i < N; i++) {
|
||||
state[i] = (Math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & 0xFFFFFFFF
|
||||
}
|
||||
this._state = state
|
||||
this._i = 0
|
||||
nextState(this._state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random signed integer.
|
||||
*
|
||||
* @return {Number} A 32 bit signed integer.
|
||||
*/
|
||||
next () {
|
||||
if (this._i === N) {
|
||||
// need to compute a new state
|
||||
nextState(this._state)
|
||||
this._i = 0
|
||||
}
|
||||
let y = this._state[this._i++]
|
||||
y ^= (y >>> 11)
|
||||
y ^= (y << 7) & 0x9d2c5680
|
||||
y ^= (y << 15) & 0xefc60000
|
||||
y ^= (y >>> 18)
|
||||
return y
|
||||
}
|
||||
}
|
48
lib/random/PRNG/PRNG.tests.js
Normal file
48
lib/random/PRNG/PRNG.tests.js
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
import Mt19937 from './Mt19937.js'
|
||||
import Xoroshiro128plus from './Xoroshiro128plus.js'
|
||||
import Xorshift32 from './Xorshift32.js'
|
||||
import * as time from '../../time.js'
|
||||
|
||||
const DIAMETER = 300
|
||||
const NUMBERS = 10000
|
||||
|
||||
function runPRNG (name, Gen) {
|
||||
console.log('== ' + name + ' ==')
|
||||
const gen = new Gen(1234)
|
||||
let head = 0
|
||||
let tails = 0
|
||||
const date = time.getUnixTime()
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.height = DIAMETER
|
||||
canvas.width = DIAMETER
|
||||
const ctx = canvas.getContext('2d')
|
||||
const vals = new Set()
|
||||
ctx.fillStyle = 'blue'
|
||||
for (let i = 0; i < NUMBERS; i++) {
|
||||
const n = gen.next() & 0xFFFFFF
|
||||
const x = (gen.next() >>> 0) % DIAMETER
|
||||
const y = (gen.next() >>> 0) % DIAMETER
|
||||
ctx.fillRect(x, y, 1, 2)
|
||||
if ((n & 1) === 1) {
|
||||
head++
|
||||
} else {
|
||||
tails++
|
||||
}
|
||||
if (vals.has(n)) {
|
||||
console.warn(`The generator generated a duplicate`)
|
||||
}
|
||||
vals.add(n)
|
||||
}
|
||||
console.log('time: ', time.getUnixTime() - date)
|
||||
console.log('head:', head, 'tails:', tails)
|
||||
console.log('%c ', `font-size: 200px; background: url(${canvas.toDataURL()}) no-repeat;`)
|
||||
const h1 = document.createElement('h1')
|
||||
h1.insertBefore(document.createTextNode(name), null)
|
||||
document.body.insertBefore(h1, null)
|
||||
document.body.appendChild(canvas)
|
||||
}
|
||||
|
||||
runPRNG('mt19937', Mt19937)
|
||||
runPRNG('xoroshiro128plus', Xoroshiro128plus)
|
||||
runPRNG('xorshift32', Xorshift32)
|
5
lib/random/PRNG/README.md
Normal file
5
lib/random/PRNG/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Pseudo Random Number Generators (PRNG)
|
||||
|
||||
Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. Two PRNGs must generate the same random sequence of numbers if given the same seed.
|
||||
|
||||
TODO: explain what POINT is
|
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
import Xorshift32 from './Xorshift32.js'
|
||||
|
||||
/**
|
||||
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
||||
*
|
||||
* This implementation follows the idea of the original xoroshiro128plus implementation,
|
||||
* but is optimized for the JavaScript runtime. I.e.
|
||||
* * The operations are performed on 32bit integers (the original implementation works with 64bit values).
|
||||
* * The initial 128bit state is computed based on a 32bit seed and Xorshift32.
|
||||
* * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus.
|
||||
* Caution: The last addition step works slightly different than in the original implementation - the add carry of the
|
||||
* first 32bit addition is not carried over to the last 32bit.
|
||||
*
|
||||
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
||||
*/
|
||||
export default class Xoroshiro128plus {
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
// This is a variant of Xoroshiro128plus to fill the initial state
|
||||
const xorshift32 = new Xorshift32(seed)
|
||||
this.state = new Uint32Array(4)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.state[i] = xorshift32.next()
|
||||
}
|
||||
this._fresh = true
|
||||
}
|
||||
next () {
|
||||
const state = this.state
|
||||
if (this._fresh) {
|
||||
this._fresh = false
|
||||
return (state[0] + state[2]) & 0xFFFFFFFF
|
||||
} else {
|
||||
this._fresh = true
|
||||
const s0 = state[0]
|
||||
const s1 = state[1]
|
||||
const s2 = state[2] ^ s0
|
||||
const s3 = state[3] ^ s1
|
||||
// function js_rotl (x, k) {
|
||||
// k = k - 32
|
||||
// const x1 = x[0]
|
||||
// const x2 = x[1]
|
||||
// x[0] = x2 << k | x1 >>> (32 - k)
|
||||
// x[1] = x1 << k | x2 >>> (32 - k)
|
||||
// }
|
||||
// rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23
|
||||
state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18)
|
||||
state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14)
|
||||
// rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9
|
||||
state[2] = s3 << 4 | s2 >>> 28
|
||||
state[3] = s2 << 4 | s3 >>> 28
|
||||
return (state[1] + state[3]) & 0xFFFFFFFF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// reference implementation
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
uint64_t s[2];
|
||||
|
||||
static inline uint64_t rotl(const uint64_t x, int k) {
|
||||
return (x << k) | (x >> (64 - k));
|
||||
}
|
||||
|
||||
uint64_t next(void) {
|
||||
const uint64_t s0 = s[0];
|
||||
uint64_t s1 = s[1];
|
||||
s1 ^= s0;
|
||||
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b
|
||||
s[1] = rotl(s1, 36); // c
|
||||
return (s[0] + s[1]) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int i;
|
||||
s[0] = 1111 | (1337ul << 32);
|
||||
s[1] = 1234 | (9999ul << 32);
|
||||
|
||||
printf("1000 outputs of genrand_int31()\n");
|
||||
for (i=0; i<100; i++) {
|
||||
printf("%10lu ", i);
|
||||
printf("%10lu ", next());
|
||||
printf("- %10lu ", s[0] >> 32);
|
||||
printf("%10lu ", (s[0] << 32) >> 32);
|
||||
printf("%10lu ", s[1] >> 32);
|
||||
printf("%10lu ", (s[1] << 32) >> 32);
|
||||
printf("\n");
|
||||
// if (i%5==4) printf("\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
*/
|
26
lib/random/PRNG/Xorshift32.js
Normal file
26
lib/random/PRNG/Xorshift32.js
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
/**
|
||||
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
|
||||
*/
|
||||
export default class Xorshift32 {
|
||||
/**
|
||||
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||
*/
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
this._state = seed
|
||||
}
|
||||
/**
|
||||
* Generate a random signed integer.
|
||||
*
|
||||
* @return {Number} A 32 bit signed integer.
|
||||
*/
|
||||
next () {
|
||||
let x = this._state
|
||||
x ^= x << 13
|
||||
x ^= x >> 17
|
||||
x ^= x << 5
|
||||
this._state = x
|
||||
return x
|
||||
}
|
||||
}
|
131
lib/random/random.js
Normal file
131
lib/random/random.js
Normal file
@ -0,0 +1,131 @@
|
||||
|
||||
import * as binary from '../binary.js'
|
||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||
import * as math from '../math.js'
|
||||
|
||||
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
|
||||
|
||||
/**
|
||||
* Description of the function
|
||||
* @callback generatorNext
|
||||
* @return {number} A 32bit integer
|
||||
*/
|
||||
|
||||
/**
|
||||
* A random type generator.
|
||||
*
|
||||
* @typedef {Object} PRNG
|
||||
* @property {generatorNext} next Generate new number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
|
||||
* This is the fastest full-period generator passing BigCrush without systematic failures.
|
||||
* But there are more PRNGs available in ./PRNG/.
|
||||
*
|
||||
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
|
||||
* @return {PRNG}
|
||||
*/
|
||||
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
|
||||
|
||||
/**
|
||||
* Generates a single random bool.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Boolean} A random boolean
|
||||
*/
|
||||
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
|
||||
|
||||
/**
|
||||
* Generates a random integer with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
|
||||
|
||||
/**
|
||||
* Generates a random integer with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
|
||||
|
||||
/**
|
||||
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
|
||||
*
|
||||
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
|
||||
*/
|
||||
export const char = gen => fromCharCode(int32(gen, 32, 126))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A single letter (a-z)
|
||||
*/
|
||||
export const letter = gen => fromCharCode(int32(gen, 97, 122))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A random word without spaces consisting of letters (a-z)
|
||||
*/
|
||||
export const word = gen => {
|
||||
const len = int32(gen, 0, 20)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += letter(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: this function produces invalid runes. Does not cover all of utf16!!
|
||||
*/
|
||||
export const utf16Rune = gen => {
|
||||
const codepoint = int32(gen, 0, 256)
|
||||
return fromCodePoint(codepoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @param {number} [maxlen = 20]
|
||||
*/
|
||||
export const utf16String = (gen, maxlen = 20) => {
|
||||
const len = int32(gen, 0, maxlen)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += utf16Rune(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one element of a given array.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Array<T>} array Non empty Array of possible values.
|
||||
* @return {T} One of the values of the supplied Array.
|
||||
* @template T
|
||||
*/
|
||||
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]
|
110
lib/random/random.test.js
Normal file
110
lib/random/random.test.js
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
*TODO: enable tests
|
||||
import * as rt from '../rich-text/formatters.mjs'
|
||||
import { test } from '../test/test.mjs'
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
|
||||
import Xorshift32 from './PRNG/Xorshift32.mjs'
|
||||
import MT19937 from './PRNG/Mt19937.mjs'
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
|
||||
import { BIT32 } from '../binary/constants.mjs'
|
||||
|
||||
function init (Gen) {
|
||||
return {
|
||||
gen: new Gen(1234)
|
||||
}
|
||||
}
|
||||
|
||||
const PRNGs = [
|
||||
{ name: 'Xoroshiro128plus', Gen: Xoroshiro128plus },
|
||||
{ name: 'Xorshift32', Gen: Xorshift32 },
|
||||
{ name: 'MT19937', Gen: MT19937 }
|
||||
]
|
||||
|
||||
const ITERATONS = 1000000
|
||||
|
||||
for (const PRNG of PRNGs) {
|
||||
const prefix = rt.orange`${PRNG.name}:`
|
||||
|
||||
test(rt.plain`${prefix} generateBool`, function generateBoolTest (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let head = 0
|
||||
let tail = 0
|
||||
let b
|
||||
let i
|
||||
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
b = generateBool(gen)
|
||||
if (b) {
|
||||
head++
|
||||
} else {
|
||||
tail++
|
||||
}
|
||||
}
|
||||
t.log(`Generated ${head} heads and ${tail} tails.`)
|
||||
t.assert(tail >= Math.floor(ITERATONS * 0.49), 'Generated enough tails.')
|
||||
t.assert(head >= Math.floor(ITERATONS * 0.49), 'Generated enough heads.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateInt integers average correctly`, function averageIntTest (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let count = 0
|
||||
let i
|
||||
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
count += generateInt(gen, 0, 100)
|
||||
}
|
||||
const average = count / ITERATONS
|
||||
const expectedAverage = 100 / 2
|
||||
t.log(`Average is: ${average}. Expected average is ${expectedAverage}.`)
|
||||
t.assert(Math.abs(average - expectedAverage) <= 1, 'Expected average is at most 1 off.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateInt32 generates integer with 32 bits`, function generateLargeIntegers (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let num = 0
|
||||
let i
|
||||
let newNum
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
newNum = generateInt32(gen, 0, MAX_SAFE_INTEGER)
|
||||
if (newNum > num) {
|
||||
num = newNum
|
||||
}
|
||||
}
|
||||
t.log(`Largest number generated is ${num} (0b${num.toString(2)})`)
|
||||
t.assert(num > (BIT32 >>> 0), 'Largest number is 32 bits long.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateReal has 53 bit resolution`, function real53bitResolution (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let num = 0
|
||||
let i
|
||||
let newNum
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
newNum = generateReal(gen) * MAX_SAFE_INTEGER
|
||||
if (newNum > num) {
|
||||
num = newNum
|
||||
}
|
||||
}
|
||||
t.log(`Largest number generated is ${num}.`)
|
||||
t.assert((MAX_SAFE_INTEGER - num) / MAX_SAFE_INTEGER < 0.01, 'Largest number is close to MAX_SAFE_INTEGER (at most 1% off).')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateChar generates all described characters`, function real53bitResolution (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
const charSet = new Set()
|
||||
const chars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~"'
|
||||
let i
|
||||
let char
|
||||
for (i = chars.length - 1; i >= 0; i--) {
|
||||
charSet.add(chars[i])
|
||||
}
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
char = generateChar(gen)
|
||||
charSet.delete(char)
|
||||
}
|
||||
t.log(`Charactes missing: ${charSet.size} - generating all of "${chars}"`)
|
||||
t.assert(charSet.size === 0, 'Generated all documented characters.')
|
||||
})
|
||||
}
|
||||
*/
|
2
lib/string.js
Normal file
2
lib/string.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const fromCharCode = String.fromCharCode
|
||||
export const fromCodePoint = String.fromCodePoint
|
3
lib/time.js
Normal file
3
lib/time.js
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
export const getDate = () => new Date()
|
||||
export const getUnixTime = () => getDate().getTime()
|
2349
package-lock.json
generated
2349
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@ -4,24 +4,16 @@
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"module": "./src/index.js",
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules src/Connectors/WebsocketsConnector/server.js",
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard src/**/*.mjs test/**/*.mjs tests-lib/**/*.js",
|
||||
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
|
||||
"docs": "esdoc",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag",
|
||||
"demos": "concurrently 'node --experimental-modules src/Connectors/WebsocketsConnector/server.js' 'http-server'"
|
||||
},
|
||||
"now": {
|
||||
"engines": {
|
||||
"node": "10.x.x"
|
||||
}
|
||||
"postversion": "npm run dist"
|
||||
},
|
||||
"files": [
|
||||
"y.*",
|
||||
@ -56,32 +48,26 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"codemirror": "^5.37.0",
|
||||
"concurrently": "^3.4.0",
|
||||
"concurrently": "^3.6.1",
|
||||
"cutest": "^0.1.9",
|
||||
"esdoc": "^1.0.4",
|
||||
"esdoc": "^1.1.0",
|
||||
"esdoc-standard-plugin": "^1.0.0",
|
||||
"quill": "^1.3.5",
|
||||
"quill-cursors": "^1.0.2",
|
||||
"quill": "^1.3.6",
|
||||
"quill-cursors": "^1.0.3",
|
||||
"rollup": "^0.58.2",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-commonjs": "^8.4.1",
|
||||
"rollup-plugin-inject": "^2.2.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.2",
|
||||
"rollup-plugin-node-resolve": "^3.4.0",
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^11.0.1",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"uws": "^10.148.0"
|
||||
"standard": "^11.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
import { createMutex } from '../../lib/mutex.js'
|
||||
|
||||
/**
|
||||
* Abstract class for bindings.
|
||||
@ -35,7 +35,7 @@ export default class Binding {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._mutualExclude = createMutualExclude()
|
||||
this._mutualExclude = createMutex()
|
||||
}
|
||||
/**
|
||||
* Remove all data observers (both from the type and the target).
|
||||
|
@ -8,6 +8,10 @@ import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||
import typeObserver from './typeObserver.js'
|
||||
import domObserver from './domObserver.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
||||
*/
|
||||
|
||||
/**
|
||||
* A binding that binds the children of a YXmlFragment to a DOM element.
|
||||
*
|
||||
@ -26,7 +30,7 @@ export default class DomBinding extends Binding {
|
||||
* @param {Element} target The bind target. Mirrors the target.
|
||||
* @param {Object} [opts] Optional configurations
|
||||
|
||||
* @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use.
|
||||
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
|
||||
*/
|
||||
constructor (type, target, opts = {}) {
|
||||
// Binding handles textType as this.type and domTextarea as this.target
|
||||
@ -48,7 +52,7 @@ export default class DomBinding extends Binding {
|
||||
/**
|
||||
* Defines which DOM attributes and elements to filter out.
|
||||
* Also filters remote changes.
|
||||
* @type {FilterFunction}
|
||||
* @type {DomFilter}
|
||||
*/
|
||||
this.filter = opts.filter || defaultFilter
|
||||
// set initial value
|
||||
@ -57,7 +61,7 @@ export default class DomBinding extends Binding {
|
||||
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
|
||||
})
|
||||
this._typeObserver = typeObserver.bind(this)
|
||||
this._domObserver = (mutations) => {
|
||||
this._domObserver = mutations => {
|
||||
domObserver.call(this, mutations, opts.document)
|
||||
}
|
||||
type.observeDeep(this._typeObserver)
|
||||
@ -119,7 +123,7 @@ export default class DomBinding extends Binding {
|
||||
|
||||
/**
|
||||
* NOTE: currently does not apply filter to existing elements!
|
||||
* @param {FilterFunction} filter The filter function to use from now on.
|
||||
* @param {DomFilter} filter The filter function to use from now on.
|
||||
*/
|
||||
setFilter (filter) {
|
||||
this.filter = filter
|
||||
|
@ -1,60 +1,65 @@
|
||||
|
||||
/* eslint-env browser */
|
||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||
import YXmlElement from '../../Types/YXml/YXmlElement.js'
|
||||
import { createAssociation, domsToTypes } from './util.js'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||
*
|
||||
* @param {Element|TextNode} element The DOM Element
|
||||
* @param {Element|Text} element The DOM Element
|
||||
* @param {?Document} _document Optional. Provide the global document object
|
||||
* @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks
|
||||
* @param {Filter} [filter=defaultFilter] Optional. Dom element filter
|
||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
||||
* @param {DomFilter} [filter=defaultFilter] Optional. Dom element filter
|
||||
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
||||
* @return {YXmlElement | YXmlText}
|
||||
* @return {YXmlElement | YXmlText | false}
|
||||
*/
|
||||
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
|
||||
let type
|
||||
switch (element.nodeType) {
|
||||
case _document.ELEMENT_NODE:
|
||||
let hookName = null
|
||||
let hook
|
||||
// configure `hookName !== undefined` if element is a hook.
|
||||
if (element.hasAttribute('data-yjs-hook')) {
|
||||
hookName = element.getAttribute('data-yjs-hook')
|
||||
hook = hooks[hookName]
|
||||
if (hook === undefined) {
|
||||
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
|
||||
delete element.removeAttribute('data-yjs-hook')
|
||||
hookName = null
|
||||
}
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let type = null
|
||||
if (element instanceof Element) {
|
||||
let hookName = null
|
||||
let hook
|
||||
// configure `hookName !== undefined` if element is a hook.
|
||||
if (element.hasAttribute('data-yjs-hook')) {
|
||||
hookName = element.getAttribute('data-yjs-hook')
|
||||
hook = hooks[hookName]
|
||||
if (hook === undefined) {
|
||||
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
|
||||
element.removeAttribute('data-yjs-hook')
|
||||
hookName = null
|
||||
}
|
||||
if (hookName === null) {
|
||||
// Not a hook
|
||||
const attrs = filterDomAttributes(element, filter)
|
||||
if (attrs === null) {
|
||||
type = false
|
||||
} else {
|
||||
type = new YXmlElement(element.nodeName)
|
||||
attrs.forEach((val, key) => {
|
||||
type.setAttribute(key, val)
|
||||
})
|
||||
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
|
||||
}
|
||||
}
|
||||
if (hookName === null) {
|
||||
// Not a hook
|
||||
const attrs = filterDomAttributes(element, filter)
|
||||
if (attrs === null) {
|
||||
type = false
|
||||
} else {
|
||||
// Is a hook
|
||||
type = new YXmlHook(hookName)
|
||||
hook.fillType(element, type)
|
||||
type = new YXmlElement(element.nodeName)
|
||||
attrs.forEach((val, key) => {
|
||||
type.setAttribute(key, val)
|
||||
})
|
||||
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
|
||||
}
|
||||
break
|
||||
case _document.TEXT_NODE:
|
||||
type = new YXmlText()
|
||||
type.insert(0, element.nodeValue)
|
||||
break
|
||||
default:
|
||||
throw new Error('Can\'t transform this node type to a YXml type!')
|
||||
} else {
|
||||
// Is a hook
|
||||
type = new YXmlHook(hookName)
|
||||
hook.fillType(element, type)
|
||||
}
|
||||
} else if (element instanceof Text) {
|
||||
type = new YXmlText()
|
||||
type.insert(0, element.nodeValue)
|
||||
} else {
|
||||
throw new Error('Can\'t transform this node type to a YXml type!')
|
||||
}
|
||||
createAssociation(binding, element, type)
|
||||
return type
|
||||
|
@ -1,5 +1,12 @@
|
||||
import isParentOf from '../../Util/isParentOf.js'
|
||||
|
||||
/**
|
||||
* @callback DomFilter
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {Map | null}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default filter method (does nothing).
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ function _getCurrentRelativeSelection (domBinding) {
|
||||
return null
|
||||
}
|
||||
|
||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : () => null
|
||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
||||
|
||||
export function beforeTransactionSelectionFixer (domBinding) {
|
||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-env browser */
|
||||
/* global getSelection */
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
||||
@ -17,11 +18,17 @@ function findScrollReference (scrollingElement) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (anchor.nodeType === document.TEXT_NODE) {
|
||||
anchor = anchor.parentElement
|
||||
/**
|
||||
* @type {Element}
|
||||
*/
|
||||
let elem = anchor.parentElement
|
||||
if (anchor instanceof Element) {
|
||||
elem = anchor
|
||||
}
|
||||
return {
|
||||
elem,
|
||||
top: elem.getBoundingClientRect().top
|
||||
}
|
||||
const top = anchor.getBoundingClientRect().top
|
||||
return { elem: anchor, top: top }
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
@ -1,6 +1,13 @@
|
||||
|
||||
import domToType from './domToType.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText
|
||||
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
|
||||
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
|
||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterates items until an undeleted item is found.
|
||||
*
|
||||
@ -32,8 +39,8 @@ export function removeAssociation (domBinding, dom, type) {
|
||||
* type).
|
||||
*
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {Element} dom The dom that is to be associated with type
|
||||
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
||||
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
||||
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
|
||||
*
|
||||
*/
|
||||
export function createAssociation (domBinding, dom, type) {
|
||||
|
@ -1,32 +0,0 @@
|
||||
|
||||
import { writeStructs } from './syncStep1.js'
|
||||
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.js'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.js'
|
||||
|
||||
/**
|
||||
* Read the Decoder and fill the Yjs instance with data in the decoder.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {BinaryDecoder} decoder The BinaryDecoder to read from.
|
||||
*/
|
||||
export function fromBinary (y, decoder) {
|
||||
y.transact(function () {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the Yjs model to binary format.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @return {BinaryEncoder} The encoder instance that can be transformed
|
||||
* to ArrayBuffer or Buffer.
|
||||
*/
|
||||
export function toBinary (y) {
|
||||
let encoder = new BinaryEncoder()
|
||||
writeStructs(y, encoder, new Map())
|
||||
writeDeleteSet(y, encoder)
|
||||
return encoder
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
import { deleteItemRange } from '../Struct/Delete.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
|
||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(' -' + user + ':')
|
||||
let dvLength = decoder.readVarUint()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
||||
}
|
||||
}
|
||||
return strBuilder
|
||||
}
|
||||
|
||||
export function writeDeleteSet (y, encoder) {
|
||||
let currentUser = null
|
||||
let currentLength
|
||||
let lastLenPos
|
||||
|
||||
let numberOfUsers = 0
|
||||
let laterDSLenPus = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
|
||||
y.ds.iterate(null, null, function (n) {
|
||||
var user = n._id.user
|
||||
var clock = n._id.clock
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
if (currentUser !== user) {
|
||||
numberOfUsers++
|
||||
// a new user was found
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
currentUser = user
|
||||
encoder.writeVarUint(user)
|
||||
// pseudo-fill pos
|
||||
lastLenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
currentLength = 0
|
||||
}
|
||||
encoder.writeVarUint(clock)
|
||||
encoder.writeVarUint(len)
|
||||
encoder.writeUint8(gc ? 1 : 0)
|
||||
currentLength++
|
||||
})
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
||||
}
|
||||
|
||||
export function readDeleteSet (y, decoder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let dv = []
|
||||
let dvLength = decoder.readUint32()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
dv.push([from, len, gc])
|
||||
}
|
||||
if (dvLength > 0) {
|
||||
let pos = 0
|
||||
let d = dv[pos]
|
||||
let deletions = []
|
||||
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (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[0]) {
|
||||
// 1)
|
||||
break
|
||||
} else if (d[0] < n._id.clock) {
|
||||
// 2)
|
||||
// delete maximum the len of d
|
||||
// else delete as much as possible
|
||||
diff = Math.min(n._id.clock - d[0], d[1])
|
||||
// deleteItemRange(y, user, d[0], diff, true)
|
||||
deletions.push([user, d[0], diff])
|
||||
} else {
|
||||
// 3)
|
||||
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
||||
if (d[2] && !n.gc) {
|
||||
// d marks as gc'd but n does not
|
||||
// then delete either way
|
||||
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]), true)
|
||||
deletions.push([user, d[0], Math.min(diff, d[1])])
|
||||
}
|
||||
}
|
||||
if (d[1] <= diff) {
|
||||
// d doesn't delete anything anymore
|
||||
d = dv[++pos]
|
||||
} else {
|
||||
d[0] = d[0] + diff // reset pos
|
||||
d[1] = d[1] - 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[0], d[1], true)
|
||||
// deletions.push([user, d[0], d[1], d[2]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.js'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
||||
import { stringifySyncStep1 } from './syncStep1.js'
|
||||
import { stringifySyncStep2 } from './syncStep2.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import RootID from '../Util/ID/RootID.js'
|
||||
import Y from '../Y.js'
|
||||
|
||||
export function messageToString ([y, buffer]) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // read roomname
|
||||
let type = decoder.readVarString()
|
||||
let strBuilder = []
|
||||
strBuilder.push('\n === ' + type + ' ===')
|
||||
if (type === 'update') {
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 1') {
|
||||
stringifySyncStep1(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 2') {
|
||||
stringifySyncStep2(y, decoder, strBuilder)
|
||||
} else {
|
||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||
}
|
||||
return strBuilder.join('\n')
|
||||
}
|
||||
|
||||
export function messageToRoomname (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // roomname
|
||||
return decoder.readVarString() // messageType
|
||||
}
|
||||
|
||||
export function logID (id) {
|
||||
if (id !== null && id._id != null) {
|
||||
id = id._id
|
||||
}
|
||||
if (id === null) {
|
||||
return '()'
|
||||
} else if (id instanceof ID) {
|
||||
return `(${id.user},${id.clock})`
|
||||
} else if (id instanceof RootID) {
|
||||
return `(${id.name},${id.type})`
|
||||
} else if (id.constructor === Y) {
|
||||
return `y`
|
||||
} else {
|
||||
throw new Error('This is not a valid ID!')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||
* @param {Item} item The item instance.
|
||||
* @param {String} [append] Additional information to append to the returned
|
||||
* string.
|
||||
* @return {String} A readable string that represents the item object.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function logItemHelper (name, item, append) {
|
||||
const left = item._left !== null ? item._left._lastId : null
|
||||
const origin = item._origin !== null ? item._origin._lastId : null
|
||||
return `${name}(id:${logID(item._id)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
|
||||
export function readStateSet (decoder) {
|
||||
let ss = new Map()
|
||||
let ssLength = decoder.readUint32()
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ss.set(user, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
export function writeStateSet (y, encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0)
|
||||
for (let [user, clock] of y.ss.state) {
|
||||
encoder.writeVarUint(user)
|
||||
encoder.writeVarUint(clock)
|
||||
len++
|
||||
}
|
||||
encoder.setUint32(lenPosition, len)
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.js'
|
||||
import { readStateSet, writeStateSet } from './stateSet.js'
|
||||
import { writeDeleteSet } from './deleteSet.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.js'
|
||||
|
||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
strBuilder.push(` - auth: "${auth}"`)
|
||||
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
||||
// write SS
|
||||
let ssBuilder = []
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ssBuilder.push(`(${user}:${clock})`)
|
||||
}
|
||||
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
||||
}
|
||||
|
||||
export function sendSyncStep1 (connector, syncUser) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(connector.y.room)
|
||||
encoder.writeVarString('sync step 1')
|
||||
encoder.writeVarString(connector.authInfo || '')
|
||||
encoder.writeVarUint(connector.protocolVersion)
|
||||
writeStateSet(connector.y, encoder)
|
||||
connector.send(syncUser, encoder.createBuffer())
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Write all Items that are not not included in ss to
|
||||
* the encoder object.
|
||||
*/
|
||||
export function writeStructs (y, encoder, ss) {
|
||||
const lenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
let len = 0
|
||||
for (let user of y.ss.state.keys()) {
|
||||
let clock = ss.get(user) || 0
|
||||
if (user !== RootFakeUserID) {
|
||||
const minBound = new ID(user, clock)
|
||||
const overlappingLeft = y.os.findPrev(minBound)
|
||||
const rightID = overlappingLeft === null ? null : overlappingLeft._id
|
||||
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
|
||||
const struct = overlappingLeft._clonePartial(clock - rightID.clock)
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
}
|
||||
y.os.iterate(minBound, new ID(user, Number.MAX_VALUE), function (struct) {
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
})
|
||||
}
|
||||
}
|
||||
encoder.setUint32(lenPos, len)
|
||||
}
|
||||
|
||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
// check protocol version
|
||||
if (protocolVersion !== y.connector.protocolVersion) {
|
||||
console.warn(
|
||||
`You tried to sync with a Yjs instance that has a different protocol version
|
||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||
`)
|
||||
y.destroy()
|
||||
}
|
||||
// write sync step 2
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(y.connector.authInfo || '')
|
||||
const ss = readStateSet(decoder)
|
||||
writeStructs(y, encoder, ss)
|
||||
writeDeleteSet(y, encoder)
|
||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
if (y.connector.role === 'slave') {
|
||||
sendSyncStep1(y.connector, sender)
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet } from './deleteSet.js'
|
||||
|
||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
||||
strBuilder.push(' == OS:')
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
// write DS to string
|
||||
strBuilder.push(' == DS:')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(` User: ${user}: `)
|
||||
let len2 = decoder.readUint32()
|
||||
for (let j = 0; j < len2; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let to = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
y.connector._setSyncedWith(sender)
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.js'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.js'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
||||
|
||||
function createFilePath (persistence, roomName) {
|
||||
@ -23,9 +23,9 @@ export default class FilePersistence {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mutex(() => {
|
||||
const filePath = createFilePath(this, room)
|
||||
const updateMessage = new BinaryEncoder()
|
||||
const updateMessage = encoding.createEncoder()
|
||||
encodeUpdate(y, encodedStructs, updateMessage)
|
||||
fs.appendFile(filePath, Buffer.from(updateMessage.createBuffer()), (err) => {
|
||||
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
@ -37,10 +37,10 @@ export default class FilePersistence {
|
||||
}
|
||||
saveState (roomName, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const encoder = new BinaryEncoder()
|
||||
const encoder = encoding.createEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
const filePath = createFilePath(this, roomName)
|
||||
fs.writeFile(filePath, Buffer.from(encoder.createBuffer()), (err) => {
|
||||
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
@ -61,7 +61,7 @@ export default class FilePersistence {
|
||||
this._mutex(() => {
|
||||
console.info(`unpacking data (${data.length})`)
|
||||
console.time('unpacking')
|
||||
decodePersisted(y, new BinaryDecoder(data))
|
||||
decodePersisted(y, decoding.createDecoder(data.buffer))
|
||||
console.timeEnd('unpacking')
|
||||
})
|
||||
resolve()
|
||||
|
@ -1,12 +1,10 @@
|
||||
/* global indexedDB, location, BroadcastChannel */
|
||||
|
||||
import Y from '../Y.js'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.js'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.js'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.js'
|
||||
import { PERSIST_STRUCTS_DS } from './decodePersisted.js';
|
||||
import { PERSIST_UPDATE } from './decodePersisted.js';
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
/*
|
||||
* Request to Promise transformer
|
||||
*/
|
||||
|
@ -39,10 +39,10 @@ export function decodePersisted (y, decoder) {
|
||||
const contentType = decoder.readVarUint()
|
||||
switch (contentType) {
|
||||
case PERSIST_UPDATE:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
integrateRemoteStructs(decoder, y)
|
||||
break
|
||||
case PERSIST_STRUCTS_DS:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
integrateRemoteStructs(decoder, y)
|
||||
readDeleteSet(y, decoder)
|
||||
break
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
import Tree from '../../lib/Tree.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
@ -33,7 +33,7 @@ export default class DeleteStore extends Tree {
|
||||
mark (id, length, gc) {
|
||||
if (length === 0) return
|
||||
// Step 1. Unmark range
|
||||
const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1))
|
||||
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
|
||||
// Resize left DSNode if necessary
|
||||
if (leftD !== null && leftD._id.user === id.user) {
|
||||
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
|
||||
@ -41,19 +41,19 @@ export default class DeleteStore extends Tree {
|
||||
if (id.clock + length < leftD._id.clock + leftD.len) {
|
||||
// overlaps new mark range and some more
|
||||
// create another DSNode to the right of new mark
|
||||
this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
||||
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
||||
}
|
||||
// resize left DSNode
|
||||
leftD.len = id.clock - leftD._id.clock
|
||||
} // Otherwise there is no overlapping
|
||||
}
|
||||
// Resize right DSNode if necessary
|
||||
const upper = new ID(id.user, id.clock + length - 1)
|
||||
const upper = ID.createID(id.user, id.clock + length - 1)
|
||||
const rightD = this.findWithUpperBound(upper)
|
||||
if (rightD !== null && rightD._id.user === id.user) {
|
||||
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
|
||||
const d = id.clock + length - rightD._id.clock
|
||||
rightD._id = new ID(rightD._id.user, rightD._id.clock + d)
|
||||
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
|
||||
rightD.len -= d
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,7 @@ export default class DeleteStore extends Tree {
|
||||
leftD.len += length
|
||||
newMark = leftD
|
||||
}
|
||||
const rightNext = this.find(new ID(id.user, id.clock + length))
|
||||
const rightNext = this.find(ID.createID(id.user, id.clock + length))
|
||||
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
|
||||
// We can merge newMark and rightNext
|
||||
newMark.len += rightNext.len
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Tree from '../../lib/Tree.js'
|
||||
import RootID from '../Util/ID/RootID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import { stringifyID, stringifyItemID } from '../message.js'
|
||||
import GC from '../Struct/GC.js'
|
||||
|
||||
export default class OperationStore extends Tree {
|
||||
@ -14,18 +14,18 @@ export default class OperationStore extends Tree {
|
||||
this.iterate(null, null, function (item) {
|
||||
if (item.constructor === GC) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
id: stringifyItemID(item),
|
||||
content: item._length,
|
||||
deleted: 'GC'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
id: stringifyItemID(item),
|
||||
origin: item._origin === null ? '()' : stringifyID(item._origin._lastId),
|
||||
left: item._left === null ? '()' : stringifyID(item._left._lastId),
|
||||
right: stringifyItemID(item._right),
|
||||
right_origin: stringifyItemID(item._right_origin),
|
||||
parent: stringifyItemID(item._parent),
|
||||
parentSub: item._parentSub,
|
||||
deleted: item._deleted,
|
||||
content: JSON.stringify(item._content)
|
||||
@ -36,7 +36,7 @@ export default class OperationStore extends Tree {
|
||||
}
|
||||
get (id) {
|
||||
let struct = this.find(id)
|
||||
if (struct === null && id instanceof RootID) {
|
||||
if (struct === null && id instanceof ID.RootID) {
|
||||
const Constr = getStruct(id.type)
|
||||
const y = this.y
|
||||
struct = new Constr()
|
||||
|
@ -1,4 +1,8 @@
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
|
||||
/**
|
||||
* @typedef {Map<number, number>} StateSet
|
||||
*/
|
||||
|
||||
export default class StateStore {
|
||||
constructor (y) {
|
||||
@ -18,14 +22,14 @@ export default class StateStore {
|
||||
const user = this.y.userID
|
||||
const state = this.getState(user)
|
||||
this.setState(user, state + len)
|
||||
return new ID(user, state)
|
||||
return ID.createID(user, state)
|
||||
}
|
||||
updateRemoteState (struct) {
|
||||
let user = struct._id.user
|
||||
let userState = this.state.get(user)
|
||||
while (struct !== null && struct._id.clock === userState) {
|
||||
userState += struct._length
|
||||
struct = this.y.os.get(new ID(user, userState))
|
||||
struct = this.y.os.get(ID.createID(user, userState))
|
||||
}
|
||||
this.state.set(user, userState)
|
||||
}
|
||||
|
@ -1,31 +1,33 @@
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { stringifyID } from '../message.js'
|
||||
import { writeStructToTransaction } from '../Util/Transaction.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Delete all items in an ID-range
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
* Delete all items in an ID-range.
|
||||
* Does not create delete operations!
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range, gcChildren) {
|
||||
const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
let item = y.os.getItemCleanStart(ID.createID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete, true)
|
||||
item._delete(y, false, true)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
let node = y.os.findNode(ID.createID(user, clock))
|
||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete, gcChildren)
|
||||
nodeVal._delete(y, false, gcChildren)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
@ -44,6 +46,13 @@ export function deleteItemRange (y, user, clock, range, gcChildren) {
|
||||
*/
|
||||
export default class Delete {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ID.ID}
|
||||
*/
|
||||
this._targetID = null
|
||||
/**
|
||||
* @type {import('./Item.js').default}
|
||||
*/
|
||||
this._target = null
|
||||
this._length = null
|
||||
}
|
||||
@ -54,15 +63,18 @@ export default class Delete {
|
||||
*
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const targetID = ID.decode(decoder)
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
this._length = decoding.readVarUint(decoder)
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
@ -77,12 +89,12 @@ export default class Delete {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
this._targetID.encode(encoder)
|
||||
encoding.writeVarUint(encoder, this._length)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,12 +114,6 @@ export default class Delete {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length, false)
|
||||
} else if (y.connector !== null) {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
@ -119,6 +125,6 @@ export default class Delete {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { writeStructToTransaction } from '../Util/Transaction.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
|
||||
// TODO should have the same base class as Item
|
||||
export default class GC {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ID.ID}
|
||||
*/
|
||||
this._id = null
|
||||
this._length = 0
|
||||
}
|
||||
@ -37,13 +41,7 @@ export default class GC {
|
||||
n._length += next._length
|
||||
y.os.delete(next._id)
|
||||
}
|
||||
if (id.user !== RootFakeUserID) {
|
||||
if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
if (id.user !== ID.RootFakeUserID) {
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
}
|
||||
@ -54,13 +52,13 @@ export default class GC {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoder.writeID(this._id)
|
||||
encoder.writeVarUint(this._length)
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
this._id.encode(encoder)
|
||||
encoding.writeVarUint(encoder, this._length)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,17 +66,20 @@ export default class GC {
|
||||
*
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const id = decoder.readID()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const id = ID.decode(decoder)
|
||||
this._id = id
|
||||
this._length = decoder.readVarUint()
|
||||
this._length = decoding.readVarUint(decoder)
|
||||
const missing = []
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
missing.push(ID.createID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
@ -89,7 +90,7 @@ export default class GC {
|
||||
|
||||
_clonePartial (diff) {
|
||||
const gc = new GC()
|
||||
gc._id = new ID(this._id.user, this._id.clock + diff)
|
||||
gc._id = ID.createID(this._id.user, this._id.clock + diff)
|
||||
gc._length = this._length - diff
|
||||
return gc
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import Delete from './Delete.js'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../Util/Transaction.js'
|
||||
import GC from './GC.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import Y from '../Y.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./Type.js').default} YType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @private
|
||||
@ -15,7 +21,7 @@ import GC from './GC.js'
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
const aID = a._id
|
||||
b._id = new ID(aID.user, aID.clock + diff)
|
||||
b._id = ID.createID(aID.user, aID.clock + diff)
|
||||
b._origin = a
|
||||
b._left = a
|
||||
b._right = a._right
|
||||
@ -55,7 +61,7 @@ export default class Item {
|
||||
constructor () {
|
||||
/**
|
||||
* The uniqe identifier of this type.
|
||||
* @type {ID}
|
||||
* @type {ID.ID | ID.RootID}
|
||||
*/
|
||||
this._id = null
|
||||
/**
|
||||
@ -99,7 +105,7 @@ export default class Item {
|
||||
/**
|
||||
* If this type's effect is reundone this type refers to the type that undid
|
||||
* this operation.
|
||||
* @type {Item}
|
||||
* @type {YType}
|
||||
*/
|
||||
this._redone = null
|
||||
}
|
||||
@ -110,7 +116,8 @@ export default class Item {
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
return new this.constructor()
|
||||
const C = this.constructor
|
||||
return C()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,6 +131,9 @@ export default class Item {
|
||||
if (this._redone !== null) {
|
||||
return this._redone
|
||||
}
|
||||
if (this._parent instanceof Y) {
|
||||
return
|
||||
}
|
||||
let struct = this._copy()
|
||||
let left, right
|
||||
if (this._parentSub === null) {
|
||||
@ -146,7 +156,7 @@ export default class Item {
|
||||
}
|
||||
if (parent._redone !== null) {
|
||||
parent = parent._redone
|
||||
// find next cloned items
|
||||
// find next cloned_redo items
|
||||
while (left !== null) {
|
||||
if (left._redone !== null && left._redone._parent === parent) {
|
||||
left = left._redone
|
||||
@ -178,7 +188,11 @@ export default class Item {
|
||||
* @private
|
||||
*/
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const id = this._id
|
||||
return ID.createID(id.user, id.clock + this._length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -227,10 +241,11 @@ export default class Item {
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} gcChildren
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_delete (y, createDelete = true) {
|
||||
_delete (y, createDelete = true, gcChildren) {
|
||||
if (!this._deleted) {
|
||||
this._deleted = true
|
||||
y.ds.mark(this._id, this._length, false)
|
||||
@ -240,9 +255,6 @@ export default class Item {
|
||||
if (createDelete) {
|
||||
// broadcast and persists Delete
|
||||
del._integrate(y, true)
|
||||
} else if (y.persistence !== null) {
|
||||
// only persist Delete
|
||||
y.persistence.saveStruct(y, del)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
@ -280,21 +292,30 @@ export default class Item {
|
||||
* * Add this struct to y.os
|
||||
* * Check if this is struct deleted
|
||||
*
|
||||
* @param {Y} y
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_integrate (y) {
|
||||
y._transaction.newTypes.add(this)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const parent = this._parent
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const selfID = this._id
|
||||
const user = selfID === null ? y.userID : selfID.user
|
||||
const userState = y.ss.getState(user)
|
||||
if (selfID === null) {
|
||||
this._id = y.ss.getNextID(this._length)
|
||||
} else if (selfID.user === RootFakeUserID) {
|
||||
// nop
|
||||
} else if (selfID.user === ID.RootFakeUserID) {
|
||||
// is parent
|
||||
return
|
||||
} else if (selfID.clock < userState) {
|
||||
// already applied..
|
||||
return []
|
||||
return
|
||||
} else if (selfID.clock === userState) {
|
||||
y.ss.setState(selfID.user, userState + this._length)
|
||||
} else {
|
||||
@ -304,7 +325,7 @@ export default class Item {
|
||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
||||
// this is the first time parent is updated
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
parent._beforeChange()
|
||||
}
|
||||
|
||||
/*
|
||||
@ -328,9 +349,9 @@ export default class Item {
|
||||
if (this._left !== null) {
|
||||
o = this._left._right
|
||||
} else if (this._parentSub !== null) {
|
||||
o = this._parent._map.get(this._parentSub) || null
|
||||
o = parent._map.get(this._parentSub) || null
|
||||
} else {
|
||||
o = this._parent._start
|
||||
o = parent._start
|
||||
}
|
||||
let conflictingItems = new Set()
|
||||
let itemsBeforeOrigin = new Set()
|
||||
@ -386,17 +407,11 @@ export default class Item {
|
||||
}
|
||||
}
|
||||
if (parent._deleted) {
|
||||
this._delete(y, false)
|
||||
this._delete(y, false, true)
|
||||
}
|
||||
y.os.put(this)
|
||||
transactionTypeChanged(y, parent, parentSub)
|
||||
if (this._id.user !== RootFakeUserID) {
|
||||
if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
if (this._id.user !== ID.RootFakeUserID) {
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
}
|
||||
@ -407,12 +422,12 @@ export default class Item {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
let info = 0
|
||||
if (this._origin !== null) {
|
||||
info += 0b1 // origin is defined
|
||||
@ -429,10 +444,10 @@ export default class Item {
|
||||
if (this._parentSub !== null) {
|
||||
info += 0b1000
|
||||
}
|
||||
encoder.writeUint8(info)
|
||||
encoder.writeID(this._id)
|
||||
encoding.writeUint8(encoder, info)
|
||||
this._id.encode(encoder)
|
||||
if (info & 0b1) {
|
||||
encoder.writeID(this._origin._lastId)
|
||||
this._origin._lastId.encode(encoder)
|
||||
}
|
||||
// TODO: remove
|
||||
/* see above
|
||||
@ -441,14 +456,14 @@ export default class Item {
|
||||
}
|
||||
*/
|
||||
if (info & 0b100) {
|
||||
encoder.writeID(this._right_origin._id)
|
||||
this._right_origin._id.encode(encoder)
|
||||
}
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
encoder.writeID(this._parent._id)
|
||||
this._parent._id.encode(encoder)
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
||||
encoding.writeVarString(encoder, JSON.stringify(this._parentSub))
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,19 +473,19 @@ export default class Item {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
const id = decoder.readID()
|
||||
const info = decoding.readUint8(decoder)
|
||||
const id = ID.decode(decoder)
|
||||
this._id = id
|
||||
// read origin
|
||||
if (info & 0b1) {
|
||||
// origin != null
|
||||
const originID = decoder.readID()
|
||||
const originID = ID.decode(decoder)
|
||||
// we have to query for left again because it might have been split/merged..
|
||||
const origin = y.os.getItemCleanEnd(originID)
|
||||
if (origin === null) {
|
||||
@ -483,7 +498,7 @@ export default class Item {
|
||||
// read right
|
||||
if (info & 0b100) {
|
||||
// right != null
|
||||
const rightID = decoder.readID()
|
||||
const rightID = ID.decode(decoder)
|
||||
// we have to query for right again because it might have been split/merged..
|
||||
const right = y.os.getItemCleanStart(rightID)
|
||||
if (right === null) {
|
||||
@ -496,11 +511,11 @@ export default class Item {
|
||||
// read parent
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
const parentID = decoder.readID()
|
||||
const parentID = ID.decode(decoder)
|
||||
// parent does not change, so we don't have to search for it again
|
||||
if (this._parent === null) {
|
||||
let parent
|
||||
if (parentID.constructor === RootID) {
|
||||
if (parentID.constructor === ID.RootID) {
|
||||
parent = y.os.get(parentID)
|
||||
} else {
|
||||
parent = y.os.getItem(parentID)
|
||||
@ -513,27 +528,17 @@ export default class Item {
|
||||
}
|
||||
} else if (this._parent === null) {
|
||||
if (this._origin !== null) {
|
||||
if (this._origin.constructor === GC) {
|
||||
// if origin is a gc, set parent also gc'd
|
||||
this._parent = this._origin
|
||||
} else {
|
||||
this._parent = this._origin._parent
|
||||
}
|
||||
this._parent = this._origin._parent
|
||||
} else if (this._right_origin !== null) {
|
||||
// if origin is a gc, set parent also gc'd
|
||||
if (this._right_origin.constructor === GC) {
|
||||
this._parent = this._right_origin
|
||||
} else {
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
||||
this._parentSub = JSON.parse(decoder.readVarString())
|
||||
this._parentSub = JSON.parse(decoding.readVarString(decoder))
|
||||
}
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
if (id instanceof ID.ID && y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(ID.createID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
import Item from './Item.js'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemEmbed extends Item {
|
||||
constructor () {
|
||||
@ -7,21 +13,28 @@ export default class ItemEmbed extends Item {
|
||||
this.embed = null
|
||||
}
|
||||
_copy (undeleteChildren, copyPosition) {
|
||||
let struct = super._copy(undeleteChildren, copyPosition)
|
||||
let struct = super._copy()
|
||||
struct.embed = this.embed
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.embed = JSON.parse(decoder.readVarString())
|
||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(JSON.stringify(this.embed))
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
|
@ -1,5 +1,11 @@
|
||||
import Item from './Item.js'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemFormat extends Item {
|
||||
constructor () {
|
||||
@ -8,7 +14,7 @@ export default class ItemFormat extends Item {
|
||||
this.value = null
|
||||
}
|
||||
_copy (undeleteChildren, copyPosition) {
|
||||
let struct = super._copy(undeleteChildren, copyPosition)
|
||||
let struct = super._copy()
|
||||
struct.key = this.key
|
||||
struct.value = this.value
|
||||
return struct
|
||||
@ -19,16 +25,23 @@ export default class ItemFormat extends Item {
|
||||
get _countable () {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.key = decoder.readVarString()
|
||||
this.value = JSON.parse(decoder.readVarString())
|
||||
this.key = decoding.readVarString(decoder)
|
||||
this.value = JSON.parse(decoding.readVarString(decoder))
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.key)
|
||||
encoder.writeVarString(JSON.stringify(this.value))
|
||||
encoding.writeVarString(encoder, this.key)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
|
@ -1,5 +1,11 @@
|
||||
import Item, { splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemJSON extends Item {
|
||||
constructor () {
|
||||
@ -14,12 +20,16 @@ export default class ItemJSON extends Item {
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
let len = decoder.readVarUint()
|
||||
let len = decoding.readVarUint(decoder)
|
||||
this._content = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ctnt = decoder.readVarString()
|
||||
const ctnt = decoding.readVarString(decoder)
|
||||
let parsed
|
||||
if (ctnt === 'undefined') {
|
||||
parsed = undefined
|
||||
@ -30,10 +40,13 @@ export default class ItemJSON extends Item {
|
||||
}
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoder.writeVarUint(len)
|
||||
encoding.writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let content = this._content[i]
|
||||
@ -42,7 +55,7 @@ export default class ItemJSON extends Item {
|
||||
} else {
|
||||
encoded = JSON.stringify(content)
|
||||
}
|
||||
encoder.writeVarString(encoded)
|
||||
encoding.writeVarString(encoder, encoded)
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -1,5 +1,11 @@
|
||||
import Item, { splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemString extends Item {
|
||||
constructor () {
|
||||
@ -14,14 +20,21 @@ export default class ItemString extends Item {
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
this._content = decoder.readVarString()
|
||||
this._content = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this._content)
|
||||
encoding.writeVarString(encoder, this._content)
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
|
@ -1,6 +1,11 @@
|
||||
import Item from './Item.js'
|
||||
import EventHandler from '../Util/EventHandler.js'
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import { createID } from '../Util/ID.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import("../Y.js").default} Y
|
||||
*/
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
function integrateChildren (y, start) {
|
||||
@ -22,7 +27,7 @@ export function getListItemIDByPosition (type, i) {
|
||||
if (!n._deleted) {
|
||||
if (pos <= i && i < pos + n._length) {
|
||||
const id = n._id
|
||||
return new ID(id.user, id.clock + i - pos)
|
||||
return createID(id.user, id.clock + i - pos)
|
||||
}
|
||||
pos++
|
||||
}
|
||||
@ -61,7 +66,7 @@ export default class Type extends Item {
|
||||
* console.log(path) // might look like => [2, 'key1']
|
||||
* child === type.get(path[0]).get(path[1])
|
||||
*
|
||||
* @param {YType} type Type target
|
||||
* @param {Type | Y | any} type Type target
|
||||
* @return {Array<string>} Path to the target
|
||||
*/
|
||||
getPathTo (type) {
|
||||
@ -91,6 +96,14 @@ export default class Type extends Item {
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates YArray Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YEvent(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call event listeners with an event. This will also add an event to all
|
||||
@ -99,6 +112,9 @@ export default class Type extends Item {
|
||||
_callEventHandler (transaction, event) {
|
||||
const changedParentTypes = transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(transaction, event)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let type = this
|
||||
while (type !== this._y) {
|
||||
let events = changedParentTypes.get(type)
|
||||
@ -183,7 +199,7 @@ export default class Type extends Item {
|
||||
this._start = null
|
||||
integrateChildren(y, start)
|
||||
}
|
||||
// integrate map children
|
||||
// integrate map children_integrate
|
||||
const map = this._map
|
||||
this._map = new Map()
|
||||
for (let t of map.values()) {
|
||||
@ -206,6 +222,12 @@ export default class Type extends Item {
|
||||
super._gc(y)
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {Object | Array | number | string}
|
||||
*/
|
||||
toJSON () {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Mark this Item as deleted.
|
||||
@ -213,7 +235,7 @@ export default class Type extends Item {
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
|
||||
* @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage
|
||||
* collect the children of this type.
|
||||
*/
|
||||
_delete (y, createDelete, gcChildren) {
|
||||
|
@ -1,9 +1,15 @@
|
||||
import Type from '../../Struct/Type.js'
|
||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||
import ItemString from '../../Struct/ItemString.js'
|
||||
import { logID, logItemHelper } from '../../MessageHandler/messageToString.js'
|
||||
import { stringifyItemID, logItemHelper } from '../../message.js'
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Struct/Item.js').default} Item
|
||||
* @typedef {import('../../Util/Transaction.js').default} Transaction
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YArray
|
||||
*
|
||||
@ -76,7 +82,7 @@ export default class YArray extends Type {
|
||||
/**
|
||||
* Returns the i-th element from a YArray.
|
||||
*
|
||||
* @param {Integer} index The index of the element to return from the YArray
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
*/
|
||||
get (index) {
|
||||
let n = this._start
|
||||
@ -112,11 +118,7 @@ export default class YArray extends Type {
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
if (c.toJSON !== null) {
|
||||
return c.toJSON()
|
||||
} else {
|
||||
return c.toString()
|
||||
}
|
||||
return c.toJSON()
|
||||
}
|
||||
return c
|
||||
})
|
||||
@ -211,8 +213,8 @@ export default class YArray extends Type {
|
||||
/**
|
||||
* Deletes elements starting from an index.
|
||||
*
|
||||
* @param {Integer} index Index at which to start deleting elements
|
||||
* @param {Integer} length The number of elements to remove. Defaults to 1.
|
||||
* @param {number} index Index at which to start deleting elements
|
||||
* @param {number} length The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
this._y.transact(() => {
|
||||
@ -318,7 +320,7 @@ export default class YArray extends Type {
|
||||
* // Insert numbers 1, 2 at position 1
|
||||
* yarray.insert(2, [1, 2])
|
||||
*
|
||||
* @param {Integer} index The index to insert content at.
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {Array} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
@ -373,6 +375,6 @@ export default class YArray extends Type {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('YArray', this, `start:${logID(this._start)}"`)
|
||||
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import Item from '../../Struct/Item.js'
|
||||
import Type from '../../Struct/Type.js'
|
||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Y.js').encodable} encodable
|
||||
* @typedef {import('../../Struct/Type.js')} YType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YMap.
|
||||
*
|
||||
|
@ -1,7 +1,7 @@
|
||||
import ItemEmbed from '../../Struct/ItemEmbed.js'
|
||||
import ItemString from '../../Struct/ItemString.js'
|
||||
import ItemFormat from '../../Struct/ItemFormat.js'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
import { YArrayEvent, default as YArray } from '../YArray/YArray.js'
|
||||
|
||||
/**
|
||||
@ -304,6 +304,9 @@ class YTextEvent extends YArrayEvent {
|
||||
let deleteLen = 0
|
||||
const addOp = function addOp () {
|
||||
if (action !== null) {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let op
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
@ -483,6 +486,9 @@ export default class YText extends YArray {
|
||||
*/
|
||||
toString () {
|
||||
let str = ''
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
@ -529,6 +535,9 @@ export default class YText extends YArray {
|
||||
let ops = []
|
||||
let currentAttributes = new Map()
|
||||
let str = ''
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let n = this._start
|
||||
function packStr () {
|
||||
if (str.length > 0) {
|
||||
@ -568,12 +577,11 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Insert text at a given index.
|
||||
*
|
||||
* @param {Integer} index The index at which to start inserting.
|
||||
* @param {number} index The index at which to start inserting.
|
||||
* @param {String} text The text to insert at the specified position.
|
||||
* @param {TextAttributes} attributes Optionally define some formatting
|
||||
* information to apply on the inserted
|
||||
* Text.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
insert (index, text, attributes = {}) {
|
||||
@ -589,7 +597,7 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Inserts an embed at a index.
|
||||
*
|
||||
* @param {Integer} index The index to insert the embed at.
|
||||
* @param {number} index The index to insert the embed at.
|
||||
* @param {Object} embed The Object that represents the embed.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* embed
|
||||
@ -609,8 +617,8 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Deletes text starting from an index.
|
||||
*
|
||||
* @param {Integer} index Index at which to start deleting.
|
||||
* @param {Integer} length The number of characters to remove. Defaults to 1.
|
||||
* @param {number} index Index at which to start deleting.
|
||||
* @param {number} length The number of characters to remove. Defaults to 1.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@ -627,8 +635,8 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Assigns properties to a range of text.
|
||||
*
|
||||
* @param {Integer} index The position where to start formatting.
|
||||
* @param {Integer} length The amount of characters to assign properties to.
|
||||
* @param {number} index The position where to start formatting.
|
||||
* @param {number} length The amount of characters to assign properties to.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* text.
|
||||
*
|
||||
|
@ -1,6 +1,12 @@
|
||||
import YMap from '../YMap/YMap.js'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
import * as encoding from '../../../lib/encoding.js'
|
||||
import * as decoding from '../../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* An YXmlElement imitates the behavior of a
|
||||
@ -8,8 +14,6 @@ import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
*
|
||||
* * An YXmlElement has attributes (key value pairs)
|
||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||
*
|
||||
* @param {String} nodeName Node name
|
||||
*/
|
||||
export default class YXmlElement extends YXmlFragment {
|
||||
constructor (nodeName = 'UNDEFINED') {
|
||||
@ -34,11 +38,11 @@ export default class YXmlElement extends YXmlFragment {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.nodeName = decoder.readVarString()
|
||||
this.nodeName = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
|
||||
@ -48,13 +52,13 @@ export default class YXmlElement extends YXmlFragment {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.nodeName)
|
||||
encoding.writeVarString(encoder, this.nodeName)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,9 +168,9 @@ export default class YXmlElement extends YXmlFragment {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
|
||||
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* @param {import('../../Bindings/DomBinding/DomBinding.js').default} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
* association to the created DOM type.
|
||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
@ -187,4 +191,12 @@ export default class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
}
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
// reassign yxmlfragment to {any} type to prevent warnings
|
||||
// assign yxmlelement to YXmlFragment so it has a reference to YXmlElement.
|
||||
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const _reasgn = YXmlFragment
|
||||
|
||||
_reasgn._YXmlElement = YXmlElement
|
||||
|
@ -1,5 +1,10 @@
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Struct/Type.js').default} YType
|
||||
* @typedef {import('../../Util/Transaction.js').default} Transaction
|
||||
*/
|
||||
|
||||
/**
|
||||
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||
*
|
||||
|
@ -3,7 +3,13 @@ import YXmlTreeWalker from './YXmlTreeWalker.js'
|
||||
|
||||
import YArray from '../YArray/YArray.js'
|
||||
import YXmlEvent from './YXmlEvent.js'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./YXmlElement.js').default} YXmlElement
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dom filter function.
|
||||
@ -48,7 +54,7 @@ export default class YXmlFragment extends YArray {
|
||||
* @param {Function} filter Function that is called on each child element and
|
||||
* returns a Boolean indicating whether the child
|
||||
* is to be included in the subtree.
|
||||
* @return {TreeWalker} A subtree and a position within it.
|
||||
* @return {YXmlTreeWalker} A subtree and a position within it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@ -67,7 +73,7 @@ export default class YXmlFragment extends YArray {
|
||||
* - attribute
|
||||
*
|
||||
* @param {CSS_Selector} query The query on the children.
|
||||
* @return {?YXmlElement} The first element that matches the query or null.
|
||||
* @return {?import('./YXmlElement.js')} The first element that matches the query or null.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@ -116,29 +122,13 @@ export default class YXmlFragment extends YArray {
|
||||
return this.map(xml => xml.toString()).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Unbind from Dom and mark this Item as deleted.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
|
||||
* collect the children of this type.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_delete (y, createDelete, gcChildren) {
|
||||
super._delete(y, createDelete, gcChildren)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Dom Element that mirrors this YXmlElement.
|
||||
*
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
|
||||
* @param {Object.<string, any>} [hooks={}] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
|
@ -1,5 +1,12 @@
|
||||
import YMap from '../YMap/YMap.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
import * as encoding from '../../../lib/encoding.js'
|
||||
import * as decoding from '../../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* You can manage binding to a custom type with YXmlHook.
|
||||
@ -35,7 +42,7 @@ export default class YXmlHook extends YMap {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
|
||||
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
@ -63,13 +70,13 @@ export default class YXmlHook extends YMap {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.hookName = decoder.readVarString()
|
||||
this.hookName = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
|
||||
@ -79,13 +86,13 @@ export default class YXmlHook extends YMap {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.hookName)
|
||||
encoding.writeVarString(encoder, this.hookName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,11 @@
|
||||
import YText from '../YText/YText.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../index.js').Y} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents text in a Dom Element. In the future this type will also handle
|
||||
* simple formatting information like bold and italic.
|
||||
@ -14,12 +19,12 @@ export default class YXmlText extends YText {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
|
||||
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
* association to the created DOM type.
|
||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
@ -33,7 +33,7 @@ export default class YXmlTreeWalker {
|
||||
/**
|
||||
* Get the next node.
|
||||
*
|
||||
* @return {YXmlElement} The next node.
|
||||
* @return {import('./YXmlElement.js').default} The next node.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
90
src/Util/ID.js
Normal file
90
src/Util/ID.js
Normal file
@ -0,0 +1,90 @@
|
||||
import { getStructReference } from './structReferences.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
|
||||
export class ID {
|
||||
constructor (user, clock) {
|
||||
this.user = user // TODO: rename to client
|
||||
this.clock = clock
|
||||
}
|
||||
clone () {
|
||||
return new ID(this.user, this.clock)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.clock === this.clock
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === ID) {
|
||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
encode (encoder) {
|
||||
encoding.writeVarUint(encoder, this.user)
|
||||
encoding.writeVarUint(encoder, this.clock)
|
||||
}
|
||||
}
|
||||
|
||||
export const createID = (user, clock) => new ID(user, clock)
|
||||
|
||||
export const RootFakeUserID = 0xFFFFFF
|
||||
|
||||
export class RootID {
|
||||
constructor (name, typeConstructor) {
|
||||
this.user = RootFakeUserID
|
||||
this.name = name
|
||||
this.type = getStructReference(typeConstructor)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === RootID) {
|
||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
encode (encoder) {
|
||||
encoding.writeVarUint(encoder, this.user)
|
||||
encoding.writeVarString(encoder, this.name)
|
||||
encoding.writeVarUint(encoder, this.type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new root id.
|
||||
*
|
||||
* @example
|
||||
* y.define('name', Y.Array) // name, and typeConstructor
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} typeConstructor must be defined in structReferences
|
||||
*/
|
||||
export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor)
|
||||
|
||||
/**
|
||||
* Read ID.
|
||||
* * If first varUint read is 0xFFFFFF a RootID is returned.
|
||||
* * Otherwise an ID is returned
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ID|RootID}
|
||||
*/
|
||||
export const decode = decoder => {
|
||||
const user = decoding.readVarUint(decoder)
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = createRootID(decoding.readVarString(decoder), null)
|
||||
rid.type = decoding.readVarUint(decoder)
|
||||
return rid
|
||||
}
|
||||
return createID(user, decoding.readVarUint(decoder))
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
|
||||
export default class ID {
|
||||
constructor (user, clock) {
|
||||
this.user = user // TODO: rename to client
|
||||
this.clock = clock
|
||||
}
|
||||
clone () {
|
||||
return new ID(this.user, this.clock)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.clock === this.clock
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === ID) {
|
||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { getStructReference } from '../structReferences.js'
|
||||
|
||||
export const RootFakeUserID = 0xFFFFFF
|
||||
|
||||
export default class RootID {
|
||||
constructor (name, typeConstructor) {
|
||||
this.user = RootFakeUserID
|
||||
this.name = name
|
||||
this.type = getStructReference(typeConstructor)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === RootID) {
|
||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
import BinaryEncoder from './Binary/Encoder.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
/**
|
||||
* @typedef {import("../Y.js").default} Y
|
||||
* @typedef {import("../Struct/Type.js").default} YType
|
||||
* @typedef {import("../Struct/Item.js").default} Item
|
||||
* @typedef {import("./YEvent.js").default} YEvent
|
||||
*/
|
||||
|
||||
/**
|
||||
* A transaction is created for every change on the Yjs model. It is possible
|
||||
@ -26,7 +32,7 @@ import BinaryEncoder from './Binary/Encoder.js'
|
||||
export default class Transaction {
|
||||
constructor (y) {
|
||||
/**
|
||||
* @type {Y} The Yjs instance.
|
||||
* @type {import("../Y.js")} The Yjs instance.
|
||||
*/
|
||||
this.y = y
|
||||
/**
|
||||
@ -38,7 +44,7 @@ export default class Transaction {
|
||||
* All types that were directly modified (property added or child
|
||||
* inserted/deleted). New types are not included in this Set.
|
||||
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
|
||||
* @type {Set<YType,String>}
|
||||
* @type {Map<YType|Y,String>}
|
||||
*/
|
||||
this.changedTypes = new Map()
|
||||
// TODO: rename deletedTypes
|
||||
@ -60,7 +66,7 @@ export default class Transaction {
|
||||
*/
|
||||
this.changedParentTypes = new Map()
|
||||
this.encodedStructsLen = 0
|
||||
this.encodedStructs = new BinaryEncoder()
|
||||
this.encodedStructs = encoding.createEncoder()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ID from './ID/ID.js'
|
||||
import * as ID from './ID.js'
|
||||
import isParentOf from './isParentOf.js'
|
||||
|
||||
class ReverseOperation {
|
||||
@ -6,8 +6,8 @@ class ReverseOperation {
|
||||
this.created = new Date()
|
||||
const beforeState = transaction.beforeState
|
||||
if (beforeState.has(y.userID)) {
|
||||
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
|
||||
this.fromState = new ID(y.userID, beforeState.get(y.userID))
|
||||
this.toState = ID.createID(y.userID, y.ss.getState(y.userID) - 1)
|
||||
this.fromState = ID.createID(y.userID, beforeState.get(y.userID))
|
||||
} else {
|
||||
this.toState = null
|
||||
this.fromState = null
|
||||
@ -28,7 +28,7 @@ class ReverseOperation {
|
||||
|
||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
let performedUndo = false
|
||||
let undoOp
|
||||
let undoOp = null
|
||||
y.transact(() => {
|
||||
while (!performedUndo && reverseBuffer.length > 0) {
|
||||
undoOp = reverseBuffer.pop()
|
||||
@ -49,7 +49,7 @@ function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
const redoitems = new Set()
|
||||
for (let del of undoOp.deletedStructs) {
|
||||
const fromState = del.from
|
||||
const toState = new ID(fromState.user, fromState.clock + del.len - 1)
|
||||
const toState = ID.createID(fromState.user, fromState.clock + del.len - 1)
|
||||
y.os.getItemCleanStart(fromState)
|
||||
y.os.getItemCleanEnd(toState)
|
||||
y.os.iterate(fromState, toState, op => {
|
||||
@ -73,7 +73,7 @@ function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
})
|
||||
}
|
||||
})
|
||||
if (performedUndo) {
|
||||
if (performedUndo && undoOp !== null) {
|
||||
// should be performed after the undo transaction
|
||||
undoOp.bindingInfos.forEach((info, binding) => {
|
||||
binding._restoreUndoStackInfo(info)
|
||||
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @typedef {import("../Y.js").default} Y
|
||||
* @typedef {import("../Struct/Type.js").default} YType
|
||||
* @typedef {import("../Struct/Item.js").default} Item
|
||||
*/
|
||||
|
||||
/**
|
||||
* YEvent describes the changes on a YType.
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
import ID from '../Util/ID/ID.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
|
||||
@ -32,7 +32,7 @@ export function defragmentItemContent (y) {
|
||||
a.constructor === b.constructor &&
|
||||
a._deleted === b._deleted &&
|
||||
a._right === b &&
|
||||
(new ID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
||||
(ID.createID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
||||
) {
|
||||
a._right = b._right
|
||||
if (a instanceof ItemJSON) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* global crypto */
|
||||
|
||||
export function generateRandomUint32 () {
|
||||
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
||||
if (typeof crypto !== 'undefined' && crypto.getRandomValues != null) {
|
||||
// browser
|
||||
let arr = new Uint32Array(1)
|
||||
crypto.getRandomValues(arr)
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.js'
|
||||
import { logID } from './messageToString.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import GC from '../Struct/GC.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index').Y} Y
|
||||
* @typedef {import('../Struct/Item.js').default} YItem
|
||||
*/
|
||||
|
||||
class MissingEntry {
|
||||
constructor (decoder, missing, struct) {
|
||||
this.decoder = decoder
|
||||
@ -16,6 +20,8 @@ class MissingEntry {
|
||||
* Integrate remote struct
|
||||
* When a remote struct is integrated, other structs might be ready to ready to
|
||||
* integrate.
|
||||
* @param {Y} y
|
||||
* @param {YItem} struct
|
||||
*/
|
||||
function _integrateRemoteStructHelper (y, struct) {
|
||||
const id = struct._id
|
||||
@ -57,29 +63,21 @@ function _integrateRemoteStructHelper (y, struct) {
|
||||
msu.delete(clock)
|
||||
}
|
||||
}
|
||||
if (msu.size === 0) {
|
||||
y._missingStructs.delete(id.user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stringifyStructs (y, decoder, strBuilder) {
|
||||
const len = decoder.readUint32()
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export function integrateRemoteStructs (decoder, y) {
|
||||
const len = decoding.readUint32(decoder)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
||||
}
|
||||
strBuilder.push(logMessage)
|
||||
}
|
||||
}
|
||||
|
||||
export function integrateRemoteStructs (y, decoder) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let reference = decoding.readVarUint(decoder)
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let decoderPos = decoder.pos
|
||||
@ -90,7 +88,7 @@ export function integrateRemoteStructs (y, decoder) {
|
||||
struct = y._readyToIntegrate.shift()
|
||||
}
|
||||
} else {
|
||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
||||
let _decoder = decoding.createDecoder(decoder.arr.buffer)
|
||||
_decoder.pos = decoderPos
|
||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
||||
let missingStructs = y._missingStructs
|
||||
@ -111,8 +109,12 @@ export function integrateRemoteStructs (y, decoder) {
|
||||
}
|
||||
|
||||
// TODO: use this above / refactor
|
||||
export function integrateRemoteStruct (y, decoder) {
|
||||
let reference = decoder.readVarUint()
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export function integrateRemoteStruct (decoder, y) {
|
||||
let reference = decoding.readVarUint(decoder)
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let decoderPos = decoder.pos
|
||||
@ -123,7 +125,7 @@ export function integrateRemoteStruct (y, decoder) {
|
||||
struct = y._readyToIntegrate.shift()
|
||||
}
|
||||
} else {
|
||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
||||
let _decoder = decoding.createDecoder(decoder.arr.buffer)
|
||||
_decoder.pos = decoderPos
|
||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
||||
let missingStructs = y._missingStructs
|
@ -1,9 +1,14 @@
|
||||
|
||||
/**
|
||||
* @typedef {import('../Struct/Type.js').default} YType
|
||||
* @typedef {import('../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if `parent` is a parent of `child`.
|
||||
*
|
||||
* @param {Type} parent
|
||||
* @param {Type} child
|
||||
* @param {YType | Y} parent
|
||||
* @param {YType | Y} child
|
||||
* @return {Boolean} Whether `parent` is a parent of `child`.
|
||||
*
|
||||
* @public
|
||||
|
@ -1,5 +1,4 @@
|
||||
import ID from './ID/ID.js'
|
||||
import RootID from './ID/RootID.js'
|
||||
import * as ID from './ID.js'
|
||||
import GC from '../Struct/GC.js'
|
||||
|
||||
// TODO: Implement function to describe ranges
|
||||
@ -72,9 +71,9 @@ export function fromRelativePosition (y, rpos) {
|
||||
if (rpos[0] === 'endof') {
|
||||
let id
|
||||
if (rpos[3] === null) {
|
||||
id = new ID(rpos[1], rpos[2])
|
||||
id = ID.createID(rpos[1], rpos[2])
|
||||
} else {
|
||||
id = new RootID(rpos[3], rpos[4])
|
||||
id = ID.createRootID(rpos[3], rpos[4])
|
||||
}
|
||||
let type = y.os.get(id)
|
||||
while (type._redone !== null) {
|
||||
@ -89,7 +88,7 @@ export function fromRelativePosition (y, rpos) {
|
||||
}
|
||||
} else {
|
||||
let offset = 0
|
||||
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
|
||||
let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val
|
||||
const diff = rpos[1] - struct._id.clock
|
||||
while (struct._redone !== null) {
|
||||
struct = struct._redone
|
||||
|
@ -1,18 +1,3 @@
|
||||
import Delete from '../Struct/Delete.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
import ItemFormat from '../Struct/ItemFormat.js'
|
||||
import ItemEmbed from '../Struct/ItemEmbed.js'
|
||||
import GC from '../Struct/GC.js'
|
||||
|
||||
import YArray from '../Types/YArray/YArray.js'
|
||||
import YMap from '../Types/YMap/YMap.js'
|
||||
import YText from '../Types/YText/YText.js'
|
||||
import YXmlText from '../Types/YXml/YXmlText.js'
|
||||
import YXmlHook from '../Types/YXml/YXmlHook.js'
|
||||
import YXmlFragment from '../Types/YXml/YXmlFragment.js'
|
||||
import YXmlElement from '../Types/YXml/YXmlElement.js'
|
||||
|
||||
const structs = new Map()
|
||||
const references = new Map()
|
||||
|
||||
@ -21,7 +6,7 @@ const references = new Map()
|
||||
* reference on all clients!
|
||||
*
|
||||
* @param {Number} reference
|
||||
* @param {class} structConstructor
|
||||
* @param {Function} structConstructor
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@ -43,20 +28,3 @@ export function getStruct (reference) {
|
||||
export function getStructReference (typeConstructor) {
|
||||
return references.get(typeConstructor)
|
||||
}
|
||||
|
||||
// TODO: reorder (Item* should have low numbers)
|
||||
registerStruct(0, ItemJSON)
|
||||
registerStruct(1, ItemString)
|
||||
registerStruct(10, ItemFormat)
|
||||
registerStruct(11, ItemEmbed)
|
||||
registerStruct(2, Delete)
|
||||
|
||||
registerStruct(3, YArray)
|
||||
registerStruct(4, YMap)
|
||||
registerStruct(5, YText)
|
||||
registerStruct(6, YXmlFragment)
|
||||
registerStruct(7, YXmlElement)
|
||||
registerStruct(8, YXmlText)
|
||||
registerStruct(9, YXmlHook)
|
||||
|
||||
registerStruct(12, GC)
|
||||
|
@ -1,54 +0,0 @@
|
||||
|
||||
export { default as Y } from './Y.js'
|
||||
export { default as UndoManager } from './Util/UndoManager.js'
|
||||
export { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||
|
||||
import Connector from './Connector.js'
|
||||
import Persistence from './Persistence.js'
|
||||
import YArray from './Types/YArray/YArray.js'
|
||||
import YMap from './Types/YMap/YMap.js'
|
||||
import YText from './Types/YText/YText.js'
|
||||
import YXmlText from './Types/YXml/YXmlText.js'
|
||||
import YXmlHook from './Types/YXml/YXmlHook.js'
|
||||
import YXmlFragment from './Types/YXml/YXmlFragment.js'
|
||||
import YXmlElement from './Types/YXml/YXmlElement.js'
|
||||
import BinaryDecoder from './Util/Binary/Decoder.js'
|
||||
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
|
||||
import { registerStruct } from './Util/structReferences.js'
|
||||
import TextareaBinding from './Bindings/TextareaBinding/TextareaBinding.js'
|
||||
import QuillBinding from './Bindings/QuillBinding/QuillBinding.js'
|
||||
import DomBinding from './Bindings/DomBinding/DomBinding.js'
|
||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
||||
|
||||
import domToType from './Bindings/DomBinding/domToType.js'
|
||||
import { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
|
||||
|
||||
// TODO: The following assignments should be moved to yjs-dist
|
||||
Y.AbstractConnector = Connector
|
||||
Y.AbstractPersistence = Persistence
|
||||
Y.Array = YArray
|
||||
Y.Map = YMap
|
||||
Y.Text = YText
|
||||
Y.XmlElement = YXmlElement
|
||||
Y.XmlFragment = YXmlFragment
|
||||
Y.XmlText = YXmlText
|
||||
Y.XmlHook = YXmlHook
|
||||
|
||||
Y.TextareaBinding = TextareaBinding
|
||||
Y.QuillBinding = QuillBinding
|
||||
Y.DomBinding = DomBinding
|
||||
|
||||
DomBinding.domToType = domToType
|
||||
DomBinding.domsToTypes = domsToTypes
|
||||
DomBinding.switchAssociation = switchAssociation
|
||||
|
||||
Y.utils = {
|
||||
BinaryDecoder,
|
||||
UndoManager,
|
||||
getRelativePosition,
|
||||
fromRelativePosition,
|
||||
registerStruct,
|
||||
integrateRemoteStructs,
|
||||
toBinary,
|
||||
fromBinary
|
||||
}
|
157
src/Y.js
157
src/Y.js
@ -2,11 +2,17 @@ import DeleteStore from './Store/DeleteStore.js'
|
||||
import OperationStore from './Store/OperationStore.js'
|
||||
import StateStore from './Store/StateStore.js'
|
||||
import { generateRandomUint32 } from './Util/generateRandomUint32.js'
|
||||
import RootID from './Util/ID/RootID.js'
|
||||
import { createRootID } from './Util/ID.js'
|
||||
import NamedEventHandler from '../lib/NamedEventHandler.js'
|
||||
import Transaction from './Util/Transaction.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as message from './message.js'
|
||||
import { integrateRemoteStructs } from './Util/integrateRemoteStructs.js'
|
||||
|
||||
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
||||
/**
|
||||
* @typedef {import('./Struct/Type.js').default} YType
|
||||
* @typedef {import('../lib/decoding.js').Decoder} Decoder
|
||||
*/
|
||||
|
||||
/**
|
||||
* Anything that can be encoded with `JSON.stringify` and can be decoded with
|
||||
@ -17,18 +23,17 @@ export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
||||
*
|
||||
* At the moment the only safe values are number and string.
|
||||
*
|
||||
* @typedef {(number|string)} encodable
|
||||
* @typedef {(number|string|Object)} encodable
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
*
|
||||
* @param {string} room Users in the same room share the same content
|
||||
* @param {Object} opts Connector definition
|
||||
* @param {AbstractPersistence} persistence Persistence adapter instance
|
||||
* @param {Object} conf configuration
|
||||
*/
|
||||
export default class Y extends NamedEventHandler {
|
||||
constructor (room, connector, persistence, conf = {}) {
|
||||
constructor (room, conf = {}) {
|
||||
super()
|
||||
this.gcEnabled = conf.gc || false
|
||||
/**
|
||||
@ -39,60 +44,46 @@ export default class Y extends NamedEventHandler {
|
||||
this._contentReady = false
|
||||
this.userID = generateRandomUint32()
|
||||
// TODO: This should be a Map so we can use encodables as keys
|
||||
this.share = {}
|
||||
this.ds = new DeleteStore(this)
|
||||
this._map = new Map()
|
||||
this.ds = new DeleteStore()
|
||||
this.os = new OperationStore(this)
|
||||
this.ss = new StateStore(this)
|
||||
this._missingStructs = new Map()
|
||||
this._readyToIntegrate = []
|
||||
this._transaction = null
|
||||
/**
|
||||
* The {@link AbstractConnector}.that is used by this Yjs instance.
|
||||
* @type {AbstractConnector}
|
||||
*/
|
||||
this.connector = null
|
||||
this.connected = false
|
||||
let initConnection = () => {
|
||||
if (connector != null) {
|
||||
if (connector.constructor === Object) {
|
||||
connector.connector.room = room
|
||||
this.connector = new Y[connector.connector.name](this, connector.connector)
|
||||
this.connected = true
|
||||
this.emit('connectorReady')
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The {@link AbstractPersistence} that is used by this Yjs instance.
|
||||
* @type {AbstractPersistence}
|
||||
*/
|
||||
this.persistence = null
|
||||
if (persistence != null) {
|
||||
this.persistence = persistence
|
||||
persistence._init(this).then(initConnection)
|
||||
} else {
|
||||
initConnection()
|
||||
}
|
||||
// for compatibility with isParentOf
|
||||
this._parent = null
|
||||
this._hasUndoManager = false
|
||||
this._deleted = false // for compatiblity of having this as a parent for types
|
||||
this._id = null
|
||||
}
|
||||
_setContentReady () {
|
||||
if (!this._contentReady) {
|
||||
this._contentReady = true
|
||||
this.emit('content')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Decoder and fill the Yjs instance with data in the decoder.
|
||||
*
|
||||
* @param {Decoder} decoder The BinaryDecoder to read from.
|
||||
*/
|
||||
importModel (decoder) {
|
||||
this.transact(function () {
|
||||
integrateRemoteStructs(decoder, this)
|
||||
message.readDeleteSet(decoder, this)
|
||||
})
|
||||
}
|
||||
whenContentReady () {
|
||||
if (this._contentReady) {
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
this.once('content', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the Yjs model to ArrayBuffer
|
||||
*
|
||||
* @return {ArrayBuffer} The Yjs model as ArrayBuffer
|
||||
*/
|
||||
exportModel () {
|
||||
const encoder = encoding.createEncoder()
|
||||
message.writeStructs(encoder, this, new Map())
|
||||
message.writeDeleteSet(encoder, this)
|
||||
return encoding.toBuffer(encoder)
|
||||
}
|
||||
_beforeChange () {}
|
||||
_callObserver (transaction, subs, remote) {}
|
||||
/**
|
||||
* Changes that happen inside of a transaction are bundled. This means that
|
||||
* the observer fires _after_ the transaction is finished and that all changes
|
||||
@ -157,9 +148,7 @@ export default class Y extends NamedEventHandler {
|
||||
* @private
|
||||
* Fake _start for root properties (y.set('name', type))
|
||||
*/
|
||||
set _start (start) {
|
||||
return null
|
||||
}
|
||||
set _start (start) {}
|
||||
|
||||
/**
|
||||
* Define a shared data type.
|
||||
@ -168,7 +157,7 @@ export default class Y extends NamedEventHandler {
|
||||
* and do not overwrite each other. I.e.
|
||||
* `y.define(name, type) === y.define(name, type)`
|
||||
*
|
||||
* After this method is called, the type is also available on `y.share[name]`.
|
||||
* After this method is called, the type is also available on `y._map.get(name)`.
|
||||
*
|
||||
* *Best Practices:*
|
||||
* Either define all types right after the Yjs instance is created or always
|
||||
@ -179,7 +168,7 @@ export default class Y extends NamedEventHandler {
|
||||
* const y = new Y(..)
|
||||
* y.define('myArray', YArray)
|
||||
* y.define('myMap', YMap)
|
||||
* // .. when accessing the type use y.share[name]
|
||||
* // .. when accessing the type use y._map.get(name)
|
||||
* y.share.myArray.insert(..)
|
||||
* y.share.myMap.set(..)
|
||||
*
|
||||
@ -190,15 +179,15 @@ export default class Y extends NamedEventHandler {
|
||||
* y.define('myMap', YMap).set(..)
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {YType Constructor} TypeConstructor The constructor of the type definition
|
||||
* @returns {YType} The created type
|
||||
* @param {Function} TypeConstructor The constructor of the type definition
|
||||
* @returns {YType} The created type. Constructed with TypeConstructor
|
||||
*/
|
||||
define (name, TypeConstructor) {
|
||||
let id = new RootID(name, TypeConstructor)
|
||||
let id = createRootID(name, TypeConstructor)
|
||||
let type = this.os.get(id)
|
||||
if (this.share[name] === undefined) {
|
||||
this.share[name] = type
|
||||
} else if (this.share[name] !== type) {
|
||||
if (this._map.get(name) === undefined) {
|
||||
this._map.set(name, type)
|
||||
} else if (this._map.get(name) !== type) {
|
||||
throw new Error('Type is already defined with a different constructor')
|
||||
}
|
||||
return type
|
||||
@ -213,66 +202,18 @@ export default class Y extends NamedEventHandler {
|
||||
* @param {String} name The typename
|
||||
*/
|
||||
get (name) {
|
||||
return this.share[name]
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect this Yjs Instance from the network. The connector will
|
||||
* unsubscribe from the room and document updates are not shared anymore.
|
||||
*/
|
||||
disconnect () {
|
||||
if (this.connected) {
|
||||
this.connected = false
|
||||
return this.connector.disconnect()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If disconnected, tell the connector to reconnect to the room.
|
||||
*/
|
||||
reconnect () {
|
||||
if (!this.connected) {
|
||||
this.connected = true
|
||||
return this.connector.reconnect()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return this._map.get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
||||
* Persisted data will remain until removed by the persistence adapter.
|
||||
*/
|
||||
destroy () {
|
||||
this.emit('destroyed', true)
|
||||
super.destroy()
|
||||
this.share = null
|
||||
if (this.connector != null) {
|
||||
if (this.connector.destroy != null) {
|
||||
this.connector.destroy()
|
||||
} else {
|
||||
this.connector.disconnect()
|
||||
}
|
||||
}
|
||||
if (this.persistence !== null) {
|
||||
this.persistence.deinit(this)
|
||||
this.persistence = null
|
||||
}
|
||||
this._map = null
|
||||
this.os = null
|
||||
this.ds = null
|
||||
this.ss = null
|
||||
}
|
||||
}
|
||||
|
||||
Y.extend = function extendYjs () {
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var f = arguments[i]
|
||||
if (typeof f === 'function') {
|
||||
f(Y)
|
||||
} else {
|
||||
throw new Error('Expected a function!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
/* eslint-env browser */
|
||||
import * as idbactions from './idbactions.js'
|
||||
import * as globals from './globals.js'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as message from './message.js'
|
||||
import * as bc from './broadcastchannel.js'
|
||||
import * as encoding from './encoding.js'
|
||||
import * as logging from './logging.js'
|
||||
import * as idb from './idb.js'
|
||||
import Y from '../src/Y.js'
|
||||
import BinaryDecoder from '../src/Util/Binary/Decoder.js'
|
||||
import { integrateRemoteStruct } from '../src/MessageHandler/integrateRemoteStructs.js'
|
||||
import { createMutualExclude } from '../src/Util/mutualExclude.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as logging from '../../lib/logging.js'
|
||||
import * as idb from '../../lib/idb.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import Y from '../Y.js'
|
||||
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
|
||||
import * as NamedEventHandler from './NamedEventHandler.js'
|
||||
|
||||
@ -70,8 +70,8 @@ export class YdbClient extends NamedEventHandler.Class {
|
||||
}))
|
||||
subscribe(this, roomname, update => mutex(() => {
|
||||
y.transact(() => {
|
||||
const decoder = new BinaryDecoder(update)
|
||||
while (decoder.hasContent()) {
|
||||
const decoder = decoding.createDecoder(update)
|
||||
while (decoding.hasContent(decoder)) {
|
||||
integrateRemoteStruct(y, decoder)
|
||||
}
|
||||
}, true)
|
||||
|
@ -29,10 +29,10 @@
|
||||
* - A client may update a room when the room is in either US or Co
|
||||
*/
|
||||
|
||||
import * as encoding from './encoding.js'
|
||||
import * as decoding from './decoding.js'
|
||||
import * as idb from './idb.js'
|
||||
import * as globals from './globals.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as idb from '../../lib/idb.js'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as message from './message.js'
|
||||
|
||||
/**
|
||||
|
55
src/index.js
Normal file
55
src/index.js
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
import Delete from './Struct/Delete.js'
|
||||
import ItemJSON from './Struct/ItemJSON.js'
|
||||
import ItemString from './Struct/ItemString.js'
|
||||
import ItemFormat from './Struct/ItemFormat.js'
|
||||
import ItemEmbed from './Struct/ItemEmbed.js'
|
||||
import GC from './Struct/GC.js'
|
||||
|
||||
import YArray from './Types/YArray/YArray.js'
|
||||
import YMap from './Types/YMap/YMap.js'
|
||||
import YText from './Types/YText/YText.js'
|
||||
import YXmlText from './Types/YXml/YXmlText.js'
|
||||
import YXmlHook from './Types/YXml/YXmlHook.js'
|
||||
import YXmlFragment from './Types/YXml/YXmlFragment.js'
|
||||
import YXmlElement from './Types/YXml/YXmlElement.js'
|
||||
|
||||
import { registerStruct } from './Util/structReferences.js'
|
||||
|
||||
export { default as Y } from './Y.js'
|
||||
export { default as UndoManager } from './Util/UndoManager.js'
|
||||
export { default as Transaction } from './Util/Transaction.js'
|
||||
|
||||
export { default as Array } from './Types/YArray/YArray.js'
|
||||
export { default as Map } from './Types/YMap/YMap.js'
|
||||
export { default as Text } from './Types/YText/YText.js'
|
||||
export { default as XmlText } from './Types/YXml/YXmlText.js'
|
||||
export { default as XmlHook } from './Types/YXml/YXmlHook.js'
|
||||
export { default as XmlFragment } from './Types/YXml/YXmlFragment.js'
|
||||
export { default as XmlElement } from './Types/YXml/YXmlElement.js'
|
||||
|
||||
export { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
|
||||
export { registerStruct as registerType } from './Util/structReferences.js'
|
||||
export { default as TextareaBinding } from './Bindings/TextareaBinding/TextareaBinding.js'
|
||||
export { default as QuillBinding } from './Bindings/QuillBinding/QuillBinding.js'
|
||||
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
||||
|
||||
export { default as domToType } from './Bindings/DomBinding/domToType.js'
|
||||
export { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
|
||||
|
||||
// TODO: reorder (Item* should have low numbers)
|
||||
registerStruct(0, ItemJSON)
|
||||
registerStruct(1, ItemString)
|
||||
registerStruct(10, ItemFormat)
|
||||
registerStruct(11, ItemEmbed)
|
||||
registerStruct(2, Delete)
|
||||
|
||||
registerStruct(3, YArray)
|
||||
registerStruct(4, YMap)
|
||||
registerStruct(5, YText)
|
||||
registerStruct(6, YXmlFragment)
|
||||
registerStruct(7, YXmlElement)
|
||||
registerStruct(8, YXmlText)
|
||||
registerStruct(9, YXmlHook)
|
||||
|
||||
registerStruct(12, GC)
|
487
src/message.js
Normal file
487
src/message.js
Normal file
@ -0,0 +1,487 @@
|
||||
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as ID from './Util/ID.js'
|
||||
import { getStruct } from './Util/structReferences.js'
|
||||
import { deleteItemRange } from './Struct/Delete.js'
|
||||
import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js'
|
||||
import Item from './Struct/Item.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./Store/StateStore.js').default} StateStore
|
||||
* @typedef {import('./Y.js').default} Y
|
||||
* @typedef {import('./Struct/Item.js').default} Item
|
||||
* @typedef {import('./Store/StateStore.js').StateSet} StateSet
|
||||
*/
|
||||
|
||||
/**
|
||||
* Core Yjs only defines three message types:
|
||||
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
||||
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the the client is assured that
|
||||
* it received all information from the remote client.
|
||||
*
|
||||
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
||||
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
||||
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
||||
*
|
||||
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
||||
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
||||
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
||||
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
||||
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
||||
* Therefore it is necesarry that the client initiates the sync.
|
||||
*
|
||||
* Construction of a message:
|
||||
* [messageType : varUint, message definition..]
|
||||
*
|
||||
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
||||
*
|
||||
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
||||
*/
|
||||
|
||||
const messageYjsSyncStep1 = 0
|
||||
const messageYjsSyncStep2 = 1
|
||||
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.readVarUint(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 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 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((user, userState) => {
|
||||
s += `(${user}: ${userState}), `
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Write StateSet to Encoder
|
||||
*
|
||||
* @param {Y} y
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
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((user, clock) => {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify an item id.
|
||||
*
|
||||
* @param {ID.ID | ID.RootID} id
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
|
||||
|
||||
/**
|
||||
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
|
||||
*
|
||||
* @param {Item | Y | null} item
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyItemID = item => {
|
||||
let result
|
||||
if (item === null) {
|
||||
result = '()'
|
||||
} else if (item instanceof Item) {
|
||||
result = stringifyID(item._id)
|
||||
} else {
|
||||
// must be a Yjs instance
|
||||
// Don't include Y in this module, so we prevent circular dependencies.
|
||||
result = 'y'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||
* @param {Item} item The item instance.
|
||||
* @param {String} [append] Additional information to append to the returned
|
||||
* string.
|
||||
* @return {String} A readable string that represents the item object.
|
||||
*
|
||||
*/
|
||||
export const logItemHelper = (name, item, append) => {
|
||||
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
|
||||
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
|
||||
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyStructs = (decoder, y) => {
|
||||
let str = ''
|
||||
const len = decoding.readUint32(decoder)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoding.readVarUint(decoder)
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
|
||||
}
|
||||
str += logMessage + '\n'
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all Items that are not not included in ss to
|
||||
* the encoder object.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
export const writeStructs = (encoder, y, ss) => {
|
||||
const lenPos = encoding.length(encoder)
|
||||
encoding.writeUint32(encoder, 0)
|
||||
let len = 0
|
||||
for (let user of y.ss.state.keys()) {
|
||||
let clock = ss.get(user) || 0
|
||||
if (user !== ID.RootFakeUserID) {
|
||||
const minBound = ID.createID(user, clock)
|
||||
const overlappingLeft = y.os.findPrev(minBound)
|
||||
const rightID = overlappingLeft === null ? null : overlappingLeft._id
|
||||
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
|
||||
const struct = overlappingLeft._clonePartial(clock - rightID.clock)
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
}
|
||||
y.os.iterate(minBound, ID.createID(user, Number.MAX_VALUE), struct => {
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
})
|
||||
}
|
||||
}
|
||||
encoding.setUint32(encoder, lenPos, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read structs and delete operations from decoder and apply them on a shared document.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readStructs = (decoder, y) => {
|
||||
const len = decoding.readUint32(decoder)
|
||||
for (let i = 0; i < len; i++) {
|
||||
integrateRemoteStruct(decoder, y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read SyncStep1 and return it as a readable string.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifySyncStep1 = (decoder) => {
|
||||
let s = 'SyncStep1: '
|
||||
const len = decoding.readUint32(decoder)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const user = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
s += `(${user}:${clock})`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync step 1 message based on the state of the current shared document.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const writeSyncStep1 = (encoder, y) => {
|
||||
encoding.writeVarUint(encoder, messageYjsSyncStep1)
|
||||
writeStateSet(encoder, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read SyncStep1 message and reply with SyncStep2.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder The reply to the received message
|
||||
* @param {encoding.Encoder} encoder The received message
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readSyncStep1 = (decoder, encoder, y) => {
|
||||
// read sync step 1 message
|
||||
const ss = readStateSet(decoder)
|
||||
// write sync step 2
|
||||
encoding.writeVarUint(encoder, messageYjsSyncStep2)
|
||||
writeStructs(encoder, y, ss)
|
||||
writeDeleteSet(encoder, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifySyncStep2 = (decoder, y) => {
|
||||
let str = ' == Sync step 2:\n'
|
||||
str += ' + Structs:\n'
|
||||
str += stringifyStructs(decoder, y)
|
||||
// write DS to string
|
||||
str += ' + Delete Set:\n'
|
||||
str += stringifyDeleteSet(decoder)
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and apply Structs and then DeleteSet to a y instance.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readSyncStep2 = (decoder, encoder, y) => {
|
||||
readStructs(decoder, y)
|
||||
readDeleteSet(decoder, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyUpdate = (decoder, y) =>
|
||||
' == Update:\n' + stringifyStructs(decoder, y)
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {encoding.Encoder} updates
|
||||
*/
|
||||
export const writeUpdate = (encoder, numOfStructs, updates) => {
|
||||
encoding.writeVarUint(encoder, messageYjsUpdate)
|
||||
encoding.writeUint32(encoder, numOfStructs)
|
||||
encoding.writeBinaryEncoder(encoder, updates)
|
||||
}
|
||||
|
||||
export const readUpdate = readStructs
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @return {string} The message converted to string
|
||||
*/
|
||||
export const stringifyMessage = (decoder, y) => {
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
let stringifiedMessage
|
||||
let stringifiedMessageType
|
||||
switch (messageType) {
|
||||
case messageYjsSyncStep1:
|
||||
stringifiedMessageType = 'YjsSyncStep1'
|
||||
stringifiedMessage = stringifySyncStep1(decoder)
|
||||
break
|
||||
case messageYjsSyncStep2:
|
||||
stringifiedMessageType = 'YjsSyncStep2'
|
||||
stringifiedMessage = stringifySyncStep2(decoder, y)
|
||||
break
|
||||
case messageYjsUpdate:
|
||||
stringifiedMessageType = 'YjsUpdate'
|
||||
stringifiedMessage = stringifyStructs(decoder, y)
|
||||
break
|
||||
default:
|
||||
stringifiedMessageType = 'Unknown'
|
||||
stringifiedMessage = 'Unknown'
|
||||
}
|
||||
return `Message ${stringifiedMessageType}:\n${stringifiedMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder A message received from another client
|
||||
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readMessage = (decoder, encoder, y) => {
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case messageYjsSyncStep1:
|
||||
readSyncStep1(decoder, encoder, y)
|
||||
break
|
||||
case messageYjsSyncStep2:
|
||||
y.transact(() => readSyncStep2(decoder, encoder, y), true)
|
||||
break
|
||||
case messageYjsUpdate:
|
||||
y.transact(() => readUpdate(decoder, y), true)
|
||||
break
|
||||
default:
|
||||
throw new Error('Unknown message type')
|
||||
}
|
||||
return messageType
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
import { test } from '../node_modules/cutest/cutest.js'
|
||||
import Chance from 'chance'
|
||||
import { test } from '../node_modules/cutest/cutest.mjs'
|
||||
import * as random from '../lib/random/random.js'
|
||||
import DeleteStore from '../src/Store/DeleteStore.js'
|
||||
import ID from '../src/Util/ID/ID.js'
|
||||
import * as ID from '../src/Util/ID.js'
|
||||
|
||||
/**
|
||||
* Converts a DS to an array of length 10.
|
||||
*
|
||||
* @example
|
||||
* const ds = new DeleteStore()
|
||||
* ds.mark(new ID(0, 0), 1, false)
|
||||
* ds.mark(new ID(0, 1), 1, true)
|
||||
* ds.mark(new ID(0, 3), 1, false)
|
||||
* ds.mark(ID.createID(0, 0), 1, false)
|
||||
* ds.mark(ID.createID(0, 1), 1, true)
|
||||
* ds.mark(ID.createID(0, 3), 1, false)
|
||||
* dsToArray(ds) // => [0, 1, undefined, 0]
|
||||
*
|
||||
* @return {Array<(undefined|number)>} Array of numbers indicating if array[i] is deleted (0), garbage collected (1), or undeleted (undefined).
|
||||
@ -32,32 +32,32 @@ function dsToArray (ds) {
|
||||
|
||||
test('DeleteStore', async function ds1 (t) {
|
||||
const ds = new DeleteStore()
|
||||
ds.mark(new ID(0, 1), 1, false)
|
||||
ds.mark(new ID(0, 2), 1, false)
|
||||
ds.mark(new ID(0, 3), 1, false)
|
||||
ds.mark(ID.createID(0, 1), 1, false)
|
||||
ds.mark(ID.createID(0, 2), 1, false)
|
||||
ds.mark(ID.createID(0, 3), 1, false)
|
||||
t.compare(dsToArray(ds), [null, 0, 0, 0])
|
||||
ds.mark(new ID(0, 2), 1, true)
|
||||
ds.mark(ID.createID(0, 2), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 0, 1, 0])
|
||||
ds.mark(new ID(0, 1), 1, true)
|
||||
ds.mark(ID.createID(0, 1), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 0])
|
||||
ds.mark(new ID(0, 3), 1, true)
|
||||
ds.mark(ID.createID(0, 3), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1])
|
||||
ds.mark(new ID(0, 5), 1, true)
|
||||
ds.mark(new ID(0, 4), 1, true)
|
||||
ds.mark(ID.createID(0, 5), 1, true)
|
||||
ds.mark(ID.createID(0, 4), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1, 1, 1])
|
||||
ds.mark(new ID(0, 0), 3, false)
|
||||
ds.mark(ID.createID(0, 0), 3, false)
|
||||
t.compare(dsToArray(ds), [0, 0, 0, 1, 1, 1])
|
||||
})
|
||||
|
||||
test('random DeleteStore tests', async function randomDS (t) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
const prng = random.createPRNG(t.getSeed())
|
||||
const ds = new DeleteStore()
|
||||
const dsArray = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const pos = chance.integer({ min: 0, max: 10 })
|
||||
const len = chance.integer({ min: 0, max: 4 })
|
||||
const gc = chance.bool()
|
||||
ds.mark(new ID(0, pos), len, gc)
|
||||
const pos = random.int32(prng, 0, 10)
|
||||
const len = random.int32(prng, 0, 4)
|
||||
const gc = random.bool(prng)
|
||||
ds.mark(ID.createID(0, pos), len, gc)
|
||||
for (let j = 0; j < len; j++) {
|
||||
dsArray[pos + j] = gc ? 1 : 0
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test } from '../node_modules/cutest/cutest.js'
|
||||
import { test } from '../node_modules/cutest/cutest.mjs'
|
||||
import simpleDiff from '../lib/simpleDiff.js'
|
||||
import Chance from 'chance'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
function runDiffTest (t, a, b, expected) {
|
||||
let result = simpleDiff(a, b)
|
||||
@ -19,9 +19,9 @@ test('diff tests', async function diff1 (t) {
|
||||
})
|
||||
|
||||
test('random diff tests', async function randomDiff (t) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
let a = chance.word()
|
||||
let b = chance.word()
|
||||
const gen = random.createPRNG(t.getSeed() * 1000000000)
|
||||
let a = random.word(gen)
|
||||
let b = random.word(gen)
|
||||
let change = simpleDiff(a, b)
|
||||
let arr = Array.from(a)
|
||||
arr.splice(change.pos, change.remove, ...Array.from(change.insert))
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { test } from '../node_modules/cutest/cutest.js'
|
||||
import BinaryEncoder from '../src/Util/Binary/Encoder.js'
|
||||
import BinaryDecoder from '../src/Util/Binary/Decoder.js'
|
||||
import { test } from '../node_modules/cutest/cutest.mjs'
|
||||
import { generateRandomUint32 } from '../src/Util/generateRandomUint32.js'
|
||||
import Chance from 'chance'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
function testEncoding (t, write, read, val) {
|
||||
let encoder = new BinaryEncoder()
|
||||
let encoder = encoding.createEncoder()
|
||||
write(encoder, val)
|
||||
let reader = new BinaryDecoder(encoder.createBuffer())
|
||||
let reader = decoding.createDecoder(encoding.toBuffer(encoder))
|
||||
let result = read(reader)
|
||||
t.log(`string encode: ${JSON.stringify(val).length} bytes / binary encode: ${encoder.length} bytes`)
|
||||
t.log(`string encode: ${JSON.stringify(val).length} bytes / binary encode: ${encoding.length(encoder)} bytes`)
|
||||
t.compare(val, result, 'Compare results')
|
||||
}
|
||||
|
||||
@ -37,8 +37,8 @@ test('varUint of 2839012934', async function varUint2839012934 (t) {
|
||||
})
|
||||
|
||||
test('varUint random', async function varUintRandom (t) {
|
||||
const chance = new Chance(t.getSeed() * Math.pow(Number.MAX_SAFE_INTEGER))
|
||||
testEncoding(t, writeVarUint, readVarUint, chance.integer({min: 0, max: (1 << 28) - 1}))
|
||||
const prng = random.createPRNG(t.getSeed() * Math.pow(Number.MAX_SAFE_INTEGER, 2))
|
||||
testEncoding(t, writeVarUint, readVarUint, random.int32(prng, 0, (1 << 28) - 1))
|
||||
})
|
||||
|
||||
test('varUint random user id', async function varUintRandomUserId (t) {
|
||||
@ -60,6 +60,6 @@ test('varString', async function varString (t) {
|
||||
})
|
||||
|
||||
test('varString random', async function varStringRandom (t) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
testEncoding(t, writeVarString, readVarString, chance.string())
|
||||
const prng = random.createPRNG(t.getSeed() * 10000000)
|
||||
testEncoding(t, writeVarString, readVarString, random.utf16String(prng))
|
||||
})
|
||||
|
@ -1,3 +1,5 @@
|
||||
// TODO: include all tests
|
||||
|
||||
import './red-black-tree.js'
|
||||
import './y-array.tests.js'
|
||||
import './y-text.tests.js'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import RedBlackTree from '../lib/Tree.js'
|
||||
import ID from '../src/Util/ID/ID.js'
|
||||
import Chance from 'chance'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as ID from '../src/Util/ID.js'
|
||||
import { test, proxyConsole } from '../node_modules/cutest/cutest.mjs'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
@ -55,28 +55,28 @@ function checkRootNodeIsBlack (t, tree) {
|
||||
|
||||
test('RedBlack Tree', async function redBlackTree (t) {
|
||||
let tree = new RedBlackTree()
|
||||
tree.put({_id: new ID(8433, 0)})
|
||||
tree.put({_id: new ID(12844, 0)})
|
||||
tree.put({_id: new ID(1795, 0)})
|
||||
tree.put({_id: new ID(30302, 0)})
|
||||
tree.put({_id: new ID(64287)})
|
||||
tree.delete(new ID(8433, 0))
|
||||
tree.put({_id: new ID(28996)})
|
||||
tree.delete(new ID(64287))
|
||||
tree.put({_id: new ID(22721)})
|
||||
tree.put({_id: ID.createID(8433, 0)})
|
||||
tree.put({_id: ID.createID(12844, 0)})
|
||||
tree.put({_id: ID.createID(1795, 0)})
|
||||
tree.put({_id: ID.createID(30302, 0)})
|
||||
tree.put({_id: ID.createID(64287)})
|
||||
tree.delete(ID.createID(8433, 0))
|
||||
tree.put({_id: ID.createID(28996)})
|
||||
tree.delete(ID.createID(64287))
|
||||
tree.put({_id: ID.createID(22721)})
|
||||
checkRootNodeIsBlack(t, tree)
|
||||
checkBlackHeightOfSubTreesAreEqual(t, tree)
|
||||
checkRedNodesDoNotHaveBlackChildren(t, tree)
|
||||
})
|
||||
|
||||
test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
let chance = new Chance(t.getSeed() * 1000000000)
|
||||
test(`random tests (${numberOfRBTreeTests})`, async function randomRBTree (t) {
|
||||
let prng = random.createPRNG(t.getSeed() * 1000000000)
|
||||
let tree = new RedBlackTree()
|
||||
let elements = []
|
||||
for (var i = 0; i < numberOfRBTreeTests; i++) {
|
||||
if (chance.bool({likelihood: 80})) {
|
||||
if (random.int32(prng, 0, 100) < 80) {
|
||||
// 80% chance to insert an element
|
||||
let obj = new ID(chance.integer({min: 0, max: numberOfRBTreeTests}), chance.integer({min: 0, max: 1}))
|
||||
let obj = ID.createID(random.int32(prng, 0, numberOfRBTreeTests), random.int32(prng, 0, 1))
|
||||
let nodeExists = tree.find(obj)
|
||||
if (nodeExists === null) {
|
||||
if (elements.some(e => e.equals(obj))) {
|
||||
@ -87,7 +87,7 @@ test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
}
|
||||
} else if (elements.length > 0) {
|
||||
// ~20% chance to delete an element
|
||||
var elem = chance.pickone(elements)
|
||||
var elem = random.oneOf(prng, elements)
|
||||
elements = elements.filter(function (e) {
|
||||
return !e.equals(elem)
|
||||
})
|
||||
@ -119,7 +119,7 @@ test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
'Find every object with lower bound search'
|
||||
)
|
||||
// TEST iteration (with lower bound search)
|
||||
let lowerBound = chance.pickone(elements)
|
||||
let lowerBound = random.oneOf(prng, elements)
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
(lowerBound.lessThan(e) || e.equals(lowerBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
@ -151,7 +151,7 @@ test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
'iterating over a tree without bounds yields the right amount of results'
|
||||
)
|
||||
|
||||
let upperBound = chance.pickone(elements)
|
||||
let upperBound = random.oneOf(prng, elements)
|
||||
expectedResults = elements.filter((e, pos) =>
|
||||
(e.lessThan(upperBound) || e.equals(upperBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
@ -168,8 +168,8 @@ test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
'iterating over a tree with upper bound yields the right amount of results'
|
||||
)
|
||||
|
||||
upperBound = chance.pickone(elements)
|
||||
lowerBound = chance.pickone(elements)
|
||||
upperBound = random.oneOf(prng, elements)
|
||||
lowerBound = random.oneOf(prng, elements)
|
||||
if (upperBound.lessThan(lowerBound)) {
|
||||
[lowerBound, upperBound] = [upperBound, lowerBound]
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { wait, initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../tests-lib/helper.js'
|
||||
import { initArrays, compareUsers, applyRandomTests } from '../tests-lib/helper.js'
|
||||
import * as Y from '../src/index.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
proxyConsole()
|
||||
test('basic spec', async function array0 (t) {
|
||||
@ -24,10 +26,10 @@ test('basic spec', async function array0 (t) {
|
||||
})
|
||||
|
||||
test('insert three elements, try re-get property', async function array1 (t) {
|
||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
var { testConnector, users, array0, array1 } = initArrays(t, { users: 2 })
|
||||
array0.insert(0, [1, 2, 3])
|
||||
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
@ -41,9 +43,9 @@ test('concurrent insert (handle three conflicts)', async function array2 (t) {
|
||||
})
|
||||
|
||||
test('concurrent insert&delete (handle three conflicts)', async function array3 (t) {
|
||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
array0.insert(1, [0])
|
||||
array1.delete(0)
|
||||
array1.delete(1, 1)
|
||||
@ -53,56 +55,52 @@ test('concurrent insert&delete (handle three conflicts)', async function array3
|
||||
})
|
||||
|
||||
test('insertions work in late sync', async function array4 (t) {
|
||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
users[1].disconnect()
|
||||
users[2].disconnect()
|
||||
array0.insert(1, ['user0'])
|
||||
array1.insert(1, ['user1'])
|
||||
array2.insert(1, ['user2'])
|
||||
await users[1].reconnect()
|
||||
await users[2].reconnect()
|
||||
await users[1].connect()
|
||||
await users[2].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('disconnect really prevents sending messages', async function array5 (t) {
|
||||
var { users, array0, array1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
users[1].disconnect()
|
||||
users[2].disconnect()
|
||||
array0.insert(1, ['user0'])
|
||||
array1.insert(1, ['user1'])
|
||||
await wait(1000)
|
||||
t.compare(array0.toJSON(), ['x', 'user0', 'y'])
|
||||
t.compare(array1.toJSON(), ['x', 'user1', 'y'])
|
||||
await users[1].reconnect()
|
||||
await users[2].reconnect()
|
||||
await users[1].connect()
|
||||
await users[2].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('deletions in late sync', async function array6 (t) {
|
||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
await users[1].disconnect()
|
||||
array1.delete(1, 1)
|
||||
array0.delete(0, 2)
|
||||
await wait()
|
||||
await users[1].reconnect()
|
||||
await users[1].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insert, then marge delete on sync', async function array7 (t) {
|
||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
await flushAll(t, users)
|
||||
await wait()
|
||||
await users[0].disconnect()
|
||||
testConnector.flushAllMessages()
|
||||
users[0].disconnect()
|
||||
array1.delete(0, 3)
|
||||
await wait()
|
||||
await users[0].reconnect()
|
||||
users[0].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
@ -174,14 +172,13 @@ test('insert & delete events for types (2)', async function array10 (t) {
|
||||
})
|
||||
|
||||
test('garbage collector', async function gc1 (t) {
|
||||
var { users, array0 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, array0 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
users[0].disconnect()
|
||||
array0.delete(0, 3)
|
||||
await wait()
|
||||
await users[0].reconnect()
|
||||
await flushAll(t, users)
|
||||
await users[0].connect()
|
||||
testConnector.flushAllMessages()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
@ -197,13 +194,13 @@ test('event target is set correctly (local)', async function array11 (t) {
|
||||
})
|
||||
|
||||
test('event target is set correctly (remote user)', async function array12 (t) {
|
||||
let { array0, array1, users } = await initArrays(t, { users: 3 })
|
||||
let { testConnector, array0, array1, users } = await initArrays(t, { users: 3 })
|
||||
var event
|
||||
array0.observe(function (e) {
|
||||
event = e
|
||||
})
|
||||
array1.insert(0, ['stuff'])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
compareEvent(t, event, {
|
||||
remote: true
|
||||
})
|
||||
@ -217,45 +214,45 @@ function getUniqueNumber () {
|
||||
}
|
||||
|
||||
var arrayTransactions = [
|
||||
function insert (t, user, chance) {
|
||||
function insert (t, user, prng) {
|
||||
const yarray = user.get('array', Y.Array)
|
||||
var uniqueNumber = getUniqueNumber()
|
||||
var content = []
|
||||
var len = chance.integer({ min: 1, max: 4 })
|
||||
var len = random.int32(prng, 1, 4)
|
||||
for (var i = 0; i < len; i++) {
|
||||
content.push(uniqueNumber)
|
||||
}
|
||||
var pos = chance.integer({ min: 0, max: yarray.length })
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, content)
|
||||
},
|
||||
function insertTypeArray (t, user, chance) {
|
||||
function insertTypeArray (t, user, prng) {
|
||||
const yarray = user.get('array', Y.Array)
|
||||
var pos = chance.integer({ min: 0, max: yarray.length })
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Array])
|
||||
var array2 = yarray.get(pos)
|
||||
array2.insert(0, [1, 2, 3, 4])
|
||||
},
|
||||
function insertTypeMap (t, user, chance) {
|
||||
function insertTypeMap (t, user, prng) {
|
||||
const yarray = user.get('array', Y.Array)
|
||||
var pos = chance.integer({ min: 0, max: yarray.length })
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Map])
|
||||
var map = yarray.get(pos)
|
||||
map.set('someprop', 42)
|
||||
map.set('someprop', 43)
|
||||
map.set('someprop', 44)
|
||||
},
|
||||
function _delete (t, user, chance) {
|
||||
function _delete (t, user, prng) {
|
||||
const yarray = user.get('array', Y.Array)
|
||||
var length = yarray.length
|
||||
if (length > 0) {
|
||||
var somePos = chance.integer({ min: 0, max: length - 1 })
|
||||
var delLength = chance.integer({ min: 1, max: Math.min(2, length - somePos) })
|
||||
var somePos = random.int32(prng, 0, length - 1)
|
||||
var delLength = random.int32(prng, 1, Math.min(2, length - somePos))
|
||||
if (yarray instanceof Y.Array) {
|
||||
if (chance.bool()) {
|
||||
if (random.bool(prng)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
somePos = chance.integer({ min: 0, max: type.length - 1 })
|
||||
delLength = chance.integer({ min: 0, max: Math.min(2, type.length - somePos) })
|
||||
somePos = random.int32(prng, 0, type.length - 1)
|
||||
delLength = random.int32(prng, 0, Math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../tests-lib/helper.js'
|
||||
import { initArrays, compareUsers, applyRandomTests } from '../tests-lib/helper.js'
|
||||
import * as Y from '../src/index.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
test('basic map tests', async function map0 (t) {
|
||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
let { testConnector, users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
users[2].disconnect()
|
||||
|
||||
map0.set('number', 1)
|
||||
@ -22,8 +24,8 @@ test('basic map tests', async function map0 (t) {
|
||||
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||
|
||||
await users[2].reconnect()
|
||||
await flushAll(t, users)
|
||||
await users[2].connect()
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
|
||||
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
|
||||
@ -39,13 +41,13 @@ test('basic map tests', async function map0 (t) {
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
let { testConnector, users, map0 } = await initArrays(t, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
map0.set('undefined', undefined)
|
||||
map0.set('null', null)
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
@ -85,11 +87,11 @@ test('Map can set custom types (Array)', async function map4 (t) {
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (converge via update)', async function map5 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
let { testConnector, users, map0 } = await initArrays(t, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
@ -99,12 +101,11 @@ test('Basic get&set of Map property (converge via update)', async function map5
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (handle conflict)', async function map6 (t) {
|
||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
let { testConnector, users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
@ -114,12 +115,11 @@ test('Basic get&set of Map property (handle conflict)', async function map6 (t)
|
||||
})
|
||||
|
||||
test('Basic get&set&delete of Map property (handle conflict)', async function map7 (t) {
|
||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
let { testConnector, users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map0.delete('stuff')
|
||||
map1.set('stuff', 'c1')
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
@ -128,13 +128,12 @@ test('Basic get&set&delete of Map property (handle conflict)', async function ma
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (handle three conflicts)', async function map8 (t) {
|
||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
let { testConnector, users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
t.compare(u.get('stuff'), 'c0')
|
||||
@ -143,19 +142,18 @@ test('Basic get&set of Map property (handle three conflicts)', async function ma
|
||||
})
|
||||
|
||||
test('Basic get&set&delete of Map property (handle three conflicts)', async function map9 (t) {
|
||||
let { users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
let { testConnector, users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
map0.set('stuff', 'deleteme')
|
||||
map0.delete('stuff')
|
||||
map1.set('stuff', 'c1')
|
||||
map2.set('stuff', 'c2')
|
||||
map3.set('stuff', 'c3')
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.get('map', Y.Map)
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
@ -173,7 +171,7 @@ test('observePath properties', async function map10 (t) {
|
||||
}
|
||||
})
|
||||
map1.set('map', new Y.Map())
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
map = map2.get('map')
|
||||
t.compare(map.get('yay'), 4)
|
||||
await compareUsers(t, users)
|
||||
@ -181,7 +179,7 @@ test('observePath properties', async function map10 (t) {
|
||||
*/
|
||||
|
||||
test('observe deep properties', async function map11 (t) {
|
||||
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
let { testConnector, users, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
var _map1 = map1.set('map', new Y.Map())
|
||||
var calls = 0
|
||||
var dmapid
|
||||
@ -194,15 +192,13 @@ test('observe deep properties', async function map11 (t) {
|
||||
dmapid = event.target.get('deepmap')._id
|
||||
})
|
||||
})
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
var _map3 = map3.get('map')
|
||||
_map3.set('deepmap', new Y.Map())
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
var _map2 = map2.get('map')
|
||||
_map2.set('deepmap', new Y.Map())
|
||||
await flushAll(t, users)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
var dmap1 = _map1.get('deepmap')
|
||||
var dmap2 = _map2.get('deepmap')
|
||||
var dmap3 = _map3.get('deepmap')
|
||||
@ -304,14 +300,14 @@ test('event has correct value when setting a primitive on a YMap (received from
|
||||
*/
|
||||
|
||||
var mapTransactions = [
|
||||
function set (t, user, chance) {
|
||||
let key = chance.pickone(['one', 'two'])
|
||||
var value = chance.string()
|
||||
function set (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
var value = random.utf16String(prng)
|
||||
user.get('map', Y.Map).set(key, value)
|
||||
},
|
||||
function setType (t, user, chance) {
|
||||
let key = chance.pickone(['one', 'two'])
|
||||
var type = chance.pickone([new Y.Array(), new Y.Map()])
|
||||
function setType (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
var type = random.oneOf(prng, [new Y.Array(), new Y.Map()])
|
||||
user.get('map', Y.Map).set(key, type)
|
||||
if (type instanceof Y.Array) {
|
||||
type.insert(0, [1, 2, 3, 4])
|
||||
@ -319,8 +315,8 @@ var mapTransactions = [
|
||||
type.set('deepkey', 'deepvalue')
|
||||
}
|
||||
},
|
||||
function _delete (t, user, chance) {
|
||||
let key = chance.pickone(['one', 'two'])
|
||||
function _delete (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
user.get('map', Y.Map).delete(key)
|
||||
}
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { initArrays, compareUsers, flushAll } from '../tests-lib/helper.js'
|
||||
import { initArrays, compareUsers } from '../tests-lib/helper.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
|
||||
proxyConsole()
|
||||
@ -64,23 +64,23 @@ test('basic format', async function text1 (t) {
|
||||
})
|
||||
|
||||
test('quill issue 1', async function quill1 (t) {
|
||||
let { users, quill0 } = await initArrays(t, { users: 2 })
|
||||
let { testConnector, users, quill0 } = await initArrays(t, { users: 2 })
|
||||
quill0.insertText(0, 'x')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, '\n', 'list', 'ordered')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, '\n', 'list', 'ordered')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('quill issue 2', async function quill2 (t) {
|
||||
let { users, quill0, text0 } = await initArrays(t, { users: 2 })
|
||||
let { testConnector, users, quill0, text0 } = await initArrays(t, { users: 2 })
|
||||
let delta
|
||||
text0.observe(function (event) {
|
||||
delta = event.delta
|
||||
})
|
||||
quill0.insertText(0, 'abc', 'bold', true)
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, 'x')
|
||||
quill0.update()
|
||||
t.compare(delta, [{ retain: 1 }, { insert: 'x', attributes: { bold: true } }])
|
||||
|
@ -1,19 +1,21 @@
|
||||
import { wait, initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../../yjs/tests-lib/helper.js'
|
||||
import { initArrays, compareUsers, applyRandomTests } from '../../yjs/tests-lib/helper.js'
|
||||
import { test } from 'cutest'
|
||||
import * as Y from '../src/index.js'
|
||||
import * as random from '../lib/random/random.js'
|
||||
|
||||
test('set property', async function xml0 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
var { testConnector, users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
xml0.setAttribute('height', '10')
|
||||
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('events', async function xml1 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
var event
|
||||
var remoteEvent
|
||||
var { testConnector, users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
var event = { attributesChanged: new Set() }
|
||||
var remoteEvent = { attributesChanged: new Set() }
|
||||
xml0.observe(function (e) {
|
||||
delete e._content
|
||||
delete e.nodes
|
||||
@ -28,21 +30,21 @@ test('events', async function xml1 (t) {
|
||||
})
|
||||
xml0.setAttribute('key', 'value')
|
||||
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)')
|
||||
// check attributeRemoved
|
||||
xml0.removeAttribute('key')
|
||||
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)')
|
||||
xml0.insert(0, [new Y.XmlText('some text')])
|
||||
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)')
|
||||
// test childRemoved
|
||||
xml0.delete(0)
|
||||
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
@ -50,13 +52,10 @@ test('events', async function xml1 (t) {
|
||||
test('attribute modifications (y -> dom)', async function xml2 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.setAttribute('height', '100px')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
|
||||
xml0.removeAttribute('height')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('height') == null, 'removeAttribute')
|
||||
xml0.setAttribute('class', 'stuffy stuff')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
@ -64,13 +63,10 @@ test('attribute modifications (y -> dom)', async function xml2 (t) {
|
||||
test('attribute modifications (dom -> y)', async function xml3 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
dom0.setAttribute('height', '100px')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
|
||||
dom0.removeAttribute('height')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('height') == null, 'removeAttribute')
|
||||
dom0.setAttribute('class', 'stuffy stuff')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
@ -79,7 +75,6 @@ test('element insert (dom -> y)', async function xml4 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
dom0.insertBefore(document.createTextNode('some text'), null)
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
await wait()
|
||||
t.assert(xml0.get(0).toString() === 'some text', 'Retrieve Text Node')
|
||||
t.assert(xml0.get(1).nodeName === 'P', 'Retrieve Element node')
|
||||
await compareUsers(t, users)
|
||||
@ -97,10 +92,8 @@ test('element insert (y -> dom)', async function xml5 (t) {
|
||||
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'one node present')
|
||||
dom0.childNodes[0].remove()
|
||||
await wait()
|
||||
t.assert(xml0.length === 0, 'no node present after delete')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
@ -117,9 +110,7 @@ test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
||||
test('delete consecutive (1) (Text)', async function xml8 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||
await wait()
|
||||
xml0.delete(1, 2)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '1', 'check content')
|
||||
@ -129,10 +120,8 @@ test('delete consecutive (1) (Text)', async function xml8 (t) {
|
||||
test('delete consecutive (2) (Text)', async function xml9 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||
await wait()
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '2', 'check content')
|
||||
@ -142,9 +131,7 @@ test('delete consecutive (2) (Text)', async function xml9 (t) {
|
||||
test('delete consecutive (1) (Element)', async function xml10 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
await wait()
|
||||
xml0.delete(1, 2)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'A', 'check content')
|
||||
@ -154,10 +141,8 @@ test('delete consecutive (1) (Element)', async function xml10 (t) {
|
||||
test('delete consecutive (2) (Element)', async function xml11 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
await wait()
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'B', 'check content')
|
||||
@ -165,12 +150,12 @@ test('delete consecutive (2) (Element)', async function xml11 (t) {
|
||||
})
|
||||
|
||||
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
||||
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
users[1].disconnect()
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')])
|
||||
await users[1].reconnect()
|
||||
await flushAll(t, users)
|
||||
await users[1].connect()
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml0.length === 6, 'check length (y)')
|
||||
t.assert(xml1.length === 6, 'check length (y) (reconnected user)')
|
||||
t.assert(dom0.childNodes.length === 6, 'check length (dom)')
|
||||
@ -179,10 +164,10 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
||||
})
|
||||
|
||||
test('move element to a different position', async function xml13 (t) {
|
||||
var { users, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
dom1.insertBefore(dom1.childNodes[0], null)
|
||||
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 0)')
|
||||
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 0)')
|
||||
@ -192,7 +177,7 @@ test('move element to a different position', async function xml13 (t) {
|
||||
})
|
||||
|
||||
test('filter node', async function xml14 (t) {
|
||||
var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
let domFilter = (nodeName, attrs) => {
|
||||
if (nodeName === 'H1') {
|
||||
return null
|
||||
@ -204,14 +189,14 @@ test('filter node', async function xml14 (t) {
|
||||
domBinding1.setFilter(domFilter)
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted')
|
||||
t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('filter attribute', async function xml15 (t) {
|
||||
var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
let domFilter = (nodeName, attrs) => {
|
||||
attrs.delete('hidden')
|
||||
return attrs
|
||||
@ -221,7 +206,7 @@ test('filter attribute', async function xml15 (t) {
|
||||
dom0.setAttribute('hidden', 'true')
|
||||
dom0.setAttribute('style', 'height: 30px')
|
||||
dom0.setAttribute('data-me', '77')
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute')
|
||||
t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update')
|
||||
t.assert(dom1.getAttribute('style') === 'height: 30px', 'User 1 received style update')
|
||||
@ -230,7 +215,7 @@ test('filter attribute', async function xml15 (t) {
|
||||
})
|
||||
|
||||
test('deep element insert', async function xml16 (t) {
|
||||
var { users, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
let deepElement = document.createElement('p')
|
||||
let boldElement = document.createElement('b')
|
||||
let attrElement = document.createElement('img')
|
||||
@ -240,7 +225,7 @@ test('deep element insert', async function xml16 (t) {
|
||||
deepElement.append(attrElement)
|
||||
dom0.append(deepElement)
|
||||
let str0 = dom0.outerHTML
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
let str1 = dom1.outerHTML
|
||||
t.compare(str0, str1, 'Dom string representation matches')
|
||||
await compareUsers(t, users)
|
||||
@ -268,7 +253,7 @@ test('treeWalker', async function xml17 (t) {
|
||||
* Incoming changes that contain malicious attributes should be deleted.
|
||||
*/
|
||||
test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
||||
var { users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
var { testConnector, users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
domBinding0.setFilter(function (nodeName, attributes) {
|
||||
attributes.delete('malicious')
|
||||
if (nodeName === 'HIDEME') {
|
||||
@ -290,70 +275,71 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
||||
let tag2 = new Y.XmlElement('tag')
|
||||
tag2.setAttribute('isHidden', 'true')
|
||||
paragraph.insert(0, [tag2])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
// check dom
|
||||
domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true')
|
||||
domBinding0.typeToDom.get(span).setAttribute('malicious', 'true')
|
||||
// check incoming attributes
|
||||
xml1.get(0).get(0).setAttribute('malicious', 'true')
|
||||
xml1.insert(0, [new Y.XmlElement('hideMe')])
|
||||
await flushAll(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
// TODO: move elements
|
||||
var xmlTransactions = [
|
||||
function attributeChange (t, user, chance) {
|
||||
user.dom.setAttribute(chance.word(), chance.word())
|
||||
function attributeChange (t, user, prng) {
|
||||
// random.word generates non-empty words. prepend something
|
||||
user.dom.setAttribute('_' + random.word(prng), random.word(prng))
|
||||
},
|
||||
function attributeChangeHidden (t, user, chance) {
|
||||
user.dom.setAttribute('hidden', chance.word())
|
||||
function attributeChangeHidden (t, user, prng) {
|
||||
user.dom.setAttribute('hidden', random.word(prng))
|
||||
},
|
||||
function insertText (t, user, chance) {
|
||||
function insertText (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
dom.insertBefore(document.createTextNode(chance.word()), succ)
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createTextNode(random.word(prng)), succ)
|
||||
},
|
||||
function insertHiddenDom (t, user, chance) {
|
||||
function insertHiddenDom (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createElement('hidden'), succ)
|
||||
},
|
||||
function insertDom (t, user, chance) {
|
||||
function insertDom (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
dom.insertBefore(document.createElement(chance.word()), succ)
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createElement('my-' + random.word(prng)), succ)
|
||||
},
|
||||
function deleteChild (t, user, chance) {
|
||||
function deleteChild (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.childNodes.length > 0) {
|
||||
var d = chance.pickone(dom.childNodes)
|
||||
var d = random.oneOf(prng, dom.childNodes)
|
||||
d.remove()
|
||||
}
|
||||
},
|
||||
function insertTextSecondLayer (t, user, chance) {
|
||||
function insertTextSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createTextNode(chance.word()), succ)
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createTextNode(random.word(prng)), succ)
|
||||
}
|
||||
},
|
||||
function insertDomSecondLayer (t, user, chance) {
|
||||
function insertDomSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createElement(chance.word()), succ)
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createElement('my-' + random.word(prng)), succ)
|
||||
}
|
||||
},
|
||||
function deleteChildSecondLayer (t, user, chance) {
|
||||
function deleteChildSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
if (dom2.childNodes.length > 0) {
|
||||
let d = chance.pickone(dom2.childNodes)
|
||||
let d = random.oneOf(prng, dom2.childNodes)
|
||||
d.remove()
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,224 @@
|
||||
|
||||
import _Y from '../src/Y.dist.js'
|
||||
import { DomBinding } from '../src/Y.js'
|
||||
import TestConnector from './test-connector.js'
|
||||
|
||||
import Chance from 'chance'
|
||||
import * as Y from '../src/index.js'
|
||||
import ItemJSON from '../src/Struct/ItemJSON.js'
|
||||
import ItemString from '../src/Struct/ItemString.js'
|
||||
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
|
||||
import Quill from 'quill'
|
||||
import GC from '../src/Struct/GC.js'
|
||||
import * as random from '../lib/random/random.js'
|
||||
import * as message from '../src/message.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
|
||||
export const Y = _Y
|
||||
export * from '../src/index.js'
|
||||
|
||||
export const database = { name: 'memory' }
|
||||
export const connector = { name: 'test', url: 'http://localhost:1234' }
|
||||
|
||||
Y.test = TestConnector
|
||||
|
||||
function getStateSet (y) {
|
||||
let ss = {}
|
||||
for (let [user, clock] of y.ss.state) {
|
||||
ss[user] = clock
|
||||
}
|
||||
return ss
|
||||
/**
|
||||
* @param {TestYInstance} y
|
||||
* @param {Y.Transaction} transaction
|
||||
*/
|
||||
const afterTransaction = (y, transaction) => {
|
||||
y.mMux(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = encoding.createEncoder()
|
||||
message.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
broadcastMessage(y, encoding.toBuffer(encoder))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getDeleteSet (y) {
|
||||
export class TestYInstance extends Y.Y {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
*/
|
||||
constructor (testConnector) {
|
||||
super()
|
||||
/**
|
||||
* @type {TestConnector}
|
||||
*/
|
||||
this.tc = testConnector
|
||||
/**
|
||||
* @type {Map<TestYInstance, Array<ArrayBuffer>>}
|
||||
*/
|
||||
this.receiving = new Map()
|
||||
/**
|
||||
* Message mutex
|
||||
* @type {Function}
|
||||
*/
|
||||
this.mMux = createMutex()
|
||||
testConnector.allConns.add(this)
|
||||
// set up observe on local model
|
||||
this.on('afterTransaction', afterTransaction)
|
||||
this.connect()
|
||||
}
|
||||
/**
|
||||
* Disconnect from TestConnector.
|
||||
*/
|
||||
disconnect () {
|
||||
this.receiving = new Map()
|
||||
this.tc.onlineConns.delete(this)
|
||||
}
|
||||
/**
|
||||
* Append yourself to the list of known Y instances in testconnector.
|
||||
* Also initiate sync with all clients.
|
||||
*/
|
||||
connect () {
|
||||
if (!this.tc.onlineConns.has(this)) {
|
||||
const encoder = encoding.createEncoder()
|
||||
message.writeSyncStep1(encoder, this)
|
||||
// publish SyncStep1
|
||||
broadcastMessage(this, encoding.toBuffer(encoder))
|
||||
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||
// remote instance sends instance to this instance
|
||||
const encoder = encoding.createEncoder()
|
||||
message.writeSyncStep1(encoder, remoteYInstance)
|
||||
this._receive(encoding.toBuffer(encoder), remoteYInstance)
|
||||
})
|
||||
this.tc.onlineConns.add(this)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Receive a message from another client. This message is only appended to the list of receiving messages.
|
||||
* TestConnector decides when this client actually reads this message.
|
||||
*
|
||||
* @param {ArrayBuffer} message
|
||||
* @param {TestYInstance} remoteClient
|
||||
*/
|
||||
_receive (message, remoteClient) {
|
||||
let messages = this.receiving.get(remoteClient)
|
||||
if (messages === undefined) {
|
||||
messages = []
|
||||
this.receiving.set(remoteClient, messages)
|
||||
}
|
||||
messages.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of TestYInstances.
|
||||
*
|
||||
* The TestYInstances add/remove themselves from the list of connections maintained in this object.
|
||||
* I think it makes sense. Deal with it.
|
||||
*/
|
||||
export class TestConnector {
|
||||
constructor (prng) {
|
||||
/**
|
||||
* @type {Set<TestYInstance>}
|
||||
*/
|
||||
this.allConns = new Set()
|
||||
/**
|
||||
* @type {Set<TestYInstance>}
|
||||
*/
|
||||
this.onlineConns = new Set()
|
||||
/**
|
||||
* @type {random.PRNG}
|
||||
*/
|
||||
this.prng = prng
|
||||
}
|
||||
/**
|
||||
* Create a new Y instance and add it to the list of connections
|
||||
*/
|
||||
createY () {
|
||||
return new TestYInstance(this)
|
||||
}
|
||||
/**
|
||||
* Choose random connection and flush a random message from a random sender.
|
||||
*
|
||||
* If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise.
|
||||
* @return {boolean}
|
||||
*/
|
||||
flushRandomMessage () {
|
||||
const prng = this.prng
|
||||
const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0)
|
||||
if (conns.length > 0) {
|
||||
const receiver = random.oneOf(prng, conns)
|
||||
const [sender, messages] = random.oneOf(prng, Array.from(receiver.receiving))
|
||||
const m = messages.shift()
|
||||
if (messages.length === 0) {
|
||||
receiver.receiving.delete(sender)
|
||||
}
|
||||
const encoder = encoding.createEncoder()
|
||||
receiver.mMux(() => {
|
||||
// do not publish data created when this function is executed (could be ss2 or update message)
|
||||
message.readMessage(decoding.createDecoder(m), encoder, receiver)
|
||||
})
|
||||
if (encoding.length(encoder) > 0) {
|
||||
// send reply message
|
||||
sender._receive(encoding.toBuffer(encoder), receiver)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @return {boolean} True iff this function actually flushed something
|
||||
*/
|
||||
flushAllMessages () {
|
||||
let didSomething = false
|
||||
while (this.flushRandomMessage()) {
|
||||
didSomething = true
|
||||
}
|
||||
return didSomething
|
||||
}
|
||||
reconnectAll () {
|
||||
this.allConns.forEach(conn => conn.connect())
|
||||
}
|
||||
disconnectAll () {
|
||||
this.allConns.forEach(conn => conn.disconnect())
|
||||
}
|
||||
syncAll () {
|
||||
this.reconnectAll()
|
||||
this.flushAllMessages()
|
||||
}
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to disconnect a randon connection.
|
||||
*/
|
||||
disconnectRandom () {
|
||||
if (this.onlineConns.size === 0) {
|
||||
return false
|
||||
}
|
||||
random.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to reconnect a random connection.
|
||||
*/
|
||||
reconnectRandom () {
|
||||
const reconnectable = []
|
||||
this.allConns.forEach(conn => {
|
||||
if (!this.onlineConns.has(conn)) {
|
||||
reconnectable.push(conn)
|
||||
}
|
||||
})
|
||||
if (reconnectable.length === 0) {
|
||||
return false
|
||||
}
|
||||
random.oneOf(this.prng, reconnectable).connect()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y // publish message created by `y` to all other online clients
|
||||
* @param {ArrayBuffer} m
|
||||
*/
|
||||
const broadcastMessage = (y, m) =>
|
||||
y.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== y) {
|
||||
remoteYInstance._receive(m, y)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Convert DS to a proper DeleteSet of Map.
|
||||
*
|
||||
* @param {Y.Y} y
|
||||
* @return {Object<number, Array<[number, number, boolean]>>}
|
||||
*/
|
||||
const getDeleteSet = y => {
|
||||
/**
|
||||
* @type {Object<number, Array<[number, number, boolean]>}
|
||||
*/
|
||||
var ds = {}
|
||||
y.ds.iterate(null, null, function (n) {
|
||||
var user = n._id.user
|
||||
@ -42,29 +235,28 @@ function getDeleteSet (y) {
|
||||
return ds
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* 1. reconnect and flush all
|
||||
* 2. user 0 gc
|
||||
* 3. get type content
|
||||
* 4. disconnect & reconnect all (so gc is propagated)
|
||||
* 5. compare os, ds, ss
|
||||
*
|
||||
* @param {any} t
|
||||
* @param {Array<TestYInstance>} users
|
||||
*/
|
||||
export async function compareUsers (t, users) {
|
||||
await Promise.all(users.map(u => u.reconnect()))
|
||||
if (users[0].connector.testRoom == null) {
|
||||
await wait(100)
|
||||
}
|
||||
await flushAll(t, users)
|
||||
await wait()
|
||||
await flushAll(t, users)
|
||||
await wait()
|
||||
await flushAll(t, users)
|
||||
await wait()
|
||||
await flushAll(t, users)
|
||||
export function compareUsers (t, users) {
|
||||
users.forEach(u => u.connect())
|
||||
do {
|
||||
users.forEach(u => {
|
||||
// flush dom changes
|
||||
u.domBinding._beforeTransactionHandler(null, null, false)
|
||||
})
|
||||
} while (users[0].tc.flushAllMessages())
|
||||
|
||||
var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
||||
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
|
||||
var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString())
|
||||
var userXmlValues = users.map(u => u.define('xml', Y.XmlElement).toString())
|
||||
var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
|
||||
var userQuillValues = users.map(u => {
|
||||
u.quill.update('yjs') // get latest changes
|
||||
@ -81,7 +273,8 @@ export async function compareUsers (t, users) {
|
||||
json = {
|
||||
type: 'GC',
|
||||
id: op._id,
|
||||
length: op._length
|
||||
length: op._length,
|
||||
content: null
|
||||
}
|
||||
} else {
|
||||
json = {
|
||||
@ -90,7 +283,8 @@ export async function compareUsers (t, users) {
|
||||
right: op._right === null ? null : op._right._id,
|
||||
length: op._length,
|
||||
deleted: op._deleted,
|
||||
parent: op._parent._id
|
||||
parent: op._parent._id,
|
||||
content: null
|
||||
}
|
||||
}
|
||||
if (op instanceof ItemJSON || op instanceof ItemString) {
|
||||
@ -100,11 +294,15 @@ export async function compareUsers (t, users) {
|
||||
})
|
||||
data.os = ops
|
||||
data.ds = getDeleteSet(u)
|
||||
data.ss = getStateSet(u)
|
||||
const ss = {}
|
||||
u.ss.state.forEach((user, clock) => {
|
||||
ss[user] = clock
|
||||
})
|
||||
data.ss = ss
|
||||
return data
|
||||
})
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
await t.asyncGroup(async () => {
|
||||
t.group(() => {
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
||||
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
|
||||
@ -115,10 +313,20 @@ export async function compareUsers (t, users) {
|
||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||
}, `Compare user${i} with user${i + 1}`)
|
||||
}
|
||||
users.forEach(user => {
|
||||
if (user._missingStructs.size !== 0) {
|
||||
t.fail('missing structs should mes empty!')
|
||||
}
|
||||
})
|
||||
users.map(u => u.destroy())
|
||||
}
|
||||
|
||||
function domFilter (nodeName, attrs) {
|
||||
/**
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {null|Map<string, string>}
|
||||
*/
|
||||
function filter (nodeName, attrs) {
|
||||
if (nodeName === 'HIDDEN') {
|
||||
return null
|
||||
}
|
||||
@ -126,30 +334,27 @@ function domFilter (nodeName, attrs) {
|
||||
return attrs
|
||||
}
|
||||
|
||||
export async function initArrays (t, opts) {
|
||||
/**
|
||||
* @param {any} t
|
||||
* @param {any} opts
|
||||
* @return {any}
|
||||
*/
|
||||
export function initArrays (t, opts) {
|
||||
var result = {
|
||||
users: []
|
||||
}
|
||||
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
||||
var conn = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, connector)
|
||||
var prng = opts.prng || random.createPRNG(t.getSeed())
|
||||
const testConnector = new TestConnector(prng)
|
||||
result.testConnector = testConnector
|
||||
for (let i = 0; i < opts.users; i++) {
|
||||
let connOpts
|
||||
if (i === 0) {
|
||||
connOpts = Object.assign({ role: 'master' }, conn)
|
||||
} else {
|
||||
connOpts = Object.assign({ role: 'slave' }, conn)
|
||||
}
|
||||
let y = new Y(connOpts.room, {
|
||||
userID: i, // evil hackery, don't try this at home
|
||||
connector: connOpts
|
||||
})
|
||||
let y = testConnector.createY()
|
||||
result.users.push(y)
|
||||
result['array' + i] = y.define('array', Y.Array)
|
||||
result['map' + i] = y.define('map', Y.Map)
|
||||
const yxml = y.define('xml', Y.XmlElement)
|
||||
result['xml' + i] = yxml
|
||||
const dom = document.createElement('my-dom')
|
||||
const domBinding = new DomBinding(yxml, dom, { domFilter })
|
||||
const domBinding = new Y.DomBinding(yxml, dom, { filter })
|
||||
result['domBinding' + i] = domBinding
|
||||
result['dom' + i] = dom
|
||||
const textType = y.define('text', Y.Text)
|
||||
@ -159,116 +364,38 @@ export async function initArrays (t, opts) {
|
||||
result['quill' + i] = quill
|
||||
y.quill = quill // put quill on the y object (so we can use it later)
|
||||
y.dom = dom
|
||||
y.on('afterTransaction', function () {
|
||||
for (let missing of y._missingStructs.values()) {
|
||||
if (missing.size > 0) {
|
||||
console.error(new Error('Test check in "afterTransaction": missing should be empty!'))
|
||||
}
|
||||
}
|
||||
})
|
||||
y.domBinding = domBinding
|
||||
}
|
||||
result.array0.delete(0, result.array0.length)
|
||||
if (result.users[0].connector.testRoom != null) {
|
||||
// flush for sync if test-connector
|
||||
await result.users[0].connector.testRoom.flushAll(result.users)
|
||||
}
|
||||
await Promise.all(result.users.map(u => {
|
||||
return new Promise(function (resolve) {
|
||||
u.connector.whenSynced(resolve)
|
||||
})
|
||||
}))
|
||||
await flushAll(t, result.users)
|
||||
testConnector.syncAll()
|
||||
return result
|
||||
}
|
||||
|
||||
export async function flushAll (t, users) {
|
||||
// users = users.filter(u => u.connector.isSynced)
|
||||
if (users.length === 0) {
|
||||
return
|
||||
}
|
||||
await wait(10)
|
||||
if (users[0].connector.testRoom != null) {
|
||||
// use flushAll method specified in Test Connector
|
||||
await users[0].connector.testRoom.flushAll(users)
|
||||
} else {
|
||||
var flushCounter = users[0].get('flushHelper', Y.Map).get('0') || 0
|
||||
flushCounter++
|
||||
await Promise.all(users.map(async (u, i) => {
|
||||
// wait for all users to set the flush counter to the same value
|
||||
await new Promise(resolve => {
|
||||
function observer () {
|
||||
var allUsersReceivedUpdate = true
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
if (u.get('flushHelper', Y.Map).get(i + '') !== flushCounter) {
|
||||
allUsersReceivedUpdate = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (allUsersReceivedUpdate) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
u.get('flushHelper', Y.Map).observe(observer)
|
||||
u.get('flushHelper').set(i + '', flushCounter)
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export async function flushSome (t, users) {
|
||||
if (users[0].connector.testRoom == null) {
|
||||
// if not test-connector, wait for some time for operations to arrive
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
|
||||
export function wait (t) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, t != null ? t : 100)
|
||||
})
|
||||
}
|
||||
|
||||
export async function applyRandomTests (t, mods, iterations) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
var initInformation = await initArrays(t, { users: 5, chance: chance })
|
||||
let { users } = initInformation
|
||||
export function applyRandomTests (t, mods, iterations) {
|
||||
const prng = random.createPRNG(t.getSeed())
|
||||
const result = initArrays(t, { users: 5, prng })
|
||||
const { testConnector, users } = result
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
if (chance.bool({likelihood: 10})) {
|
||||
// 10% chance to disconnect/reconnect a user
|
||||
// we make sure that the first users always is connected
|
||||
let user = chance.pickone(users.slice(1))
|
||||
if (user.connector.isSynced) {
|
||||
if (users.filter(u => u.connector.isSynced).length > 1) {
|
||||
// make sure that at least one user remains in the room
|
||||
await user.disconnect()
|
||||
if (users[0].connector.testRoom == null) {
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
if (random.int32(prng, 0, 100) <= 2) {
|
||||
// 2% chance to disconnect/reconnect a random user
|
||||
if (random.bool(prng)) {
|
||||
testConnector.disconnectRandom()
|
||||
} else {
|
||||
await user.reconnect()
|
||||
if (users[0].connector.testRoom == null) {
|
||||
await wait(100)
|
||||
}
|
||||
await new Promise(function (resolve) {
|
||||
user.connector.whenSynced(resolve)
|
||||
})
|
||||
testConnector.reconnectRandom()
|
||||
}
|
||||
} else if (chance.bool({likelihood: 5})) {
|
||||
// 20%*!prev chance to flush all & garbagecollect
|
||||
} else if (random.int32(prng, 0, 100) <= 1) {
|
||||
// 1% chance to flush all & garbagecollect
|
||||
// TODO: We do not gc all users as this does not work yet
|
||||
// await garbageCollectUsers(t, users)
|
||||
await flushAll(t, users)
|
||||
// await users[0].db.emptyGarbageCollector()
|
||||
await flushAll(t, users)
|
||||
} else if (chance.bool({likelihood: 10})) {
|
||||
// 20%*!prev chance to flush some operations
|
||||
await flushSome(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
// await users[0].db.emptyGarbageCollector() // TODO: reintroduce GC tests!
|
||||
} else if (random.int32(prng, 0, 100) <= 50) {
|
||||
// 50% chance to flush a random message
|
||||
testConnector.flushRandomMessage()
|
||||
}
|
||||
let user = chance.pickone(users)
|
||||
var test = chance.pickone(mods)
|
||||
test(t, user, chance)
|
||||
let user = random.oneOf(prng, users)
|
||||
var test = random.oneOf(prng, mods)
|
||||
test(t, user, prng)
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
return initInformation
|
||||
compareUsers(t, users)
|
||||
return result
|
||||
}
|
||||
|
@ -1,162 +0,0 @@
|
||||
import { wait } from './helper.js'
|
||||
import { messageToString } from '../src/MessageHandler/messageToString.js'
|
||||
import AbstractConnector from '../src/Connector.js'
|
||||
|
||||
var rooms = {}
|
||||
|
||||
export class TestRoom {
|
||||
constructor (roomname) {
|
||||
this.room = roomname
|
||||
this.users = new Map()
|
||||
}
|
||||
join (connector) {
|
||||
const userID = connector.y.userID
|
||||
this.users.set(userID, connector)
|
||||
for (let [uid, user] of this.users) {
|
||||
if (uid !== userID && (user.role === 'master' || connector.role === 'master')) {
|
||||
// The order is important because there is no timeout in send/receiveMessage
|
||||
// (the user that receives a sync step must already now about the sender)
|
||||
if (user.role === 'master') {
|
||||
connector.userJoined(uid, user.role)
|
||||
user.userJoined(userID, connector.role)
|
||||
} else if (connector.role === 'master') {
|
||||
user.userJoined(userID, connector.role)
|
||||
connector.userJoined(uid, user.role)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
leave (connector) {
|
||||
this.users.delete(connector.y.userID)
|
||||
this.users.forEach(user => {
|
||||
user.userLeft(connector.y.userID)
|
||||
})
|
||||
}
|
||||
send (sender, receiver, m) {
|
||||
var user = this.users.get(receiver)
|
||||
if (user != null) {
|
||||
user.receiveMessage(sender, m)
|
||||
}
|
||||
}
|
||||
broadcast (sender, m) {
|
||||
this.users.forEach((user, receiver) => {
|
||||
this.send(sender, receiver, m)
|
||||
})
|
||||
}
|
||||
async flushAll (users) {
|
||||
let flushing = true
|
||||
let allUsers = Array.from(this.users.values())
|
||||
if (users == null) {
|
||||
users = allUsers.map(user => user.y)
|
||||
}
|
||||
while (flushing) {
|
||||
await wait(10)
|
||||
let res = await Promise.all(allUsers.map(user => user._flushAll(users)))
|
||||
flushing = res.some(status => status === 'flushing')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTestRoom (roomname) {
|
||||
if (rooms[roomname] == null) {
|
||||
rooms[roomname] = new TestRoom(roomname)
|
||||
}
|
||||
return rooms[roomname]
|
||||
}
|
||||
|
||||
export default class TestConnector extends AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
if (options.room == null) {
|
||||
throw new Error('You must define a room name!')
|
||||
}
|
||||
options.forwardAppliedOperations = options.role === 'master'
|
||||
super(y, options)
|
||||
this.options = options
|
||||
this.room = options.room
|
||||
this.chance = options.chance
|
||||
this.testRoom = getTestRoom(this.room)
|
||||
this.testRoom.join(this)
|
||||
}
|
||||
disconnect () {
|
||||
this.testRoom.leave(this)
|
||||
return super.disconnect()
|
||||
}
|
||||
logBufferParsed () {
|
||||
console.log(' === Logging buffer of user ' + this.y.userID + ' === ')
|
||||
for (let [user, conn] of this.connections) {
|
||||
console.log(` ${user}:`)
|
||||
for (let i = 0; i < conn.buffer.length; i++) {
|
||||
console.log(messageToString(conn.buffer[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnect () {
|
||||
this.testRoom.join(this)
|
||||
super.reconnect()
|
||||
return new Promise(resolve => {
|
||||
this.whenSynced(resolve)
|
||||
})
|
||||
}
|
||||
send (uid, message) {
|
||||
super.send(uid, message)
|
||||
this.testRoom.send(this.y.userID, uid, message)
|
||||
}
|
||||
broadcast (message) {
|
||||
super.broadcast(message)
|
||||
this.testRoom.broadcast(this.y.userID, message)
|
||||
}
|
||||
async whenSynced (f) {
|
||||
var synced = false
|
||||
var periodicFlushTillSync = () => {
|
||||
if (synced) {
|
||||
f()
|
||||
} else {
|
||||
this.testRoom.flushAll([this.y]).then(function () {
|
||||
setTimeout(periodicFlushTillSync, 10)
|
||||
})
|
||||
}
|
||||
}
|
||||
periodicFlushTillSync()
|
||||
return super.whenSynced(function () {
|
||||
synced = true
|
||||
})
|
||||
}
|
||||
receiveMessage (sender, m) {
|
||||
if (this.y.userID !== sender && this.connections.has(sender)) {
|
||||
var buffer = this.connections.get(sender).buffer
|
||||
if (buffer == null) {
|
||||
buffer = this.connections.get(sender).buffer = []
|
||||
}
|
||||
buffer.push(m)
|
||||
if (this.chance.bool({likelihood: 30})) {
|
||||
// flush 1/2 with 30% chance
|
||||
var flushLength = Math.round(buffer.length / 2)
|
||||
buffer.splice(0, flushLength).forEach(m => {
|
||||
super.receiveMessage(sender, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
async _flushAll (flushUsers) {
|
||||
if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) {
|
||||
// this one needs to sync with every other user
|
||||
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
|
||||
}
|
||||
for (let i = 0; i < flushUsers.length; i++) {
|
||||
let userID = flushUsers[i].connector.y.userID
|
||||
if (userID !== this.y.userID && this.connections.has(userID)) {
|
||||
let buffer = this.connections.get(userID).buffer
|
||||
if (buffer != null) {
|
||||
var messages = buffer.splice(0)
|
||||
for (let j = 0; j < messages.length; j++) {
|
||||
super.receiveMessage(userID, messages[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'done'
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user