turtb 0.5.0
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/LICENSE +661 -0
- package/bin/turtb.js +259 -0
- package/lib/display/h.js +204 -0
- package/lib/display/helpers.js +78 -0
- package/lib/display/render.js +177 -0
- package/lib/display/shapes.js +91 -0
- package/lib/turtle/Signer.js +120 -0
- package/lib/turtle/Signer.test.js +38 -0
- package/lib/turtle/TurtleBranch.js +147 -0
- package/lib/turtle/TurtleBranch.test.js +89 -0
- package/lib/turtle/TurtleDictionary.js +157 -0
- package/lib/turtle/TurtleDictionary.test.js +331 -0
- package/lib/turtle/U8aTurtle.js +203 -0
- package/lib/turtle/U8aTurtle.test.js +60 -0
- package/lib/turtle/Workspace.js +62 -0
- package/lib/turtle/Workspace.test.js +63 -0
- package/lib/turtle/codecs/CodecType.js +36 -0
- package/lib/turtle/codecs/CodecTypeVersion.js +37 -0
- package/lib/turtle/codecs/Commit.js +10 -0
- package/lib/turtle/codecs/CompositeCodec.js +86 -0
- package/lib/turtle/codecs/TreeNode.js +38 -0
- package/lib/turtle/codecs/codec.js +441 -0
- package/lib/turtle/connections/AbstractUpdater.js +176 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.js +102 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.test.js +26 -0
- package/lib/turtle/connections/TurtleBranchUpdater.js +47 -0
- package/lib/turtle/connections/TurtleDB.js +165 -0
- package/lib/turtle/connections/TurtleDB.test.js +45 -0
- package/lib/turtle/connections/TurtleTalker.js +34 -0
- package/lib/turtle/connections/TurtleTalker.test.js +101 -0
- package/lib/turtle/utils.js +192 -0
- package/lib/turtle/utils.test.js +158 -0
- package/lib/utils/Assert.js +115 -0
- package/lib/utils/NestedSet.js +68 -0
- package/lib/utils/NestedSet.test.js +30 -0
- package/lib/utils/OWN_KEYS.js +1 -0
- package/lib/utils/Recaller.js +175 -0
- package/lib/utils/Recaller.test.js +75 -0
- package/lib/utils/TestRunner.js +200 -0
- package/lib/utils/TestRunner.test.js +144 -0
- package/lib/utils/TestRunnerConstants.js +13 -0
- package/lib/utils/combineUint8ArrayLikes.js +18 -0
- package/lib/utils/combineUint8Arrays.js +23 -0
- package/lib/utils/components.js +88 -0
- package/lib/utils/crypto.js +17 -0
- package/lib/utils/deepEqual.js +16 -0
- package/lib/utils/deepEqual.test.js +27 -0
- package/lib/utils/fileTransformer.js +16 -0
- package/lib/utils/handleRedirect.js +93 -0
- package/lib/utils/logger.js +24 -0
- package/lib/utils/nextTick.js +47 -0
- package/lib/utils/noble-secp256k1.js +602 -0
- package/lib/utils/proxyWithRecaller.js +51 -0
- package/lib/utils/toCombinedVersion.js +14 -0
- package/lib/utils/toSubVersions.js +14 -0
- package/lib/utils/toVersionCount.js +5 -0
- package/lib/utils/webSocketMuxFactory.js +123 -0
- package/lib/utils/zabacaba.js +25 -0
- package/package.json +24 -0
- package/src/ArchiveUpdater.js +99 -0
- package/src/S3Updater.js +99 -0
- package/src/archiveSync.js +28 -0
- package/src/fileSync.js +155 -0
- package/src/getExistenceLength.js +19 -0
- package/src/manageCert.js +36 -0
- package/src/originSync.js +75 -0
- package/src/outletSync.js +50 -0
- package/src/proxyFolder.js +195 -0
- package/src/s3Sync.js +32 -0
- package/src/webSync.js +101 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { decodeNumberFromU8a, encodeNumberToU8a } from '../utils.js'
|
|
2
|
+
import { combineUint8Arrays } from '../../utils/combineUint8Arrays.js'
|
|
3
|
+
import { combineUint8ArrayLikes } from '../../utils/combineUint8ArrayLikes.js'
|
|
4
|
+
import { CodecType } from './CodecType.js'
|
|
5
|
+
import { Commit } from './Commit.js'
|
|
6
|
+
import { CompositeCodec } from './CompositeCodec.js'
|
|
7
|
+
import { TreeNode } from './TreeNode.js'
|
|
8
|
+
import { logSilly } from '../../utils/logger.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('../U8aTurtle.js').U8aTurtle} U8aTurtle
|
|
12
|
+
* @typedef {import('../TurtleDictionary.js').TurtleDictionary} TurtleDictionary
|
|
13
|
+
* @typedef {import('./CodecType.js').CodecType} CodecType
|
|
14
|
+
* @typedef {import('./CodecType.js').CodecOptions} CodecOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const codec = new CompositeCodec()
|
|
18
|
+
|
|
19
|
+
const minAddressBytes = 1
|
|
20
|
+
const maxAddressBytes = 4
|
|
21
|
+
const addressVersions = maxAddressBytes - minAddressBytes + 1
|
|
22
|
+
const maxWordLength = 4
|
|
23
|
+
const wordLengthVersions = maxWordLength + 1
|
|
24
|
+
const TypedArrays = [Uint8Array, Int8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Array} objectRefs
|
|
28
|
+
* @param {U8aTurtle} u8aTurtle
|
|
29
|
+
* @param {CodecOptions} options
|
|
30
|
+
* @returns {Array.<[any, any]>}
|
|
31
|
+
*/
|
|
32
|
+
const objectRefsToEntries = (objectRefs, u8aTurtle, options) => {
|
|
33
|
+
let keyRefs = objectRefs.slice(0, objectRefs.length / 2)
|
|
34
|
+
let valueRefs = objectRefs.slice(keyRefs.length)
|
|
35
|
+
if (!options.keysAsRefs) keyRefs = keyRefs.map(key => u8aTurtle.lookup(key))
|
|
36
|
+
if (!options.valuesAsRefs) valueRefs = valueRefs.map(value => u8aTurtle.lookup(value))
|
|
37
|
+
return keyRefs.map((key, index) => [key, valueRefs[index]])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Array.<[any,any]>} entries
|
|
42
|
+
* @param {TurtleDictionary} dictionary
|
|
43
|
+
* @param {CodecOptions} options
|
|
44
|
+
* @returns {Array}
|
|
45
|
+
*/
|
|
46
|
+
const entriesToObjectRefs = (entries, dictionary, options) => {
|
|
47
|
+
let keyRefs = []
|
|
48
|
+
let valueRefs = []
|
|
49
|
+
entries.forEach(([key, value]) => {
|
|
50
|
+
keyRefs.push(key)
|
|
51
|
+
valueRefs.push(value)
|
|
52
|
+
})
|
|
53
|
+
if (options.keysAsRefs) keyRefs = keyRefs.map(key => +key)
|
|
54
|
+
else keyRefs = keyRefs.map(key => dictionary.upsert(key))
|
|
55
|
+
if (!options.valuesAsRefs) valueRefs = valueRefs.map(value => dictionary.upsert(value))
|
|
56
|
+
return [...keyRefs, ...valueRefs]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {U8aTurtle} u8aTurtle
|
|
61
|
+
* @returns {[Uint8Array, Uint8Array]}
|
|
62
|
+
*/
|
|
63
|
+
export function splitEncodedCommit (u8aTurtle) {
|
|
64
|
+
if (codec.codecTypeVersionsByFooter[u8aTurtle.getByte()].codecType !== COMMIT) {
|
|
65
|
+
throw new Error('non-commit found where commit is expected')
|
|
66
|
+
}
|
|
67
|
+
const encodedCommit = codec.extractEncodedValue(u8aTurtle)
|
|
68
|
+
const encodedData = u8aTurtle.uint8Array.subarray(0, -encodedCommit.length)
|
|
69
|
+
return [encodedData, encodedCommit]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {CodecType} codecType
|
|
74
|
+
* @param {number} address
|
|
75
|
+
* @param {number} minAddressBytes
|
|
76
|
+
* @param {...number} subversions
|
|
77
|
+
* @returns {Uint8Array}
|
|
78
|
+
*/
|
|
79
|
+
function encodeAddress (codecType, address, minAddressBytes, ...subversions) {
|
|
80
|
+
const u8aAddress = encodeNumberToU8a(address, minAddressBytes)
|
|
81
|
+
const footer = codec.deriveFooter(codecType, [u8aAddress.length - minAddressBytes, ...subversions])
|
|
82
|
+
return combineUint8ArrayLikes([u8aAddress, footer])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const UNDEFINED = new CodecType({
|
|
86
|
+
name: 'undefined',
|
|
87
|
+
test: value => value === undefined,
|
|
88
|
+
decode: () => undefined,
|
|
89
|
+
encode: () => new Uint8Array([codec.deriveFooter(UNDEFINED, [0])]),
|
|
90
|
+
getWidth: () => 0,
|
|
91
|
+
versionArrayCounts: [1]
|
|
92
|
+
})
|
|
93
|
+
codec.addCodecType(UNDEFINED)
|
|
94
|
+
|
|
95
|
+
export const NULL = new CodecType({
|
|
96
|
+
name: 'null',
|
|
97
|
+
test: value => value === null,
|
|
98
|
+
decode: () => null,
|
|
99
|
+
encode: () => new Uint8Array([codec.deriveFooter(NULL, [0])]),
|
|
100
|
+
getWidth: () => 0,
|
|
101
|
+
versionArrayCounts: [1]
|
|
102
|
+
})
|
|
103
|
+
codec.addCodecType(NULL)
|
|
104
|
+
|
|
105
|
+
export const FALSE = new CodecType({
|
|
106
|
+
name: 'boolean(false)',
|
|
107
|
+
test: value => value === false,
|
|
108
|
+
decode: () => false,
|
|
109
|
+
encode: () => new Uint8Array([codec.deriveFooter(FALSE, [0])]),
|
|
110
|
+
getWidth: () => 0,
|
|
111
|
+
versionArrayCounts: [1]
|
|
112
|
+
})
|
|
113
|
+
codec.addCodecType(FALSE)
|
|
114
|
+
|
|
115
|
+
export const TRUE = new CodecType({
|
|
116
|
+
name: 'boolean(true)',
|
|
117
|
+
test: value => value === true,
|
|
118
|
+
decode: () => true,
|
|
119
|
+
encode: () => new Uint8Array([codec.deriveFooter(TRUE, [0])]),
|
|
120
|
+
getWidth: () => 0,
|
|
121
|
+
versionArrayCounts: [1]
|
|
122
|
+
})
|
|
123
|
+
codec.addCodecType(TRUE)
|
|
124
|
+
|
|
125
|
+
export const NUMBER = new CodecType({
|
|
126
|
+
name: 'number',
|
|
127
|
+
test: value => typeof value === 'number',
|
|
128
|
+
decode: (uint8Array) => new Float64Array(new Uint8Array(uint8Array).buffer)[0],
|
|
129
|
+
encode: (value) => combineUint8ArrayLikes([new Float64Array([value]), codec.deriveFooter(NUMBER, [0])]),
|
|
130
|
+
getWidth: () => 8,
|
|
131
|
+
versionArrayCounts: [1]
|
|
132
|
+
})
|
|
133
|
+
codec.addCodecType(NUMBER)
|
|
134
|
+
|
|
135
|
+
const STRING = new CodecType({
|
|
136
|
+
name: 'string',
|
|
137
|
+
test: value => typeof value === 'string',
|
|
138
|
+
decode: (uint8Array, _codecVersion, u8aTurtle) => {
|
|
139
|
+
const stringAsU8a = u8aTurtle.lookup(decodeNumberFromU8a(uint8Array))
|
|
140
|
+
return new TextDecoder().decode(stringAsU8a)
|
|
141
|
+
},
|
|
142
|
+
encode: (value, dictionary) => {
|
|
143
|
+
const stringAsU8a = new TextEncoder().encode(value)
|
|
144
|
+
const address = dictionary.upsert(stringAsU8a)
|
|
145
|
+
return encodeAddress(STRING, address, minAddressBytes)
|
|
146
|
+
},
|
|
147
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
148
|
+
versionArrayCounts: [addressVersions]
|
|
149
|
+
})
|
|
150
|
+
codec.addCodecType(STRING)
|
|
151
|
+
|
|
152
|
+
export const DATE = new CodecType({
|
|
153
|
+
name: 'date',
|
|
154
|
+
test: value => value instanceof Date,
|
|
155
|
+
decode: (uint8Array) => new Date(new Float64Array(new Uint8Array(uint8Array).buffer)[0]),
|
|
156
|
+
encode: (value) => combineUint8ArrayLikes([new Float64Array([value.getTime()]), codec.deriveFooter(DATE, [0])]),
|
|
157
|
+
getWidth: () => 8,
|
|
158
|
+
versionArrayCounts: [1]
|
|
159
|
+
})
|
|
160
|
+
codec.addCodecType(DATE)
|
|
161
|
+
|
|
162
|
+
export const BIGINT = new CodecType({
|
|
163
|
+
name: 'bigint',
|
|
164
|
+
test: value => typeof value === 'bigint',
|
|
165
|
+
decode: (uint8Array, codecVersion, u8aTurtle) => {
|
|
166
|
+
const sign = codecVersion.versionArrays[1] ? -1n : 1n
|
|
167
|
+
const hex = u8aTurtle.lookup(decodeNumberFromU8a(uint8Array))
|
|
168
|
+
return sign * BigInt(`0x${[...hex].map(byte => `0${byte.toString(16)}`.slice(-2)).join('')}`)
|
|
169
|
+
},
|
|
170
|
+
encode: (value, dictionary) => {
|
|
171
|
+
const signVersion = value < 0n ? 1 : 0
|
|
172
|
+
const sign = value < 0n ? -1n : 1n
|
|
173
|
+
let bigintHex = (sign * value).toString(16)
|
|
174
|
+
if (bigintHex.length % 2) bigintHex = `0${bigintHex}`
|
|
175
|
+
const uint8Array = new Uint8Array(bigintHex.match(/.{1,2}/g).map(hex => parseInt(hex, 16)))
|
|
176
|
+
const address = dictionary.upsert(uint8Array)
|
|
177
|
+
return encodeAddress(BIGINT, address, minAddressBytes, signVersion)
|
|
178
|
+
},
|
|
179
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
180
|
+
versionArrayCounts: [addressVersions, 2]
|
|
181
|
+
})
|
|
182
|
+
codec.addCodecType(BIGINT)
|
|
183
|
+
|
|
184
|
+
export const WORD = new CodecType({
|
|
185
|
+
name: 'uint8array(length<=4)',
|
|
186
|
+
test: value => value instanceof Uint8Array && value.length < wordLengthVersions,
|
|
187
|
+
decode: (uint8Array) => uint8Array,
|
|
188
|
+
encode: (value) => combineUint8ArrayLikes([value, codec.deriveFooter(WORD, [value.length])]),
|
|
189
|
+
getWidth: codecVersion => codecVersion.versionArrays[0],
|
|
190
|
+
versionArrayCounts: [wordLengthVersions]
|
|
191
|
+
})
|
|
192
|
+
codec.addCodecType(WORD)
|
|
193
|
+
|
|
194
|
+
export const TYPED_ARRAY = new CodecType({
|
|
195
|
+
name: 'typed-array',
|
|
196
|
+
test: value => (value instanceof Object.getPrototypeOf(Uint8Array)),
|
|
197
|
+
decode: (uint8Array, codecVersion, u8aTurtle) => {
|
|
198
|
+
let value = u8aTurtle.lookup(decodeNumberFromU8a(uint8Array))
|
|
199
|
+
if (value instanceof TreeNode) {
|
|
200
|
+
value = combineUint8Arrays([...value.inOrder(u8aTurtle)].map(address => u8aTurtle.lookup(address)))
|
|
201
|
+
} else if (value.length === 0) {
|
|
202
|
+
value = new Uint8Array()
|
|
203
|
+
}
|
|
204
|
+
const TypedArray = TypedArrays[codecVersion.versionArrays[1]]
|
|
205
|
+
return new TypedArray(value.buffer)
|
|
206
|
+
},
|
|
207
|
+
encode: (value, dictionary) => {
|
|
208
|
+
const typedArrayVersion = TypedArrays.findIndex(TypedArray => value instanceof TypedArray)
|
|
209
|
+
if (!(value instanceof Uint8Array)) {
|
|
210
|
+
value = new Uint8Array(value.buffer)
|
|
211
|
+
}
|
|
212
|
+
let address
|
|
213
|
+
if (value.length === 0) {
|
|
214
|
+
address = encodeNumberToU8a(dictionary.upsert([], [EMPTY_ARRAY]), minAddressBytes)
|
|
215
|
+
}
|
|
216
|
+
if (WORD.test(value)) {
|
|
217
|
+
address = dictionary.upsert(value, [WORD])
|
|
218
|
+
} else {
|
|
219
|
+
const wordsLength = Math.ceil(value.length / maxWordLength)
|
|
220
|
+
const words = new Array(wordsLength)
|
|
221
|
+
for (let i = 0; i < wordsLength; ++i) {
|
|
222
|
+
words[i] = dictionary.upsert(value.slice(i * maxWordLength, (i + 1) * maxWordLength), [WORD])
|
|
223
|
+
}
|
|
224
|
+
address = dictionary.upsert(words, [TREE_NODE])
|
|
225
|
+
}
|
|
226
|
+
return encodeAddress(TYPED_ARRAY, address, minAddressBytes, typedArrayVersion)
|
|
227
|
+
},
|
|
228
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
229
|
+
versionArrayCounts: [addressVersions, TypedArrays.length]
|
|
230
|
+
})
|
|
231
|
+
codec.addCodecType(TYPED_ARRAY)
|
|
232
|
+
|
|
233
|
+
export const EMPTY_ARRAY = new CodecType({
|
|
234
|
+
name: 'array(length==0)',
|
|
235
|
+
test: value => Array.isArray(value) && value.length === 0,
|
|
236
|
+
decode: () => [],
|
|
237
|
+
encode: () => new Uint8Array([codec.deriveFooter(EMPTY_ARRAY, [0])]),
|
|
238
|
+
getWidth: () => 0,
|
|
239
|
+
versionArrayCounts: [1]
|
|
240
|
+
})
|
|
241
|
+
codec.addCodecType(EMPTY_ARRAY)
|
|
242
|
+
|
|
243
|
+
export const NONEMPTY_ARRAY = new CodecType({
|
|
244
|
+
name: 'array(length>1)',
|
|
245
|
+
test: value => Array.isArray(value),
|
|
246
|
+
decode: (uint8Array, codecVersion, u8aTurtle, options) => {
|
|
247
|
+
const address = decodeNumberFromU8a(uint8Array)
|
|
248
|
+
if (codecVersion.versionArrays[1]) { // is sparse array
|
|
249
|
+
const arrayAsObject = u8aTurtle.lookup(address, options)
|
|
250
|
+
return Object.assign([], arrayAsObject)
|
|
251
|
+
}
|
|
252
|
+
u8aTurtle = u8aTurtle.getAncestorByAddress(address)
|
|
253
|
+
let refs
|
|
254
|
+
if (u8aTurtle.getCodecType(address) === TREE_NODE) {
|
|
255
|
+
const treeNode = u8aTurtle.lookup(address)
|
|
256
|
+
refs = [...treeNode.inOrder(u8aTurtle)]
|
|
257
|
+
} else {
|
|
258
|
+
refs = [address]
|
|
259
|
+
}
|
|
260
|
+
if (!options.valuesAsRefs) {
|
|
261
|
+
return refs.map(address => u8aTurtle.lookup(address))
|
|
262
|
+
}
|
|
263
|
+
return refs
|
|
264
|
+
},
|
|
265
|
+
encode: (value, dictionary, options) => {
|
|
266
|
+
let address
|
|
267
|
+
let isSparse = 0
|
|
268
|
+
if (JSON.stringify(Object.keys(value)) !== JSON.stringify(Object.keys([...value]))) { // is sparse array
|
|
269
|
+
address = dictionary.upsert(Object.assign({}, value, { length: value.length }), [OBJECT], options)
|
|
270
|
+
isSparse = 1
|
|
271
|
+
} else {
|
|
272
|
+
if (!options.valuesAsRefs) value = value.map(value => dictionary.upsert(value))
|
|
273
|
+
if (value.length === 1) {
|
|
274
|
+
address = value[0]
|
|
275
|
+
} else {
|
|
276
|
+
address = dictionary.upsert(value, [TREE_NODE])
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return encodeAddress(NONEMPTY_ARRAY, address, minAddressBytes, isSparse)
|
|
280
|
+
},
|
|
281
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
282
|
+
versionArrayCounts: [addressVersions, 2]
|
|
283
|
+
})
|
|
284
|
+
codec.addCodecType(NONEMPTY_ARRAY)
|
|
285
|
+
|
|
286
|
+
export const SET = new CodecType({
|
|
287
|
+
name: 'set',
|
|
288
|
+
test: value => value instanceof Set,
|
|
289
|
+
decode: (uint8Array, _codecVersion, u8aTurtle, options) => {
|
|
290
|
+
return new Set(u8aTurtle.lookup(decodeNumberFromU8a(uint8Array), options))
|
|
291
|
+
},
|
|
292
|
+
encode: (value, dictionary, options) => {
|
|
293
|
+
const objectAsArray = [...value.values()]
|
|
294
|
+
const address = dictionary.upsert(objectAsArray, [EMPTY_ARRAY, NONEMPTY_ARRAY], options)
|
|
295
|
+
return encodeAddress(SET, address, minAddressBytes)
|
|
296
|
+
},
|
|
297
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
298
|
+
versionArrayCounts: [addressVersions]
|
|
299
|
+
})
|
|
300
|
+
codec.addCodecType(SET)
|
|
301
|
+
|
|
302
|
+
export const MAP = new CodecType({
|
|
303
|
+
name: 'map',
|
|
304
|
+
test: value => value instanceof Map,
|
|
305
|
+
decode: (uint8Array, _codecVersion, u8aTurtle, options) => {
|
|
306
|
+
return new Map(objectRefsToEntries(
|
|
307
|
+
u8aTurtle.lookup(decodeNumberFromU8a(uint8Array)),
|
|
308
|
+
u8aTurtle,
|
|
309
|
+
options
|
|
310
|
+
))
|
|
311
|
+
},
|
|
312
|
+
encode: (value, dictionary, options) => {
|
|
313
|
+
const objectRefs = entriesToObjectRefs(value.entries(), dictionary, options)
|
|
314
|
+
const address = dictionary.upsert(objectRefs)
|
|
315
|
+
return encodeAddress(MAP, address, minAddressBytes)
|
|
316
|
+
},
|
|
317
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
318
|
+
versionArrayCounts: [addressVersions]
|
|
319
|
+
})
|
|
320
|
+
codec.addCodecType(MAP)
|
|
321
|
+
|
|
322
|
+
export const COMMIT = new CodecType({
|
|
323
|
+
name: 'commit',
|
|
324
|
+
test: value => value instanceof Commit,
|
|
325
|
+
decode: (uint8Array, codecVersion, u8aTurtle, options) => {
|
|
326
|
+
const address = decodeNumberFromU8a(uint8Array.subarray(0, codecVersion.versionArrays[0] + minAddressBytes))
|
|
327
|
+
const signature = uint8Array.subarray(-64)
|
|
328
|
+
const value = options.valuesAsRefs ? address : u8aTurtle.lookup(address)
|
|
329
|
+
return new Commit(value, signature)
|
|
330
|
+
},
|
|
331
|
+
encode: (value, dictionary, options) => {
|
|
332
|
+
const address = options.valuesAsRefs ? value.document : dictionary.upsert(value.document)
|
|
333
|
+
const u8aAddress = encodeNumberToU8a(address, minAddressBytes)
|
|
334
|
+
const footer = codec.deriveFooter(COMMIT, [u8aAddress.length - minAddressBytes])
|
|
335
|
+
return combineUint8ArrayLikes([u8aAddress, value.signature, footer])
|
|
336
|
+
},
|
|
337
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes + 64,
|
|
338
|
+
versionArrayCounts: [addressVersions],
|
|
339
|
+
isOpaque: true
|
|
340
|
+
})
|
|
341
|
+
codec.addCodecType(COMMIT)
|
|
342
|
+
|
|
343
|
+
export const OBJECT = new CodecType({
|
|
344
|
+
name: 'object',
|
|
345
|
+
test: value => typeof value === 'object',
|
|
346
|
+
decode: (uint8Array, _codecVersion, u8aTurtle, options) => {
|
|
347
|
+
return Object.fromEntries(objectRefsToEntries(
|
|
348
|
+
u8aTurtle.lookup(decodeNumberFromU8a(uint8Array)),
|
|
349
|
+
u8aTurtle,
|
|
350
|
+
options
|
|
351
|
+
))
|
|
352
|
+
},
|
|
353
|
+
encode: (value, dictionary, options) => {
|
|
354
|
+
const objectRefs = entriesToObjectRefs(Object.entries(value), dictionary, options)
|
|
355
|
+
const address = dictionary.upsert(objectRefs)
|
|
356
|
+
return encodeAddress(OBJECT, address, minAddressBytes)
|
|
357
|
+
},
|
|
358
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + minAddressBytes,
|
|
359
|
+
versionArrayCounts: [addressVersions]
|
|
360
|
+
})
|
|
361
|
+
codec.addCodecType(OBJECT)
|
|
362
|
+
|
|
363
|
+
export const TREE_NODE = new CodecType({
|
|
364
|
+
name: 'tree-node',
|
|
365
|
+
test: value => Array.isArray(value) && value.length > 1,
|
|
366
|
+
decode: (uint8Array, codecVersion) => {
|
|
367
|
+
const [leftAddressLength] = codecVersion.versionArrays
|
|
368
|
+
const leftAddress = decodeNumberFromU8a(uint8Array.subarray(0, leftAddressLength + minAddressBytes))
|
|
369
|
+
const rightAddress = decodeNumberFromU8a(uint8Array.subarray(leftAddressLength + minAddressBytes))
|
|
370
|
+
return new TreeNode(leftAddress, rightAddress)
|
|
371
|
+
},
|
|
372
|
+
encode: (value, dictionary) => {
|
|
373
|
+
const leftLength = 2 ** (31 - Math.clz32(value.length - 1))
|
|
374
|
+
let leftAddress
|
|
375
|
+
if (leftLength === 1) leftAddress = encodeNumberToU8a(value[0], minAddressBytes)
|
|
376
|
+
else leftAddress = encodeNumberToU8a(dictionary.upsert(value.slice(0, leftLength), [TREE_NODE]), minAddressBytes)
|
|
377
|
+
let rightAddress
|
|
378
|
+
if (value.length === leftLength + 1) rightAddress = encodeNumberToU8a(value[value.length - 1], minAddressBytes)
|
|
379
|
+
else rightAddress = encodeNumberToU8a(dictionary.upsert(value.slice(leftLength), [TREE_NODE]), minAddressBytes)
|
|
380
|
+
const footer = codec.deriveFooter(TREE_NODE, [leftAddress.length - minAddressBytes, rightAddress.length - minAddressBytes])
|
|
381
|
+
return combineUint8ArrayLikes([leftAddress, rightAddress, footer])
|
|
382
|
+
},
|
|
383
|
+
getWidth: codecVersion => codecVersion.versionArrays[0] + codecVersion.versionArrays[1] + 2 * (minAddressBytes),
|
|
384
|
+
versionArrayCounts: [addressVersions, addressVersions]
|
|
385
|
+
})
|
|
386
|
+
codec.addCodecType(TREE_NODE)
|
|
387
|
+
|
|
388
|
+
export const ATOMIC_UINT8ARRAY = new CodecType({
|
|
389
|
+
name: 'atomic-uintarray',
|
|
390
|
+
test: value => value instanceof Uint8Array,
|
|
391
|
+
decode: (uint8Array, codecTypeVersion) => {
|
|
392
|
+
const valueLengthLength = codecTypeVersion.versionArrays[0] + 1
|
|
393
|
+
return uint8Array.subarray(0, -valueLengthLength)
|
|
394
|
+
},
|
|
395
|
+
encode: (value) => {
|
|
396
|
+
const encodedValueLength = encodeNumberToU8a(value.length, 1)
|
|
397
|
+
const footer = codec.deriveFooter(ATOMIC_UINT8ARRAY, [encodedValueLength.length - 1])
|
|
398
|
+
return combineUint8ArrayLikes([value, encodedValueLength, footer])
|
|
399
|
+
},
|
|
400
|
+
getWidth: (codecVersion, u8aTurtle, index) => {
|
|
401
|
+
const valueLengthLength = codecVersion.versionArrays[0] + 1
|
|
402
|
+
const encodedValueLength = u8aTurtle.slice(index - valueLengthLength, index)
|
|
403
|
+
const valueLength = decodeNumberFromU8a(encodedValueLength)
|
|
404
|
+
return valueLength + valueLengthLength
|
|
405
|
+
},
|
|
406
|
+
versionArrayCounts: [3]
|
|
407
|
+
})
|
|
408
|
+
codec.addCodecType(ATOMIC_UINT8ARRAY)
|
|
409
|
+
|
|
410
|
+
export const OPAQUE_UINT8ARRAY = new CodecType({
|
|
411
|
+
name: 'opaque-uint8array',
|
|
412
|
+
test: value => value instanceof Uint8Array,
|
|
413
|
+
decode: (uint8Array, codecTypeVersion) => {
|
|
414
|
+
const valueLengthLength = codecTypeVersion.versionArrays[0] + 1
|
|
415
|
+
return uint8Array.subarray(0, -valueLengthLength)
|
|
416
|
+
},
|
|
417
|
+
encode: (value) => {
|
|
418
|
+
const encodedValueLength = encodeNumberToU8a(value.length, 1)
|
|
419
|
+
const footer = codec.deriveFooter(OPAQUE_UINT8ARRAY, [encodedValueLength.length - 1])
|
|
420
|
+
return combineUint8ArrayLikes([value, encodedValueLength, footer])
|
|
421
|
+
},
|
|
422
|
+
getWidth: (codecVersion, u8aTurtle, index) => {
|
|
423
|
+
const valueLengthLength = codecVersion.versionArrays[0] + 1
|
|
424
|
+
const encodedValueLength = u8aTurtle.slice(index - valueLengthLength, index)
|
|
425
|
+
const valueLength = decodeNumberFromU8a(encodedValueLength)
|
|
426
|
+
return valueLength + valueLengthLength
|
|
427
|
+
},
|
|
428
|
+
versionArrayCounts: [3],
|
|
429
|
+
isOpaque: true
|
|
430
|
+
})
|
|
431
|
+
codec.addCodecType(OPAQUE_UINT8ARRAY)
|
|
432
|
+
|
|
433
|
+
setTimeout(() => logSilly(() => console.log(
|
|
434
|
+
codec.codecTypeVersionsByFooter.map((ctv, i) => `${
|
|
435
|
+
i.toString(2).padStart(8, '0')
|
|
436
|
+
} ${
|
|
437
|
+
ctv.codecType.name
|
|
438
|
+
} ${
|
|
439
|
+
ctv.versionArrays
|
|
440
|
+
}`).join('\n'))
|
|
441
|
+
), 0)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { logDebug } from '../../utils/logger.js'
|
|
2
|
+
import { Recaller } from '../../utils/Recaller.js'
|
|
3
|
+
import { OPAQUE_UINT8ARRAY } from '../codecs/codec.js'
|
|
4
|
+
import { verifyCommitU8a } from '../Signer.js'
|
|
5
|
+
import { TurtleDictionary } from '../TurtleDictionary.js'
|
|
6
|
+
import { b36ToUint8Array, deepEqualUint8Arrays } from '../utils.js'
|
|
7
|
+
import { TurtleTalker } from './TurtleTalker.js'
|
|
8
|
+
|
|
9
|
+
export class AbstractUpdater extends TurtleTalker {
|
|
10
|
+
#isUpdating = false
|
|
11
|
+
#incomingUint8ArraysByAddress = {}
|
|
12
|
+
#outgoingAddressesByUint8Array = new Map()
|
|
13
|
+
#previousOutgoingAddressesAddress
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} name
|
|
16
|
+
* @param {string} publicKey
|
|
17
|
+
* @param {boolean} [Xours=false]
|
|
18
|
+
* @param {Recaller} [recaller=new Recaller(`${name}.recaller`)]
|
|
19
|
+
*/
|
|
20
|
+
constructor (
|
|
21
|
+
name,
|
|
22
|
+
publicKey,
|
|
23
|
+
Xours = false,
|
|
24
|
+
recaller = new Recaller(`${name}.recaller`)
|
|
25
|
+
) {
|
|
26
|
+
super(name, Xours, recaller)
|
|
27
|
+
if (!publicKey) throw new Error('Updaters need a publicKey')
|
|
28
|
+
this.publicKey = publicKey
|
|
29
|
+
this.outgoingDictionary = new TurtleDictionary(`${name}.outgoingDictionary`, recaller)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
start () {
|
|
33
|
+
if (this._started) return
|
|
34
|
+
this._started = true
|
|
35
|
+
this.incomingBranch.recaller.watch(`${JSON.stringify(this.name)}.start`, () => {
|
|
36
|
+
const incomingUint8ArrayAddresses = this.incomingBranch.lookup()?.uint8ArrayAddresses
|
|
37
|
+
this.update(incomingUint8ArrayAddresses)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getUint8ArraysLength () { throw new Error('class extending AbstractTurtleTalker must implement getUint8ArraysLength') }
|
|
42
|
+
async setUint8ArraysLength (length) { throw new Error('class extending AbstractTurtleTalker must implement getUint8ArraysLength') }
|
|
43
|
+
async getUint8Array (index) { throw new Error('class extending AsyncTurtleBranchInterface must implement getUint8Array') }
|
|
44
|
+
async pushUint8Array (uint8Array) { throw new Error('class extending AsyncTurtleBranchInterface must implement pushUint8Array') }
|
|
45
|
+
async popUint8Array () { throw new Error('class extending AsyncTurtleBranchInterface must implement popUint8Array') }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Array.<number>} incomingUint8ArrayAddresses
|
|
49
|
+
*/
|
|
50
|
+
update = async (incomingUint8ArrayAddresses) => {
|
|
51
|
+
if (this.#isUpdating) return this.recaller.reportKeyAccess(this, '#isUpdating', 'update', JSON.stringify(this.name)) // try again when when it's done updating
|
|
52
|
+
this.#isUpdating = true
|
|
53
|
+
let length = await this.getUint8ArraysLength()
|
|
54
|
+
const outgoingTurtleTalk = { uint8ArrayAddresses: [], ts: new Date().getTime() }
|
|
55
|
+
if (incomingUint8ArrayAddresses) { // they're ready
|
|
56
|
+
// handle incoming message (if any exist)
|
|
57
|
+
logUpdate(this.name, this.publicKey, incomingUint8ArrayAddresses, true)
|
|
58
|
+
await Promise.all(incomingUint8ArrayAddresses.map(indexString => this.getUint8Array(+indexString)))
|
|
59
|
+
for (const indexString in incomingUint8ArrayAddresses) {
|
|
60
|
+
const i = +indexString
|
|
61
|
+
const incomingAddress = incomingUint8ArrayAddresses[i]
|
|
62
|
+
const incomingUint8Array = this.incomingBranch.lookup(incomingAddress)
|
|
63
|
+
if (i < length) { // we should already have this one
|
|
64
|
+
const ourUint8Array = await this.getUint8Array(i)
|
|
65
|
+
if (this.#incomingUint8ArraysByAddress[incomingAddress] === undefined && deepEqualUint8Arrays(ourUint8Array, incomingUint8Array)) {
|
|
66
|
+
this.#incomingUint8ArraysByAddress[incomingAddress] = ourUint8Array
|
|
67
|
+
}
|
|
68
|
+
if (this.#incomingUint8ArraysByAddress[incomingAddress] !== ourUint8Array) { // collision!
|
|
69
|
+
if (this.Xours) {
|
|
70
|
+
incomingUint8ArrayAddresses.length = i
|
|
71
|
+
break
|
|
72
|
+
} else {
|
|
73
|
+
await this.setUint8ArraysLength(i)
|
|
74
|
+
length = await this.getUint8ArraysLength()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (i === length) { // we don't have this one yet
|
|
79
|
+
if (this.publicKey) {
|
|
80
|
+
const previousUint8Array = i && await this.getUint8Array(i - 1)
|
|
81
|
+
if (this.Xours && !(await verifyCommitU8a(this.publicKey, incomingUint8Array, previousUint8Array))) {
|
|
82
|
+
if (this.Xours) {
|
|
83
|
+
incomingUint8ArrayAddresses.length = Math.max(i - 1, 0)
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await this.pushUint8Array(incomingUint8Array)
|
|
89
|
+
length = await this.getUint8ArraysLength()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const startingIndex = Math.min(incomingUint8ArrayAddresses.length, length)
|
|
93
|
+
if (startingIndex > 0 && this.Xours) {
|
|
94
|
+
const uint8Array = await this.getUint8Array(startingIndex - 1)
|
|
95
|
+
if (this.#outgoingAddressesByUint8Array.get(uint8Array) === undefined) {
|
|
96
|
+
this.#outgoingAddressesByUint8Array.set(uint8Array, this.outgoingDictionary.upsert(uint8Array, [OPAQUE_UINT8ARRAY]))
|
|
97
|
+
outgoingTurtleTalk.uint8ArrayAddresses[startingIndex - 1] = this.#outgoingAddressesByUint8Array.get(uint8Array)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (let i = startingIndex; i < length; ++i) { // send them what they're missing
|
|
101
|
+
const uint8Array = await this.getUint8Array(i)
|
|
102
|
+
if (this.#outgoingAddressesByUint8Array.get(uint8Array) === undefined) {
|
|
103
|
+
this.#outgoingAddressesByUint8Array.set(uint8Array, this.outgoingDictionary.upsert(uint8Array, [OPAQUE_UINT8ARRAY]))
|
|
104
|
+
}
|
|
105
|
+
outgoingTurtleTalk.uint8ArrayAddresses[i] = this.#outgoingAddressesByUint8Array.get(uint8Array)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
outgoingTurtleTalk.uint8ArrayAddresses.length = length
|
|
110
|
+
const outgoingAddressesAddress = this.outgoingDictionary.upsert(outgoingTurtleTalk.uint8ArrayAddresses)
|
|
111
|
+
if (this.#previousOutgoingAddressesAddress !== outgoingAddressesAddress) {
|
|
112
|
+
this.#previousOutgoingAddressesAddress = outgoingAddressesAddress
|
|
113
|
+
this.outgoingDictionary.upsert(outgoingTurtleTalk)
|
|
114
|
+
this.outgoingDictionary.squash(this.outgoingBranch.index + 1)
|
|
115
|
+
this.outgoingBranch.u8aTurtle = this.outgoingDictionary.u8aTurtle
|
|
116
|
+
logDebug(() => console.log(this.name, this.publicKey))
|
|
117
|
+
logUpdate(this.name, this.publicKey, outgoingTurtleTalk.uint8ArrayAddresses, false)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.#isUpdating = false
|
|
121
|
+
this.recaller.reportKeyMutation(this, '#isUpdating', 'update', JSON.stringify(this.name))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @type {Promise.<void>}
|
|
126
|
+
*/
|
|
127
|
+
get settle () {
|
|
128
|
+
let resolve
|
|
129
|
+
const settlePromise = new Promise((...args) => { [resolve] = args })
|
|
130
|
+
const checkSettle = () => {
|
|
131
|
+
const incoming = this.incomingBranch.lookup()
|
|
132
|
+
logDebug(() => console.log('checkSettle', this.turtleBranch.index + 1, '>=', incoming?.uint8ArrayAddresses?.length))
|
|
133
|
+
if (this.turtleBranch.index + 1 >= incoming?.uint8ArrayAddresses?.length) {
|
|
134
|
+
this.incomingBranch.recaller.unwatch(checkSettle)
|
|
135
|
+
resolve()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.incomingBranch.recaller.watch(`TBMux"${this.name}".settle`, checkSettle)
|
|
139
|
+
return settlePromise
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} name
|
|
145
|
+
* @param {string} publicKey
|
|
146
|
+
* @param {Array.<number>} uint8ArrayAddresses
|
|
147
|
+
* @param {boolean} isIncoming
|
|
148
|
+
*/
|
|
149
|
+
export function logUpdate (name, publicKey, uint8ArrayAddresses, isIncoming) {
|
|
150
|
+
const separator = isIncoming ? '\x1b[31m <- \x1b[0m' : '\x1b[32m -> \x1b[0m'
|
|
151
|
+
const type = isIncoming ? '\x1b[31m(incoming)\x1b[0m' : '\x1b[32m(outgoing)\x1b[0m'
|
|
152
|
+
// let publicKey = tbMuxBranch.lookup('publicKey')
|
|
153
|
+
const [r0, g0, b0, r1, g1, b1] = b36ToUint8Array(publicKey).slice(-6).map(v => Math.round(255 - v * v / 255).toString())
|
|
154
|
+
const colorBlock = `\x1b[48;2;${r0};${g0};${b0};38;2;${r1};${g1};${b1}m▛▞▖🐢 ▝▞▟\x1b[0m`
|
|
155
|
+
let prettyAddresses = []
|
|
156
|
+
publicKey = `<${publicKey.slice(0, 4)}...${publicKey.slice(-4)}>`
|
|
157
|
+
const leftmost = uint8ArrayAddresses.findIndex(x => x !== undefined)
|
|
158
|
+
if (leftmost === -1) {
|
|
159
|
+
prettyAddresses.push(`\x1b[2mempty × ${uint8ArrayAddresses.length}]\x1b[0m`)
|
|
160
|
+
} else {
|
|
161
|
+
if (leftmost > 0) {
|
|
162
|
+
prettyAddresses.push(`\x1b[2mempty × ${leftmost}\x1b[0m`)
|
|
163
|
+
}
|
|
164
|
+
if (uint8ArrayAddresses.length > leftmost + 4) {
|
|
165
|
+
prettyAddresses.push(`\x1b[34m${uint8ArrayAddresses[leftmost]}\x1b[0m`)
|
|
166
|
+
prettyAddresses.push(`\x1b[2m... (${uint8ArrayAddresses.length - leftmost - 2})\x1b[0m`)
|
|
167
|
+
prettyAddresses.push(`\x1b[34m${uint8ArrayAddresses[uint8ArrayAddresses.length - 1]}\x1b[0m`)
|
|
168
|
+
} else {
|
|
169
|
+
for (let i = leftmost; i < uint8ArrayAddresses.length; ++i) {
|
|
170
|
+
prettyAddresses.push(`\x1b[34m${uint8ArrayAddresses[i]}\x1b[0m`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
prettyAddresses = `(${uint8ArrayAddresses.length}) [${prettyAddresses.join(', ')}]`
|
|
175
|
+
logDebug(() => console.log(`${colorBlock} ${[publicKey, type, `\x1b[31m${JSON.stringify(name)}\x1b[0m`, prettyAddresses].join(separator)}`))
|
|
176
|
+
}
|