@live-change/serialization 0.9.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/binary.js ADDED
@@ -0,0 +1,129 @@
1
+ const textEncoder = new TextEncoder()
2
+ const textDecoder = new TextDecoder()
3
+
4
+ /** Writer that will write data in binary format. */
5
+ class BinaryWriter {
6
+ #outputBuffer = new ArrayBuffer(1024)
7
+ #outputPosition = 0
8
+ #outputView = new DataView(this.#outputBuffer)
9
+ #structureBuffer
10
+ #structurePosition = 0
11
+ #structureView
12
+ constructor(structure) {
13
+ if(structure) {
14
+ this.#structureBuffer = new ArrayBuffer(1024)
15
+ this.#structureView = new DataView(this.#structureBuffer)
16
+ }
17
+ }
18
+
19
+ allocateMore() {
20
+ const newBuffer = new ArrayBuffer(this.#outputBuffer.byteLength * 2)
21
+ const newView = new DataView(newBuffer)
22
+ new Uint8Array(newBuffer).set(new Uint8Array(this.#outputBuffer))
23
+ this.#outputBuffer = newBuffer
24
+ this.#outputView = newView
25
+ }
26
+
27
+ writeNumber(num) {
28
+ if(this.#outputPosition + 8 > this.#outputBuffer.byteLength) this.allocateMore()
29
+ this.#outputView.setFloat64(this.#outputPosition, num)
30
+ this.#outputPosition += 8
31
+ return this
32
+ }
33
+
34
+ writeType(type) {
35
+ this.writeInteger(type)
36
+ return this
37
+ }
38
+
39
+ writeSize(size) {
40
+ this.writeInteger(size)
41
+ return this
42
+ }
43
+
44
+ writeString(str) {
45
+ const encoded = textEncoder.encode(str, new Uint8Array(this.#outputBuffer, this.#outputPosition))
46
+ this.writeInteger(encoded.length)
47
+ // copy string to output buffer
48
+ new Uint8Array(this.#outputBuffer, this.#outputPosition).set(encoded)
49
+ this.#outputPosition += encoded.length
50
+ return this
51
+ }
52
+
53
+ writeKey(key) {
54
+ const encoded = textEncoder.encode(str, new Uint8Array(this.#outputBuffer, this.#outputPosition))
55
+ this.writeInteger(encoded.length)
56
+ if(this.#structureBuffer) {
57
+ new Uint8Array(this.#structureBuffer, this.#structurePosition).set(encoded)
58
+ this.#structurePosition += encoded.length
59
+ return this
60
+ }
61
+ new Uint8Array(this.#outputBuffer, this.#outputPosition).set(encoded)
62
+ this.#outputPosition += encoded.length
63
+ return this
64
+ }
65
+
66
+ writeBoolean(bool) {
67
+ this.#outputView.setUint8(this.#outputPosition++, bool ? 1 : 0)
68
+ return this
69
+ }
70
+
71
+ writeNull() {
72
+ }
73
+
74
+ }
75
+
76
+ export class BinaryReader {
77
+ #inputBuffer
78
+ #inputPosition = 0
79
+ #inputView = new DataView(this.#inputBuffer)
80
+ constructor(inputBuffer, structureBuffer) {
81
+ this.#inputBuffer = inputBuffer
82
+ this.#inputView = new DataView(inputBuffer)
83
+ }
84
+
85
+ readNumber() {
86
+ const result = this.#inputView.getFloat64(this.#inputPosition)
87
+ this.#inputPosition += 8
88
+ return result
89
+ }
90
+
91
+ readType() {
92
+ return this.readInteger()
93
+ }
94
+
95
+ readSize() {
96
+ return this.readInteger()
97
+ }
98
+
99
+ readString() {
100
+ const size = this.readInteger()
101
+ const result = textDecoder.decode(new Uint8Array(this.#inputBuffer, this.#inputPosition, size))
102
+ this.#inputPosition += size
103
+ return result
104
+ }
105
+
106
+ readKey() {
107
+ return this.readString()
108
+ }
109
+
110
+ readBoolean() {
111
+ return this.#inputView.getUint8(this.#inputPosition++) === 1
112
+ }
113
+
114
+ readNull() {
115
+ }
116
+ }
117
+
118
+ import { write, read } from './serialization.js'
119
+
120
+ export function serializeToBinary(key) {
121
+ const writer = new BinaryWriter()
122
+ write(key, writer)
123
+ return writer.getOutput()
124
+ }
125
+
126
+ export function deserializeFromBinary(serialized, structure) {
127
+ const reader = new BinaryReader(serialized, structure)
128
+ return read(reader)
129
+ }
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './serialization.js'
2
+
3
+ export * from './stringKey.js'
4
+
5
+ export * from './string.js'
6
+
7
+ export * from './binary.js'
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@live-change/serialization",
3
+ "version": "0.9.139",
4
+ "scripts": {},
5
+ "type": "module",
6
+ "author": "Michał Łaszczewski <michal@laszczewski.pl>",
7
+ "license": "ISC",
8
+ "description": "",
9
+ "gitHead": "9202c36abc25e3baf5fe39806d89c3fab203f428"
10
+ }
@@ -0,0 +1,81 @@
1
+
2
+ export const typeMap = Object.freeze({
3
+ 'string': 's',
4
+ 'number': 'n',
5
+ 'boolean': 'b',
6
+ 'object': 'o',
7
+ 'null': 'z',
8
+ 'array': 'a'
9
+ })
10
+
11
+ export const types = Object.freeze(Object.fromEntries(
12
+ Object.entries(typeMap).map(([type, key]) => [key, type])
13
+ ))
14
+
15
+ /** Serialize any data using writer
16
+ *
17
+ * @param {any} data
18
+ * @param {Writer} writer
19
+ */
20
+ export function write(data, writer) {
21
+ switch(typeof data) {
22
+ case 'string': return writer.writeType(typeMap.string).writeString(data)
23
+ case 'number': {
24
+ return writer.writeType(typeMap.number).writeNumber(data)
25
+ }
26
+ case 'boolean': return writer.writeType(typeMap.boolean).writeBoolean(data)
27
+ case 'object':
28
+ if(data === null) return writer.writeType(typeMap.null)
29
+ if(Array.isArray(data)) {
30
+ writer.writeType(typeMap.array).writeSize(data.length)
31
+ for(let value of data) write(value, writer)
32
+ return writer
33
+ }
34
+ const entries = Object.entries(data)
35
+ writer.writeType(typeMap.object).writeSize(entries.length)
36
+ for(let [key, value] of entries) {
37
+ writer.writeKey(key)
38
+ write(value, writer)
39
+ }
40
+ return writer
41
+ default:
42
+ throw new Error(`Unsupported type: ${typeof data}`)
43
+ }
44
+ }
45
+
46
+ /** Deserialize data using reader
47
+ *
48
+ * @param {Reader} reader
49
+ * @returns {any}
50
+ */
51
+ export function read(reader) {
52
+ const type = reader.readType()
53
+ switch(type) {
54
+ case typeMap.string:
55
+ return reader.readString()
56
+ case typeMap.number:
57
+ return reader.readNumber()
58
+ case typeMap.boolean:
59
+ return reader.readBoolean()
60
+ case typeMap.object: {
61
+ const size = reader.readSize()
62
+ const entries = []
63
+ for(let i = 0; i < size; i++) {
64
+ const key = reader.readKey()
65
+ const value = read(reader)
66
+ entries.push([key, value])
67
+ }
68
+ return Object.fromEntries(entries)
69
+ }
70
+ case typeMap.null:
71
+ return null
72
+ case typeMap.array: {
73
+ const size = reader.readSize()
74
+ const array = []
75
+ for(let i = 0; i < size; i++) array.push(read(reader))
76
+ return array
77
+ }
78
+ default:
79
+ throw new Error(`Unsupported type: ${type}`)
80
+ }
81
+ }
package/string.js ADDED
@@ -0,0 +1,104 @@
1
+ /** String writer that writer data to human readable string */
2
+ export class StringWriter {
3
+ #outputArray = []
4
+ constructor() {
5
+ }
6
+
7
+ writeType(type) {
8
+ this.#outputArray.push(type)
9
+ return this
10
+ }
11
+
12
+ writeSize(size) {
13
+ this.#outputArray.push(size.toFixed())
14
+ return this
15
+ }
16
+
17
+ writeKey(key) {
18
+ this.#outputArray.push(key.length.toFixed())
19
+ this.#outputArray.push(key)
20
+ return this
21
+ }
22
+
23
+ writeString(str) {
24
+ this.#outputArray.push(str.length.toFixed())
25
+ this.#outputArray.push(str)
26
+ return this
27
+ }
28
+
29
+ writeNumber(num) {
30
+ this.#outputArray.push(num.toString())
31
+ return this
32
+ }
33
+
34
+ writeBoolean(bool) {
35
+ this.#outputArray.push(bool ? 'true' : 'false')
36
+ return this
37
+ }
38
+
39
+ getOutput() {
40
+ return this.#outputArray.join(';')
41
+ }
42
+
43
+ }
44
+
45
+ export class StringReader {
46
+ #data
47
+ #dataPointer = 0
48
+ constructor(serialized) {
49
+ this.#data = serialized
50
+ }
51
+
52
+ readToken() {
53
+ const nextSeparator = this.#data.indexOf(';', this.#dataPointer)
54
+ const token = this.#data.slice(this.#dataPointer, nextSeparator === -1 ? this.#data.length : nextSeparator)
55
+ this.#dataPointer = nextSeparator + 1
56
+ return token
57
+ }
58
+
59
+ readType() {
60
+ return this.readToken()
61
+ }
62
+
63
+ readKey() {
64
+ const keyLength = parseInt(this.readToken())
65
+ const key = this.#data.slice(this.#dataPointer, this.#dataPointer + keyLength)
66
+ this.#dataPointer = this.#dataPointer + keyLength + 1
67
+ return key
68
+ }
69
+
70
+ readSize() {
71
+ const size = parseInt(this.readToken())
72
+ return size
73
+ }
74
+
75
+ readString() {
76
+ const size = parseInt(this.readToken())
77
+ const str = this.#data.slice(this.#dataPointer, this.#dataPointer + size)
78
+ this.#dataPointer = this.#dataPointer + size + 1
79
+ return str
80
+ }
81
+
82
+ readNumber() {
83
+ const num = parseFloat(this.readToken())
84
+ return num
85
+ }
86
+
87
+ readBoolean() {
88
+ const bool = this.readToken() === 'true'
89
+ return bool
90
+ }
91
+ }
92
+
93
+ import { write, read } from './serialization.js'
94
+
95
+ export function serializeToString(key) {
96
+ const writer = new StringWriter()
97
+ write(key, writer)
98
+ return writer.getOutput()
99
+ }
100
+
101
+ export function deserializeFromString(serialized, structure) {
102
+ const reader = new StringReader(serialized, structure)
103
+ return read(reader)
104
+ }
package/stringKey.js ADDED
@@ -0,0 +1,180 @@
1
+ import { StringWriter, StringReader } from './string.js'
2
+
3
+ export class StringKeyWriter {
4
+ // private variable for data buffer, data must be sortable in the same order as keys
5
+ #data = []
6
+ // private variable for structure buffer, structure will be added to the end of the data buffer
7
+ #structureWriter = new StringWriter()
8
+ #separator = '\x00'
9
+ constructor(separator) {
10
+ if(separator) this.#separator = separator
11
+ }
12
+
13
+ writeType(type) {
14
+ this.#structureWriter.writeType(type)
15
+ return this
16
+ }
17
+
18
+ writeSize(size) {
19
+ this.#structureWriter.writeSize(size)
20
+ return this
21
+ }
22
+
23
+ writeKey(key) {
24
+ this.#structureWriter.writeKey(key)
25
+ return this
26
+ }
27
+
28
+ writeString(str) {
29
+ this.#structureWriter.writeSize(str.length)
30
+ this.#data.push(str)
31
+ return this
32
+ }
33
+
34
+ /* writeInteger(num) {
35
+ const numString = num.toString()
36
+ const numKey = `${numString.length}:${numString}`
37
+ this.#structureWriter.writeSize(numString.length)
38
+ this.#data.push(numKey)
39
+ return this
40
+ }
41
+
42
+ writeFloat(num) {
43
+ const numString = num.toString()
44
+ const numKey = `${numString.length}:${numString}`
45
+ this.#structureWriter.writeSize(numString.length)
46
+ this.#data.push(numKey)
47
+ return this
48
+ } */
49
+
50
+ writeNumber(num) {
51
+ const exponentialString = num.toExponential()
52
+ const [mantisaString, exponentString] = exponentialString.split('e')
53
+
54
+ const mantisa = +mantisaString
55
+ const negative = mantisa < 0
56
+ const mantisaNumbers = mantisaString.slice(negative ? 1 : 0).replace('.', '')
57
+ const mantisaEncoded = negative
58
+ ? (Math.pow(10, mantisaNumbers.length) - (+mantisaNumbers))
59
+ : mantisaNumbers
60
+
61
+ const exponent = +exponentString
62
+ const negativeExponent = exponent < 0
63
+ const negativeEncoded = negative ^ negativeExponent
64
+ const exponentEncoded = negativeEncoded ? 9999 - Math.abs(exponent) : Math.abs(exponent) + 1
65
+ const digitizedExponent = exponentEncoded.toString(10).padStart(4, '0')
66
+
67
+ const prefix = (negativeEncoded ? '/' : '') + digitizedExponent
68
+ const numKey = `${negative ? '-' : ''}${prefix}v${mantisaEncoded}`
69
+ this.#structureWriter.writeSize(numKey.length)
70
+ this.#data.push(numKey)
71
+ return this
72
+ }
73
+
74
+ writeBoolean(bool) {
75
+ this.#data.push(bool ? 'true' : 'false')
76
+ return this
77
+ }
78
+
79
+ writeNull() {
80
+ return this
81
+ }
82
+
83
+ getOutput() {
84
+ //console.log('data', this.#data)
85
+ //console.log('structure', this.#structureWriter.getOutput())
86
+ const structure = this.#structureWriter.getOutput()
87
+ return this.#data.join(this.#separator) + this.#separator + structure + ':' + structure.length
88
+ }
89
+
90
+ getStructure() {
91
+ return this.#structureWriter.getOutput()
92
+ }
93
+
94
+ getData() {
95
+ return this.#data.join(this.#separator)
96
+ }
97
+
98
+ }
99
+
100
+ export class StringKeyReader {
101
+ #data
102
+ #dataPointer = 0
103
+ #structureReader
104
+
105
+ constructor(serialized) {
106
+ const lastSeparator = serialized.lastIndexOf(':')
107
+ const structureLength = parseInt(serialized.slice(lastSeparator + 1))
108
+ const structure = serialized.slice(lastSeparator - structureLength, lastSeparator)
109
+ this.#data = serialized.slice(0, lastSeparator - structureLength - 1)
110
+ this.#structureReader = new StringReader(structure)
111
+ }
112
+
113
+ readType() {
114
+ return this.#structureReader.readType()
115
+ }
116
+
117
+ readSize() {
118
+ return this.#structureReader.readSize()
119
+ }
120
+
121
+ readKey() {
122
+ return this.#structureReader.readKey()
123
+ }
124
+
125
+ readNull() {
126
+ return null
127
+ }
128
+
129
+ readString() {
130
+ const size = this.#structureReader.readSize()
131
+ const result = this.#data.slice(this.#dataPointer, this.#dataPointer + size)
132
+ this.#dataPointer += size + 1
133
+ return result
134
+ }
135
+
136
+ readNumber() {
137
+ const size = this.#structureReader.readSize()
138
+ const numKey = this.#data.slice(this.#dataPointer, this.#dataPointer + size)
139
+ this.#dataPointer += size + 1
140
+
141
+ const [prefix, encodedMantisa] = numKey.split('v')
142
+ const negative = prefix[0] === '-'
143
+ const encodedExponentString = prefix.slice(negative ? 1 : 0)
144
+ const negativeEncoded = encodedExponentString[0] == '/'
145
+ const encodedExponent = +encodedExponentString.slice(negativeEncoded ? 1 : 0)
146
+ const absoluteExponent = negativeEncoded ? (9999 - encodedExponent) : encodedExponent - 1
147
+ const negativeExponent = negativeEncoded ^ negative
148
+ const exponent = negativeExponent ? -absoluteExponent : absoluteExponent
149
+
150
+ const mantisaInteger = negative
151
+ ? ''+(Math.pow(10, encodedMantisa.length) - (+encodedMantisa))
152
+ : ''+encodedMantisa
153
+
154
+ const mantisa = mantisaInteger.length > 1
155
+ ? `${mantisaInteger[0]}.${mantisaInteger.slice(1)}`
156
+ : mantisaInteger
157
+ const exponentialString = `${negative ? '-' : ''}${mantisa}e${exponent}`
158
+
159
+ return +exponentialString
160
+ }
161
+
162
+ readBoolean() {
163
+ const result = this.#data.slice(this.#dataPointer, this.#dataPointer + 4)
164
+ this.#dataPointer += 5
165
+ return result === 'true'
166
+ }
167
+ }
168
+
169
+ import { write, read } from './serialization.js'
170
+
171
+ export function serializeKeyToString(key) {
172
+ const writer = new StringKeyWriter()
173
+ write(key, writer)
174
+ return writer.getOutput()
175
+ }
176
+
177
+ export function deserializeKeyFromString(serialized, structure) {
178
+ const reader = new StringKeyReader(serialized, structure)
179
+ return read(reader)
180
+ }
@@ -0,0 +1,144 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { serializeKeyToString, deserializeKeyFromString } from '../stringKey.js'
4
+
5
+ function testSerializeToString(data) {
6
+ const serialized = serializeKeyToString(data)
7
+ console.log('serialized', serialized)
8
+ const deserialized = deserializeKeyFromString(serialized)
9
+ console.log('deserialized', deserialized)
10
+ assert.deepStrictEqual(deserialized, data)
11
+ }
12
+
13
+ function deepCompare(a, b) {
14
+ if(typeof a !== typeof b) return 'different'
15
+ if(typeof a === 'object') {
16
+ if(Array.isArray(a) && Array.isArray(b)) {
17
+ const maxLength = Math.max(a.length, b.length)
18
+ for(let i = 0; i < maxLength; i++) {
19
+ const av = a[i]
20
+ const bv = b[i]
21
+ if(av === undefined || bv === undefined) return 'different'
22
+ const result = deepCompare(a[i], b[i])
23
+ if(result !== 'equal') return result
24
+ }
25
+ }
26
+ for(let key in b) {
27
+ const av = a[key]
28
+ const bv = b[key]
29
+ if(av === undefined || bv === undefined) return 'different'
30
+ }
31
+ for(let key in a) {
32
+ const av = a[key]
33
+ const bv = b[key]
34
+ if(av === undefined || bv === undefined) return 'different'
35
+
36
+ const result = deepCompare(av, bv)
37
+ if(result !== 'equal') return result
38
+ }
39
+ return 'equal'
40
+ }
41
+ if(a === b) return 'equal'
42
+ if(a > b) return 'greater'
43
+ if(a < b) return 'less'
44
+ return 'different'
45
+ }
46
+
47
+ function testCompareSerialization(a, b) {
48
+ const serializedA = serializeKeyToString(a)
49
+ const serializedB = serializeKeyToString(b)
50
+ const comparison = deepCompare(a, b)
51
+ const serializedComparison = deepCompare(serializedA, serializedB)
52
+ if(comparison === 'different') {
53
+ return // ignore different values
54
+ }
55
+ if(comparison !== serializedComparison) {
56
+ console.log("Compare Failed", JSON.stringify(a), JSON.stringify(b), '=>', comparison, '!=',
57
+ serializedComparison, JSON.stringify(serializedA), JSON.stringify(serializedB))
58
+ }
59
+ assert.deepStrictEqual(comparison, serializedComparison)
60
+ }
61
+
62
+ describe('stringKey serialization', () => {
63
+ it('should serialize and deserialize numbers', () => {
64
+ testSerializeToString(1)
65
+ testSerializeToString(1.1)
66
+ testSerializeToString(1.11)
67
+ testSerializeToString(0.001)
68
+ testSerializeToString(10000)
69
+ testSerializeToString(-1)
70
+ testSerializeToString(-1.1)
71
+ testSerializeToString(-1.11)
72
+ testSerializeToString(-0.001)
73
+ testSerializeToString(-10000)
74
+ })
75
+ it('should serialize and deserialize strings', () => {
76
+ testSerializeToString('')
77
+ testSerializeToString('a')
78
+ testSerializeToString('ab')
79
+ testSerializeToString('abc')
80
+ testSerializeToString('abcd')
81
+ testSerializeToString('abcde')
82
+ testSerializeToString('abcdef')
83
+ testSerializeToString('a\n\r')
84
+ testSerializeToString('a{}\t')
85
+ })
86
+ it('should serialize and deserialize booleans', () => {
87
+ testSerializeToString(true)
88
+ testSerializeToString(false)
89
+ })
90
+ it('should serialize and deserialize null', () => {
91
+ testSerializeToString(null)
92
+ })
93
+ it('should serialize and deserialize objects', () => {
94
+ testSerializeToString({ a: 1, c: 'a', b: 2 })
95
+ })
96
+ it('should serialize and deserialize arrays', () => {
97
+ testSerializeToString([1, 2, 3])
98
+ testSerializeToString(['a', 'b', 'c'])
99
+ testSerializeToString([true, false, null])
100
+ testSerializeToString([{ a: 1, c: 'a', b: 2 }, { a: 3, b: 4 }])
101
+ })
102
+
103
+ it('should leave numbers in the same order', () => {
104
+ const numers = [1, 2, 1.1, 1.11, 0.001, 10000, 10000.1, -1, -1.1, -1.11, -0.001, -10000, -10000.1]
105
+ for(let i = 0; i < numers.length; i++) {
106
+ for(let j = i + 1; j < numers.length; j++) {
107
+ testCompareSerialization(numers[i], numers[j])
108
+ }
109
+ }
110
+ })
111
+ it('should leave strings in the same order', () => {
112
+ const strings = ['', 'a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'a\n\r', 'a{}\t']
113
+ for(let i = 0; i < strings.length; i++) {
114
+ for(let j = i + 1; j < strings.length; j++) {
115
+ testCompareSerialization(strings[i], strings[j])
116
+ }
117
+ }
118
+ })
119
+ it('should leave booleans in the same order', () => {
120
+ const booleans = [true, false]
121
+ for(let i = 0; i < booleans.length; i++) {
122
+ for(let j = i + 1; j < booleans.length; j++) {
123
+ testCompareSerialization(booleans[i], booleans[j])
124
+ }
125
+ }
126
+ })
127
+ it('should leave arrays in the same order', () => {
128
+ const arrays = [[], [1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]
129
+ for(let i = 0; i < arrays.length; i++) {
130
+ for(let j = i + 1; j < arrays.length; j++) {
131
+ testCompareSerialization(arrays[i], arrays[j])
132
+ }
133
+ }
134
+ })
135
+ it('should leave objects in the same order', () => {
136
+ const objects = [{}, {a: 1}, {a: 1, b: 2}, {a: 1, b: 2, c: 3}, {a:2, b: 1}, {a: 2}, {a:1, b:1}]
137
+ for(let i = 0; i < objects.length; i++) {
138
+ for(let j = i + 1; j < objects.length; j++) {
139
+ testCompareSerialization(objects[i], objects[j])
140
+ }
141
+ }
142
+ })
143
+
144
+ })