diff --git a/README.md b/README.md index 0217c70..3354ea0 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# P2P Poll App \ No newline at end of file +# P2P Poll App + +## Setup +mkdir yjs-poll +cd yjs-poll +npm init -y +npm install y-websocket ws diff --git a/yjs-poll/backend.js b/yjs-poll/backend.js new file mode 100644 index 0000000..5dc95cd --- /dev/null +++ b/yjs-poll/backend.js @@ -0,0 +1,28 @@ +import { WebSocketServer } from 'ws'; + +// Create a WebSocket server +const WS_PORT = 8080; +const wss = new WebSocketServer({ port: WS_PORT }); + +console.log('WebSocket server is running on ws://localhost:' + String(WS_PORT)); + +// Connection event handler +wss.on('connection', (ws) => { + console.log('Client connected'); + + // Message event handler + ws.on('message', (message) => { + let msg_str = String(message); + console.log("Received: " + msg_str); + + // If this is a text or state message (no Yjs logic) - echo the message back to the client + if (msg_str.startsWith("TEXT_MESSAGE") | msg_str.startsWith("STATE_MESSAGE")) { + ws.send(msg_str); + } + }); + + // Close event handler + ws.on('close', () => { + console.log('Client disconnected'); + }); +}); diff --git a/yjs-poll/frontend.html b/yjs-poll/frontend.html new file mode 100644 index 0000000..09f04b5 --- /dev/null +++ b/yjs-poll/frontend.html @@ -0,0 +1,191 @@ + + +
+import * as array from 'lib0/array'+
array.last(arr: ArrayLike<L>): LReturn the last element of an array. The element must exist
array.create(): Array<C>array.copy(a: Array<D>): Array<D>array.appendTo(dest: Array<M>, src: Array<M>)Append elements from src to dest
array.from(arraylike: ArrayLike<T>|Iterable<T>): TTransforms something array-like to an actual Array.
array.every(arr: ARR, f: function(ITEM, number, ARR):boolean): booleanTrue iff condition holds on every element in the Array.
array.some(arr: ARR, f: function(S, number, ARR):boolean): booleanTrue iff condition holds on some element in the Array.
array.equalFlat(a: ArrayLike<ELEM>, b: ArrayLike<ELEM>): booleanarray.flatten(arr: Array<Array<ELEM>>): Array<ELEM>array.isArrayarray.unique(arr: Array<T>): Array<T>array.uniqueBy(arr: ArrayLike<T>, mapper: function(T):M): Array<T>import * as binary from 'lib0/binary'+
binary.BIT1: numbern-th bit activated.
binary.BIT2binary.BIT3binary.BIT4binary.BIT5binary.BIT6binary.BIT7binary.BIT8binary.BIT9binary.BIT10binary.BIT11binary.BIT12binary.BIT13binary.BIT14binary.BIT15binary.BIT16binary.BIT17binary.BIT18binary.BIT19binary.BIT20binary.BIT21binary.BIT22binary.BIT23binary.BIT24binary.BIT25binary.BIT26binary.BIT27binary.BIT28binary.BIT29binary.BIT30binary.BIT31binary.BIT32binary.BITS0: numberFirst n bits activated.
binary.BITS1binary.BITS2binary.BITS3binary.BITS4binary.BITS5binary.BITS6binary.BITS7binary.BITS8binary.BITS9binary.BITS10binary.BITS11binary.BITS12binary.BITS13binary.BITS14binary.BITS15binary.BITS16binary.BITS17binary.BITS18binary.BITS19binary.BITS20binary.BITS21binary.BITS22binary.BITS23binary.BITS24binary.BITS25binary.BITS26binary.BITS27binary.BITS28binary.BITS29binary.BITS30binary.BITS31: numberbinary.BITS32: numberimport * as broadcastchannel from 'lib0/broadcastchannel'+ +
// In browser window A:
+broadcastchannel.subscribe('my events', data => console.log(data))
+broadcastchannel.publish('my events', 'Hello world!') // => A: 'Hello world!' fires synchronously in same tab
+
+// In browser window B:
+broadcastchannel.publish('my events', 'hello from tab B') // => A: 'hello from tab B'
+
+broadcastchannel.subscribe(room: string, f: function(any, any):any)Subscribe to global publish events.
broadcastchannel.unsubscribe(room: string, f: function(any, any):any)Unsubscribe from publish global events.
broadcastchannel.publish(room: string, data: any, origin: any)Publish data to all subscribers (including subscribers on this tab)
import * as buffer from 'lib0/buffer'+
buffer.createUint8ArrayFromLen(len: number)buffer.createUint8ArrayViewFromArrayBuffer(buffer: ArrayBuffer, byteOffset: number, length: number)Create Uint8Array with initial content from buffer
buffer.createUint8ArrayFromArrayBuffer(buffer: ArrayBuffer)Create Uint8Array with initial content from buffer
buffer.toBase64buffer.fromBase64buffer.copyUint8Array(uint8Array: Uint8Array): Uint8ArrayCopy the content of an Uint8Array view to a new ArrayBuffer.
buffer.encodeAny(data: any): Uint8ArrayEncode anything as a UInt8Array. It's a pun on typescripts's any type.
+See encoding.writeAny for more information.
buffer.decodeAny(buf: Uint8Array): anyDecode an any-encoded value.
import * as cache from 'lib0/cache'+
new cache.Cache(timeout: number)cache.removeStale(cache: module:cache.Cache<K, V>): numbercache.set(cache: module:cache.Cache<K, V>, key: K, value: V)cache.get(cache: module:cache.Cache<K, V>, key: K): V | undefinedcache.refreshTimeout(cache: module:cache.Cache<K, V>, key: K)cache.getAsync(cache: module:cache.Cache<K, V>, key: K): V | Promise<V> | undefinedWorks well in conjunktion with setIfUndefined which has an async init function. +Using getAsync & setIfUndefined ensures that the init function is only called once.
cache.remove(cache: module:cache.Cache<K, V>, key: K)cache.setIfUndefined(cache: module:cache.Cache<K, V>, key: K, init: function():Promise<V>, removeNull: boolean): Promise<V> | Vcache.create(timeout: number)import * as component from 'lib0/component'+
component.registry: CustomElementRegistrycomponent.define(name: string, constr: any, opts: ElementDefinitionOptions)component.whenDefined(name: string): Promise<CustomElementConstructor>new component.Lib0Component(state: S)component.Lib0Component#state: S|nullcomponent.Lib0Component#setState(state: S, forceStateUpdate: boolean)component.Lib0Component#updateState(stateUpdate: any)component.createComponent(name: string, cnf: module:component~CONF<T>): Class<module:component.Lib0Component>component.createComponentDefiner(definer: function)component.defineListComponentcomponent.defineLazyLoadingComponentimport * as conditions from 'lib0/conditions'+
conditions.undefinedToNullimport * as crypto from 'lib0/crypto'+
y(data: string | Uint8Array): Uint8ArrayymmetricKey(secret: string | Uint8Array, salt: string | Uint8Array, opts: Object, opts.extractable: boolean, opts.usages: Array<'sign'|'verify'|'encrypt'|'decrypt'>): PromiseLike<CryptoKey>ymmetricKey()eAsymmetricKey(opts: Object, opts.extractable: boolean, opts.usages: Array<'sign'|'verify'|'encrypt'|'decrypt'>)eAsymmetricKey()ey(key: CryptoKey)ey()ymmetricKey(jwk: any, opts: Object, opts.extractable: boolean, opts.usages: Array<'sign'|'verify'|'encrypt'|'decrypt'>)ymmetricKey()symmetricKey(jwk: any, opts: Object, opts.extractable: boolean, opts.usages: Array<'sign'|'verify'|'encrypt'|'decrypt'>)symmetricKey()(data: Uint8Array, key: CryptoKey): PromiseLike<Uint8Array>()(data: Uint8Array, key: CryptoKey): PromiseLike<Uint8Array>()(data: Uint8Array, privateKey: CryptoKey): PromiseLike<Uint8Array>()(signature: Uint8Array, data: Uint8Array, publicKey: CryptoKey): PromiseLike<boolean>()import * as decoding from 'lib0/decoding'+ +
Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function.
+Encodes numbers in little-endian order (least to most significant byte order) +and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/) +which is also used in Protocol Buffers.
+// encoding step
+const encoder = encoding.createEncoder()
+encoding.writeVarUint(encoder, 256)
+encoding.writeVarString(encoder, 'Hello world!')
+const buf = encoding.toUint8Array(encoder)
+
+// decoding step
+const decoder = decoding.createDecoder(buf)
+decoding.readVarUint(decoder) // => 256
+decoding.readVarString(decoder) // => 'Hello world!'
+decoding.hasContent(decoder) // => false - all data is read
+
+new decoding.Decoder(uint8Array: Uint8Array)A Decoder handles the decoding of an Uint8Array.
decoding.Decoder#arr: Uint8ArrayDecoding target.
decoding.Decoder#pos: numberCurrent decoding position.
decoding.createDecoder(uint8Array: Uint8Array): module:decoding.Decoderdecoding.hasContent(decoder: module:decoding.Decoder): booleandecoding.clone(decoder: module:decoding.Decoder, newPos: number): module:decoding.DecoderClone a decoder instance. +Optionally set a new position parameter.
decoding.readUint8Array(decoder: module:decoding.Decoder, len: number): Uint8ArrayCreate an Uint8Array view of the next len bytes and advance the position by len.
Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
+Use buffer.copyUint8Array to copy the result into a new Uint8Array.
decoding.readVarUint8Array(decoder: module:decoding.Decoder): Uint8ArrayRead variable length Uint8Array.
+Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
+Use buffer.copyUint8Array to copy the result into a new Uint8Array.
decoding.readTailAsUint8Array(decoder: module:decoding.Decoder): Uint8ArrayRead the rest of the content as an ArrayBuffer
decoding.skip8(decoder: module:decoding.Decoder): numberSkip one byte, jump to the next position.
decoding.readUint8(decoder: module:decoding.Decoder): numberRead one byte as unsigned integer.
decoding.readUint16(decoder: module:decoding.Decoder): numberRead 2 bytes as unsigned integer.
decoding.readUint32(decoder: module:decoding.Decoder): numberRead 4 bytes as unsigned integer.
decoding.readUint32BigEndian(decoder: module:decoding.Decoder): numberRead 4 bytes as unsigned integer in big endian order. +(most significant byte first)
decoding.peekUint8(decoder: module:decoding.Decoder): numberLook ahead without incrementing the position +to the next byte and read it as unsigned integer.
decoding.peekUint16(decoder: module:decoding.Decoder): numberLook ahead without incrementing the position +to the next byte and read it as unsigned integer.
decoding.peekUint32(decoder: module:decoding.Decoder): numberLook ahead without incrementing the position +to the next byte and read it as unsigned integer.
decoding.readVarUint(decoder: module:decoding.Decoder): numberRead unsigned integer (32bit) with variable length. +1/8th of the storage is used as encoding overhead.
+decoding.readVarInt(decoder: module:decoding.Decoder): numberRead signed integer (32bit) with variable length. +1/8th of the storage is used as encoding overhead.
+decoding.peekVarUint(decoder: module:decoding.Decoder): numberLook ahead and read varUint without incrementing position
decoding.peekVarInt(decoder: module:decoding.Decoder): numberLook ahead and read varUint without incrementing position
decoding.readVarStringdecoding.peekVarString(decoder: module:decoding.Decoder): stringLook ahead and read varString without incrementing position
decoding.readFromDataView(decoder: module:decoding.Decoder, len: number): DataViewdecoding.readFloat32(decoder: module:decoding.Decoder)decoding.readFloat64(decoder: module:decoding.Decoder)decoding.readBigInt64(decoder: module:decoding.Decoder)decoding.readBigUint64(decoder: module:decoding.Decoder)decoding.readAny(decoder: module:decoding.Decoder)new decoding.RleDecoder(uint8Array: Uint8Array, reader: function(module:decoding.Decoder):T)T must not be null.
decoding.RleDecoder#s: T|nullCurrent state
decoding.RleDecoder#read()decoding.RleDecoder#s: Tnew decoding.IntDiffDecoder(uint8Array: Uint8Array, start: number)decoding.IntDiffDecoder#s: numberCurrent state
decoding.IntDiffDecoder#read(): numbernew decoding.RleIntDiffDecoder(uint8Array: Uint8Array, start: number)decoding.RleIntDiffDecoder#s: numberCurrent state
decoding.RleIntDiffDecoder#read(): numberdecoding.RleIntDiffDecoder#s: numbernew decoding.UintOptRleDecoder(uint8Array: Uint8Array)decoding.UintOptRleDecoder#s: numberdecoding.UintOptRleDecoder#read()decoding.UintOptRleDecoder#s: numbernew decoding.IncUintOptRleDecoder(uint8Array: Uint8Array)decoding.IncUintOptRleDecoder#s: numberdecoding.IncUintOptRleDecoder#read()new decoding.IntDiffOptRleDecoder(uint8Array: Uint8Array)decoding.IntDiffOptRleDecoder#s: numberdecoding.IntDiffOptRleDecoder#read(): numbernew decoding.StringDecoder(uint8Array: Uint8Array)decoding.StringDecoder#spos: numberdecoding.StringDecoder#read(): stringdecoding.RleDecoder#arr: Uint8ArrayDecoding target.
decoding.RleDecoder#pos: numberCurrent decoding position.
decoding.IntDiffDecoder#arr: Uint8ArrayDecoding target.
decoding.IntDiffDecoder#pos: numberCurrent decoding position.
decoding.RleIntDiffDecoder#arr: Uint8ArrayDecoding target.
decoding.RleIntDiffDecoder#pos: numberCurrent decoding position.
decoding.UintOptRleDecoder#arr: Uint8ArrayDecoding target.
decoding.UintOptRleDecoder#pos: numberCurrent decoding position.
decoding.IncUintOptRleDecoder#arr: Uint8ArrayDecoding target.
decoding.IncUintOptRleDecoder#pos: numberCurrent decoding position.
decoding.IntDiffOptRleDecoder#arr: Uint8ArrayDecoding target.
decoding.IntDiffOptRleDecoder#pos: numberCurrent decoding position.
import * as diff from 'lib0/diff'+
diff.simpleDiffString(a: string, b: string): module:diff~SimpleDiff<string>Create a diff between two strings. This diff implementation is highly +efficient, but not very sophisticated.
diff.simpleDiffdiff.simpleDiffArray(a: Array<T>, b: Array<T>, compare: function(T, T):boolean): module:diff~SimpleDiff<Array<T>>Create a diff between two arrays. This diff implementation is highly +efficient, but not very sophisticated.
+Note: This is basically the same function as above. Another function was created so that the runtime +can better optimize these function calls.
diff.simpleDiffStringWithCursor(a: string, b: string, cursor: number)Diff text and try to diff at the current cursor position.
import * as dom from 'lib0/dom'+
dom.doc: Documentdom.createElementdom.createDocumentFragmentdom.createTextNodedom.domParserdom.emitCustomEventdom.setAttributesdom.setAttributesMapdom.fragmentdom.appenddom.removedom.addEventListenerdom.removeEventListenerdom.addEventListenersdom.removeEventListenersdom.elementdom.canvasdom.textdom.pairToStyleStringdom.pairsToStyleStringdom.mapToStyleStringdom.querySelectordom.querySelectorAlldom.getElementByIddom.parseFragmentdom.childNodes: anydom.parseElementdom.replaceWithdom.insertBeforedom.appendChilddom.ELEMENT_NODEdom.TEXT_NODEdom.CDATA_SECTION_NODEdom.COMMENT_NODEdom.DOCUMENT_NODEdom.DOCUMENT_TYPE_NODEdom.DOCUMENT_FRAGMENT_NODEdom.checkNodeType(node: any, type: number)dom.isParentOf(parent: Node, child: HTMLElement)import * as encoding from 'lib0/encoding'+ +
Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function.
+Encodes numbers in little-endian order (least to most significant byte order) +and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/) +which is also used in Protocol Buffers.
+// encoding step
+const encoder = encoding.createEncoder()
+encoding.writeVarUint(encoder, 256)
+encoding.writeVarString(encoder, 'Hello world!')
+const buf = encoding.toUint8Array(encoder)
+
+// decoding step
+const decoder = decoding.createDecoder(buf)
+decoding.readVarUint(decoder) // => 256
+decoding.readVarString(decoder) // => 'Hello world!'
+decoding.hasContent(decoder) // => false - all data is read
+
+new encoding.Encoder()A BinaryEncoder handles the encoding to an Uint8Array.
encoding.Encoder#bufs: Array<Uint8Array>encoding.createEncoder(): module:encoding.Encoderencoding.length(encoder: module:encoding.Encoder): numberThe current length of the encoded data.
encoding.toUint8Array(encoder: module:encoding.Encoder): Uint8ArrayTransform to Uint8Array.
encoding.verifyLen(encoder: module:encoding.Encoder, len: number)Verify that it is possible to write len bytes wtihout checking. If
+necessary, a new Buffer with the required length is attached.
encoding.write(encoder: module:encoding.Encoder, num: number)Write one byte to the encoder.
encoding.set(encoder: module:encoding.Encoder, pos: number, num: number)Write one byte at a specific position. +Position must already be written (i.e. encoder.length > pos)
encoding.writeUint8(encoder: module:encoding.Encoder, num: number)Write one byte as an unsigned integer.
encoding.setUint8(encoder: module:encoding.Encoder, pos: number, num: number)Write one byte as an unsigned Integer at a specific location.
encoding.writeUint16(encoder: module:encoding.Encoder, num: number)Write two bytes as an unsigned integer.
encoding.setUint16(encoder: module:encoding.Encoder, pos: number, num: number)Write two bytes as an unsigned integer at a specific location.
encoding.writeUint32(encoder: module:encoding.Encoder, num: number)Write two bytes as an unsigned integer
encoding.writeUint32BigEndian(encoder: module:encoding.Encoder, num: number)Write two bytes as an unsigned integer in big endian order. +(most significant byte first)
encoding.setUint32(encoder: module:encoding.Encoder, pos: number, num: number)Write two bytes as an unsigned integer at a specific location.
encoding.writeVarUint(encoder: module:encoding.Encoder, num: number)Write a variable length unsigned integer. Max encodable integer is 2^53.
encoding.writeVarInt(encoder: module:encoding.Encoder, num: number)Write a variable length integer.
+We use the 7th bit instead for signaling that this is a negative number.
encoding.writeVarStringencoding.writeBinaryEncoder(encoder: module:encoding.Encoder, append: module:encoding.Encoder)Write the content of another Encoder.
encoding.writeUint8Array(encoder: module:encoding.Encoder, uint8Array: Uint8Array)Append fixed-length Uint8Array to the encoder.
encoding.writeVarUint8Array(encoder: module:encoding.Encoder, uint8Array: Uint8Array)Append an Uint8Array to Encoder.
encoding.writeOnDataView(encoder: module:encoding.Encoder, len: number): DataViewCreate an DataView of the next len bytes. Use it to write data after
+calling this function.
// write float32 using DataView
+const dv = writeOnDataView(encoder, 4)
+dv.setFloat32(0, 1.1)
+// read float32 using DataView
+const dv = readFromDataView(encoder, 4)
+dv.getFloat32(0) // => 1.100000023841858 (leaving it to the reader to find out why this is the correct result)
+encoding.writeFloat32(encoder: module:encoding.Encoder, num: number)encoding.writeFloat64(encoder: module:encoding.Encoder, num: number)encoding.writeBigInt64(encoder: module:encoding.Encoder, num: bigint)encoding.writeBigUint64(encoder: module:encoding.Encoder, num: bigint)encoding.writeAny(encoder: module:encoding.Encoder, data: undefined|null|number|bigint|boolean|string|Object<string,any>|Array<any>|Uint8Array)Encode data with efficient binary format.
+Differences to JSON: +• Transforms data to a binary format (not to a string) +• Encodes undefined, NaN, and ArrayBuffer (these can't be represented in JSON) +• Numbers are efficiently encoded either as a variable length integer, as a +32 bit float, as a 64 bit float, or as a 64 bit bigint.
+Encoding table:
+| Data Type | +Prefix | +Encoding Method | +Comment | +
|---|---|---|---|
| undefined | +127 | ++ | Functions, symbol, and everything that cannot be identified is encoded as undefined | +
| null | +126 | ++ | + |
| integer | +125 | +writeVarInt | +Only encodes 32 bit signed integers | +
| float32 | +124 | +writeFloat32 | ++ |
| float64 | +123 | +writeFloat64 | ++ |
| bigint | +122 | +writeBigInt64 | ++ |
| boolean (false) | +121 | ++ | True and false are different data types so we save the following byte | +
| boolean (true) | +120 | ++ | - 0b01111000 so the last bit determines whether true or false | +
| string | +119 | +writeVarString | ++ |
| object<string,any> | +118 | +custom | +Writes {length} then {length} key-value pairs | +
| array |
+117 | +custom | +Writes {length} then {length} json values | +
| Uint8Array | +116 | +writeVarUint8Array | +We use Uint8Array for any kind of binary data | +
Reasons for the decreasing prefix: +We need the first bit for extendability (later we may want to encode the +prefix with writeVarUint). The remaining 7 bits are divided as follows: +[0-30] the beginning of the data range is used for custom purposes +(defined by the function that uses this library) +[31-127] the end of the data range is used for data encoding by +lib0/encoding.js
new encoding.RleEncoder(writer: function(module:encoding.Encoder, T):void)Now come a few stateful encoder that have their own classes.
encoding.RleEncoder#s: T|nullCurrent state
encoding.RleEncoder#write(v: T)new encoding.IntDiffEncoder(start: number)Basic diff decoder using variable length encoding.
+Encodes the values [3, 1100, 1101, 1050, 0] to [3, 1097, 1, -51, -1050] using writeVarInt.
encoding.IntDiffEncoder#s: numberCurrent state
encoding.IntDiffEncoder#write(v: number)new encoding.RleIntDiffEncoder(start: number)A combination of IntDiffEncoder and RleEncoder.
+Basically first writes the IntDiffEncoder and then counts duplicate diffs using RleEncoding.
+Encodes the values [1,1,1,2,3,4,5,6] as [1,1,0,2,1,5] (RLE([1,0,0,1,1,1,1,1]) ⇒ RleIntDiff[1,1,0,2,1,5])
encoding.RleIntDiffEncoder#s: numberCurrent state
encoding.RleIntDiffEncoder#write(v: number)new encoding.UintOptRleEncoder()Optimized Rle encoder that does not suffer from the mentioned problem of the basic Rle encoder.
+Internally uses VarInt encoder to write unsigned integers. If the input occurs multiple times, we write +write it as a negative number. The UintOptRleDecoder then understands that it needs to read a count.
+Encodes [1,2,3,3,3] as [1,2,-3,3] (once 1, once 2, three times 3)
encoding.UintOptRleEncoder#s: numberencoding.UintOptRleEncoder#write(v: number)encoding.UintOptRleEncoder#toUint8Array()new encoding.IncUintOptRleEncoder()Increasing Uint Optimized RLE Encoder
+The RLE encoder counts the number of same occurences of the same value. +The IncUintOptRle encoder counts if the value increases. +I.e. 7, 8, 9, 10 will be encoded as [-7, 4]. 1, 3, 5 will be encoded +as [1, 3, 5].
encoding.IncUintOptRleEncoder#s: numberencoding.IncUintOptRleEncoder#write(v: number)encoding.IncUintOptRleEncoder#toUint8Array()new encoding.IntDiffOptRleEncoder()A combination of the IntDiffEncoder and the UintOptRleEncoder.
+The count approach is similar to the UintDiffOptRleEncoder, but instead of using the negative bitflag, it encodes +in the LSB whether a count is to be read. Therefore this Encoder only supports 31 bit integers!
+Encodes [1, 2, 3, 2] as [3, 1, 6, -1] (more specifically [(1 << 1) | 1, (3 << 0) | 0, -1])
+Internally uses variable length encoding. Contrary to normal UintVar encoding, the first byte contains:
+Therefore, only five bits remain to encode diff ranges.
+Use this Encoder only when appropriate. In most cases, this is probably a bad idea.
encoding.IntDiffOptRleEncoder#s: numberencoding.IntDiffOptRleEncoder#write(v: number)encoding.IntDiffOptRleEncoder#toUint8Array()new encoding.StringEncoder()Optimized String Encoder.
+Encoding many small strings in a simple Encoder is not very efficient. The function call to decode a string takes some time and creates references that must be eventually deleted. +In practice, when decoding several million small strings, the GC will kick in more and more often to collect orphaned string objects (or maybe there is another reason?).
+This string encoder solves the above problem. All strings are concatenated and written as a single string using a single encoding call.
+The lengths are encoded using a UintOptRleEncoder.
encoding.StringEncoder#sarr: Array<string>encoding.StringEncoder#write(string: string)encoding.StringEncoder#toUint8Array()encoding.RleEncoder#bufs: Array<Uint8Array>encoding.IntDiffEncoder#bufs: Array<Uint8Array>encoding.RleIntDiffEncoder#bufs: Array<Uint8Array>import * as env from 'lib0/environment'+
env.isNodeenv.isBrowserenv.isMacenv.hasParamenv.getParamenv.getVariableenv.getConf(name: string): string|nullenv.hasConfenv.productionenv.supportsColorimport * as error from 'lib0/error'+
error.create(s: string): Errorerror.methodUnimplemented(): nevererror.unexpectedCase(): neverimport * as eventloop from 'lib0/eventloop'+
eventloop.enqueue(f: function():void)eventloop#destroy()eventloop.timeout(timeout: number, callback: function): module:eventloop~TimeoutObjecteventloop.interval(timeout: number, callback: function): module:eventloop~TimeoutObjecteventloop.Animationeventloop.animationFrame(cb: function(number):void): module:eventloop~TimeoutObjecteventloop.idleCallback(cb: function): module:eventloop~TimeoutObjectNote: this is experimental and is probably only useful in browsers.
eventloop.createDebouncer(timeout: number): function(function():void):voidimport * as function from 'lib0/function'+
function.callAll(fs: Array<function>, args: Array<any>)Calls all functions in fs with args. Only throws after all functions were called.
function.nopfunction.apply(f: function():T): Tfunction.id(a: A): Afunction.equalityStrict(a: T, b: T): booleanfunction.equalityFlat(a: Array<T>|object, b: Array<T>|object): booleanfunction.equalityDeep(a: any, b: any): booleanfunction.isOneOf(value: V, options: Array<OPTS>)import * as lib0 from 'lib0/index'+ +
Not recommended if the module bundler doesn't support dead code elimination.
+import * as indexeddb from 'lib0/indexeddb'+
indexeddb.rtop(request: IDBRequest): Promise<any>IDB Request to Promise transformer
indexeddb.openDB(name: string, initDB: function(IDBDatabase):any): Promise<IDBDatabase>indexeddb.deleteDB(name: string)indexeddb.createStores(db: IDBDatabase, definitions: Array<Array<string>|Array<string|IDBObjectStoreParameters|undefined>>)indexeddb.transact(db: IDBDatabase, stores: Array<string>, access: "readwrite"|"readonly"): Array<IDBObjectStore>indexeddb.count(store: IDBObjectStore, range: IDBKeyRange): Promise<number>indexeddb.get(store: IDBObjectStore, key: String | number | ArrayBuffer | Date | Array<any> ): Promise<String | number | ArrayBuffer | Date | Array<any>>indexeddb.del(store: IDBObjectStore, key: String | number | ArrayBuffer | Date | IDBKeyRange | Array<any> )indexeddb.put(store: IDBObjectStore, item: String | number | ArrayBuffer | Date | boolean, key: String | number | ArrayBuffer | Date | Array<any>)indexeddb.add(store: IDBObjectStore, item: String|number|ArrayBuffer|Date|boolean, key: String|number|ArrayBuffer|Date|Array.<any>): Promise<any>indexeddb.addAutoKey(store: IDBObjectStore, item: String|number|ArrayBuffer|Date): Promise<number>indexeddb.getAll(store: IDBObjectStore, range: IDBKeyRange, limit: number): Promise<Array<any>>indexeddb.getAllKeys(store: IDBObjectStore, range: IDBKeyRange, limit: number): Promise<Array<any>>indexeddb.queryFirst(store: IDBObjectStore, query: IDBKeyRange|null, direction: 'next'|'prev'|'nextunique'|'prevunique'): Promise<any>indexeddb.getLastKey(store: IDBObjectStore, range: IDBKeyRange?): Promise<any>indexeddb.getFirstKey(store: IDBObjectStore, range: IDBKeyRange?): Promise<any>indexeddb.getAllKeysValues(store: IDBObjectStore, range: IDBKeyRange, limit: number): Promise<Array<KeyValuePair>>indexeddb.iterate(store: IDBObjectStore, keyrange: IDBKeyRange|null, f: function(any,any):void|boolean|Promise<void|boolean>, direction: 'next'|'prev'|'nextunique'|'prevunique')Iterate on keys and values
indexeddb.iterateKeys(store: IDBObjectStore, keyrange: IDBKeyRange|null, f: function(any):void|boolean|Promise<void|boolean>, direction: 'next'|'prev'|'nextunique'|'prevunique')Iterate on the keys (no values)
indexeddb.getStore(t: IDBTransaction, store: String)IDBObjectStoreOpen store from transaction
indexeddb.createIDBKeyRangeBound(lower: any, upper: any, lowerOpen: boolean, upperOpen: boolean)indexeddb.createIDBKeyRangeUpperBound(upper: any, upperOpen: boolean)indexeddb.createIDBKeyRangeLowerBound(lower: any, lowerOpen: boolean)import * as isomorphic from 'lib0/isomorphic'+
import * as iterator from 'lib0/iterator'+
iterator.mapIterator(iterator: Iterator<T>, f: function(T):R): IterableIterator<R>iterator.createIterator(next: function():IteratorResult<T>): IterableIterator<T>iterator.iteratorFilter(iterator: Iterator<T>, filter: function(T):boolean)iterator.iteratorMap(iterator: Iterator<T>, fmap: function(T):M)import * as json from 'lib0/json'+
json.stringify(object: any): stringTransform JavaScript object to JSON.
json.parse(json: string): anyParse JSON object.
import * as list from 'lib0/list'+
new e#ListNode()e#next: this|nulle#prev: this|nullnew st()art: N | nulld: N | null(): module:list.List<N>()(queue: module:list.List<N>)()(queue: module:list.List<N>, node: N)Remove a single node from the queue. Only works with Queues that operate on Doubly-linked lists of nodes.
()odeode()etween(queue: module:list.List<N>, left: N| null, right: N| null, node: N)etween()(queue: module:list.List<N>, node: N, newNode: N)Remove a single node from the queue. Only works with Queues that operate on Doubly-linked lists of nodes.
()(queue: module:list.List<N>, n: N)()nt(queue: module:list.List<N>, n: N)nt()t(list: module:list.List<N>): N| nullt()(list: module:list.List<N>): N| null()(list: module:list.List<N>, f: function(N):M): Array<M>()(list: module:list.List<N>)()(list: module:list.List<N>, f: function(N):M)()import * as logging from 'lib0/logging'+
logging.BOLDlogging.UNBOLDlogging.BLUElogging.GREYlogging.GREENlogging.REDlogging.PURPLElogging.ORANGElogging.UNCOLORlogging.print(args: Array<string|Symbol|Object|number>)logging.warn(args: Array<string|Symbol|Object|number>)logging.printError(err: Error)logging.printImg(url: string, height: number)logging.printImgBase64(base64: string, height: number)logging.group(args: Array<string|Symbol|Object|number>)logging.groupCollapsed(args: Array<string|Symbol|Object|number>)logging.groupEndlogging.printDom(createNode: function():Node)logging.printCanvas(canvas: HTMLCanvasElement, height: number)logging.vconsolesnew logging.VConsole(dom: Element)logging.VConsole#ccontainer: Elementlogging.VConsole#group(args: Array<string|Symbol|Object|number>, collapsed: boolean)logging.VConsole#groupCollapsed(args: Array<string|Symbol|Object|number>)logging.VConsole#groupEnd()logging.VConsole#print(args: Array<string|Symbol|Object|number>)logging.VConsole#printError(err: Error)logging.VConsole#printImg(url: string, height: number)logging.VConsole#printDom(node: Node)logging.VConsole#destroy()logging.createVConsole(dom: Element)logging.createModuleLogger(moduleName: string): function(...any):voidimport * as map from 'lib0/map'+
map.create(): Map<any, any>Creates a new Map instance.
map.copy(m: Map<X,Y>): Map<X,Y>Copy a Map object into a fresh Map object.
map.setIfUndefined(map: Map<K, T>, key: K, createT: function():T): TGet map property. Create T if property is undefined and set T on map.
+const listeners = map.setIfUndefined(events, 'eventName', set.create)
+listeners.add(listener)
+map.map(m: Map<K,V>, f: function(V,K):R): Array<R>Creates an Array and populates it with the content of all key-value pairs using the f(value, key) function.
map.any(m: Map<K,V>, f: function(V,K):boolean): booleanTests whether any key-value pairs pass the test implemented by f(value, key).
map.all(m: Map<K,V>, f: function(V,K):boolean): booleanTests whether all key-value pairs pass the test implemented by f(value, key).
import * as math from 'lib0/math'+
math.floormath.ceilmath.absmath.imulmath.roundmath.log10math.log2math.logmath.sqrtmath.add(a: number, b: number): numbermath.min(a: number, b: number): numbermath.max(a: number, b: number): numbermath.isNaNmath.powmath.exp10(exp: number): numberBase 10 exponential function. Returns the value of 10 raised to the power of pow.
math.signmath.isNegativeZero(n: number): booleanimport * as metric from 'lib0/metric'+
metric.yottametric.zettametric.exametric.petametric.terametric.gigametric.megametric.kilometric.hectometric.decametric.decimetric.centimetric.millimetric.micrometric.nanometric.picometric.femtometric.attometric.zeptometric.yoctometric.prefix(n: number, baseMultiplier: number): {n:number,prefix:string}Calculate the metric prefix for a number. Assumes E.g. prefix(1000) = { n: 1, prefix: 'k' }
import * as mutex from 'lib0/mutex'+
mutex.createMutex(): mutexCreates a mutual exclude function with the following property:
+const mutex = createMutex()
+mutex(() => {
+ // This function is immediately executed
+ mutex(() => {
+ // This function is not executed, as the mutex is already active.
+ })
+})
+import * as number from 'lib0/number'+
number.MAX_SAFE_INTEGERnumber.MIN_SAFE_INTEGERnumber.LOWEST_INT32number.HIGHEST_INT32: numbernumber.isIntegernumber.isNaNnumber.parseIntimport * as object from 'lib0/object'+
object.create(): Object<string,any>object.assignObject.assign
object.keys(obj: Object<string,any>)object.forEach(obj: Object<string,any>, f: function(any,string):any)object.map(obj: Object<string,any>, f: function(any,string):R): Array<R>object.length(obj: Object<string,any>): numberobject.some(obj: Object<string,any>, f: function(any,string):boolean): booleanobject.isEmpty(obj: Object|undefined)object.every(obj: Object<string,any>, f: function(any,string):boolean): booleanobject.hasProperty(obj: any, key: string|symbol): booleanCalls Object.prototype.hasOwnProperty.
object.equalFlat(a: Object<string,any>, b: Object<string,any>): booleanimport * as observable from 'lib0/observable'+
new observable.Observable()Handles named events.
observable.Observable#on(name: N, f: function)observable.Observable#once(name: N, f: function)observable.Observable#off(name: N, f: function)observable.Observable#emit(name: N, args: Array<any>)Emit a named event. All registered event listeners that listen to the +specified name will receive the event.
observable.Observable#destroy()websocket.WebsocketClient#on(name: N, f: function)websocket.WebsocketClient#once(name: N, f: function)websocket.WebsocketClient#off(name: N, f: function)websocket.WebsocketClient#emit(name: N, args: Array<any>)Emit a named event. All registered event listeners that listen to the +specified name will receive the event.
import * as pair from 'lib0/pair'+
new pair.Pair(left: L, right: R)pair.create(left: L, right: R): module:pair.Pair<L,R>pair.createReversed(right: R, left: L): module:pair.Pair<L,R>pair.forEach(arr: Array<module:pair.Pair<L,R>>, f: function(L, R):any)pair.map(arr: Array<module:pair.Pair<L,R>>, f: function(L, R):X): Array<X>import * as prng from 'lib0/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.
+prng.DefaultPRNGprng.create(seed: number): module:prng~PRNGCreate 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/.
prng.bool(gen: module:prng~PRNG): BooleanGenerates a single random bool.
prng.int53(gen: module:prng~PRNG, min: Number, max: Number): NumberGenerates a random integer with 53 bit resolution.
prng.uint53(gen: module:prng~PRNG, min: Number, max: Number): NumberGenerates a random integer with 53 bit resolution.
prng.int32(gen: module:prng~PRNG, min: Number, max: Number): NumberGenerates a random integer with 32 bit resolution.
prng.uint32(gen: module:prng~PRNG, min: Number, max: Number): NumberGenerates a random integer with 53 bit resolution.
prng.int31(gen: module:prng~PRNG, min: Number, max: Number): Numberprng.real53(gen: module:prng~PRNG): NumberGenerates a random real on [0, 1) with 53 bit resolution.
prng.char(gen: module:prng~PRNG): stringGenerates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
prng.letter(gen: module:prng~PRNG): stringprng.word(gen: module:prng~PRNG, minLen: number, maxLen: number): stringprng.utf16Rune(gen: module:prng~PRNG): stringTODO: this function produces invalid runes. Does not cover all of utf16!!
prng.utf16String(gen: module:prng~PRNG, maxlen: number)prng.oneOf(gen: module:prng~PRNG, array: Array<T>): TReturns one element of a given array.
prng.uint8Array(gen: module:prng~PRNG, len: number): Uint8Arrayprng.uint16Array(gen: module:prng~PRNG, len: number): Uint16Arrayprng.uint32Array(gen: module:prng~PRNG, len: number): Uint32Arrayimport * as promise from 'lib0/promise'+
promise.create(f: function(PromiseResolve<T>,function(Error):void):any): Promise<T>promise.createEmpty(f: function(function():void,function(Error):void):void): Promise<void>promise.all(arrp: Array<Promise<T>>): Promise<Array<T>>Promise.all wait for all promises in the array to resolve and return the result
promise.reject(reason: Error): Promise<never>promise.resolve(res: T|void): Promise<T|void>promise.resolveWith(res: T): Promise<T>promise.until(timeout: number, check: function():boolean, intervalResolution: number): Promise<void>promise.wait(timeout: number): Promise<undefined>promise.isPromise(p: any): booleanChecks if an object is a promise using ducktyping.
+Promises are often polyfilled, so it makes sense to add some additional guarantees if the user of this +library has some insane environment where global Promise objects are overwritten.
import * as queue from 'lib0/queue'+
new de#QueueNode()de#next: module:queue.QueueNode|nullnew ueue()tart: module:queue.QueueNode | nullnd: module:queue.QueueNode | null(): module:queue.Queue()(queue: module:queue.Queue)()(queue: module:queue.Queue, n: module:queue.QueueNode)()(queue: module:queue.Queue): module:queue.QueueNode | null()import * as random from 'lib0/random'+ +
Attention: falls back to Math.random if the browser does not support crypto.
+random.randrandom.uint32random.uint53random.oneOf(arr: Array<T>): Trandom.uuidv4import * as set from 'lib0/set'+
set.createset.toArray(set: Set<T>): Array<T>set.first(set: Set<T>): Tset.from(entries: Iterable<T>): Set<T>import * as sort from 'lib0/sort'+ +
Note: These sort implementations were created to compare different sorting algorithms in JavaScript. +Don't use them if you don't know what you are doing. Native Array.sort is almost always a better choice.
+sort.insertionSort(arr: Array<T>, compare: function(T,T):number): voidsort.quicksort(arr: Array<T>, compare: function(T,T):number): voidThis algorithm beats Array.prototype.sort in Chrome only with arrays with 10 million entries. +In most cases [].sort will do just fine. Make sure to performance test your use-case before you +integrate this algorithm.
+Note that Chrome's sort is now a stable algorithm (Timsort). Quicksort is not stable.
import * as statistics from 'lib0/statistics'+
statistics.median(arr: Array<number>): numberstatistics.average(arr: Array<number>): numberimport * as storage from 'lib0/storage'+ +
Uses LocalStorage in the browser and falls back to in-memory storage.
+storage.varStorageThis is basically localStorage in browser, or a polyfill in nodejs
storage.onChange(eventHandler: function({ key: string, newValue: string, oldValue: string }): void)A polyfill for addEventListener('storage', event => {..}) that does nothing if the polyfill is being used.
import * as string from 'lib0/string'+
string.fromCharCodestring.fromCodePointstring.trimLeft(s: string): stringstring.fromCamelCase(s: string, separator: string): stringstring.utf8ByteLength(str: string): numberCompute the utf8ByteLength
string.utf8TextEncoderstring.encodeUtf8string.decodeUtf8string.splice(str: string, index: number, remove: number, insert: string)import * as symbol from 'lib0/symbol'+
symbol.create(): SymbolReturn fresh symbol.
symbol.isSymbol(s: any): booleanimport * as testing from 'lib0/testing'+ +
// test.js template for creating a test executable
+import { runTests } from 'lib0/testing'
+import * as log from 'lib0/logging'
+import * as mod1 from './mod1.test.js'
+import * as mod2 from './mod2.test.js'
+import { isBrowser, isNode } from 'lib0/environment.js'
+
+if (isBrowser) {
+ // optional: if this is ran in the browser, attach a virtual console to the dom
+ log.createVConsole(document.body)
+}
+
+runTests({
+ mod1,
+ mod2,
+}).then(success => {
+ if (isNode) {
+ process.exit(success ? 0 : 1)
+ }
+})
+
+// mod1.test.js
+/**
+ * runTests automatically tests all exported functions that start with "test".
+ * The name of the function should be in camelCase and is used for the logging output.
+ *
+ * @param {t.TestCase} tc
+ *\/
+export const testMyFirstTest = tc => {
+ t.compare({ a: 4 }, { a: 4 }, 'objects are equal')
+}
+
+Now you can simply run node test.js to run your test or run test.js in the browser.
testing.extensivetesting.envSeednew testing.TestCase(moduleName: string, testName: string)testing.TestCase#moduleName: stringtesting.TestCase#testName: stringtesting.TestCase#resetSeed()testing.TestCase#prng: prng.PRNGA PRNG for this test case. Use only this PRNG for randomness to make the test case reproducible.
testing.repetitionTimetesting.run(moduleName: string, name: string, f: function(module:testing.TestCase):void|Promise<any>, i: number, numberOfTests: number)testing.describe(description: string, info: string)Describe what you are currently testing. The message will be logged.
+export const testMyFirstTest = tc => {
+ t.describe('crunching numbers', 'already crunched 4 numbers!') // the optional second argument can describe the state.
+}
+testing.info(info: string)Describe the state of the current computation.
+export const testMyFirstTest = tc => {
+ t.info(already crunched 4 numbers!') // the optional second argument can describe the state.
+}
+testing.printDomtesting.printCanvastesting.group(description: string, f: function(void):void)Group outputs in a collapsible category.
+export const testMyFirstTest = tc => {
+ t.group('subtest 1', () => {
+ t.describe('this message is part of a collapsible section')
+ })
+ await t.groupAsync('subtest async 2', async () => {
+ await someaction()
+ t.describe('this message is part of a collapsible section')
+ })
+}
+testing.groupAsync(description: string, f: function(void):Promise<any>)Group outputs in a collapsible category.
+export const testMyFirstTest = async tc => {
+ t.group('subtest 1', () => {
+ t.describe('this message is part of a collapsible section')
+ })
+ await t.groupAsync('subtest async 2', async () => {
+ await someaction()
+ t.describe('this message is part of a collapsible section')
+ })
+}
+testing.measureTime(message: string, f: function():void): numberMeasure the time that it takes to calculate something.
+export const testMyFirstTest = async tc => {
+ t.measureTime('measurement', () => {
+ heavyCalculation()
+ })
+ await t.groupAsync('async measurement', async () => {
+ await heavyAsyncCalculation()
+ })
+}
+testing.measureTimeAsync(message: string, f: function():Promise<any>): Promise<number>Measure the time that it takes to calculate something.
+export const testMyFirstTest = async tc => {
+ t.measureTimeAsync('measurement', async () => {
+ await heavyCalculation()
+ })
+ await t.groupAsync('async measurement', async () => {
+ await heavyAsyncCalculation()
+ })
+}
+testing.compareArrays(as: Array<T>, bs: Array<T>, m: string): booleantesting.compareStrings(a: string, b: string, m: string)testing.compareObjects(a: Object<K,V>, b: Object<K,V>, m: string)testing.compare(a: T, b: T, message: string?, customCompare: function(any,T,T,string,any):boolean)testing.assert(condition: boolean, message: string?)testing.promiseRejected(f: function():Promise<any>)testing.fails(f: function():void)testing.runTests(tests: Object<string, Object<string, function(module:testing.TestCase):void|Promise<any>>>)testing.fail(reason: string)testing.skip(cond: boolean)import * as time from 'lib0/time'+
time.getDate(): DateReturn current time.
time.getUnixTime(): numberReturn current unix time.
time.humanizeDuration(d: number): stringTransform time (in ms) to a human readable format. E.g. 1100 => 1.1s. 60s => 1min. .001 => 10μs.
import * as tree from 'lib0/tree'+
new tree.Tree()This is a Red Black Tree implementation
tree.Tree#findNext(id: K)tree.Tree#findPrev(id: K)tree.Tree#findNodeWithLowerBound(from: K)tree.Tree#findNodeWithUpperBound(to: K)tree.Tree#findSmallestNode(): Vtree.Tree#findWithLowerBound(from: K): Vtree.Tree#findWithUpperBound(to: K): Vtree.Tree#iterate(from: K, from: K, f: K)tree.Tree#find(id: K): V|nulltree.Tree#findNode(id: K): module:tree~N<V>|nulltree.Tree#delete(id: K)tree.Tree#put()import * as url from 'lib0/url'+
url.decodeQueryParams(url: string): Object<string,string>Parse query parameters from an url.
url.encodeQueryParams(params: Object<string,string>): stringimport * as webcrypto.browser from 'lib0/webcrypto.browser'+
()omValuesomValues()import * as webcrypto.node from 'lib0/webcrypto.node'+
()to.subtle: anyomValuesomValues()import * as websocket from 'lib0/websocket'+ +
Implements exponential backoff reconnects, ping/pong, and a nice event system using [lib0/observable].
+new websocket.WebsocketClient(url: string, opts: object, opts.binaryType: 'arraybuffer' | 'blob' | null)websocket.WebsocketClient#ws: WebSocket?websocket.WebsocketClient#shouldConnect: booleanWhether to connect to other peers or not
websocket.WebsocketClient#send(message: any)websocket.WebsocketClient#destroy()websocket.WebsocketClient#disconnect()websocket.WebsocketClient#connect()import * as ${mod.name} from 'lib0/${fileName.slice(0, -3)}'`)
+ if (descRest.length > 0) {
+ strBuilder.push(descRest)
+ }
+ strBuilder.push('${item.longname.slice(7)}${typeEval ? (': ' + toSafeHtml(typeEval[1])) : ''}${item.kind === 'class' ? 'new ' : ''}${item.longname.slice(7)}(${toSafeHtml(paramVal)})${toSafeHtml(returnVal)}${item.longname.slice(7)}: ${toSafeHtml(/** @type {RegExpExecArray} */ (jsdocTypeRegex.exec(item.comment))[1])}