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.
Files changed (70) hide show
  1. package/LICENSE +661 -0
  2. package/bin/turtb.js +259 -0
  3. package/lib/display/h.js +204 -0
  4. package/lib/display/helpers.js +78 -0
  5. package/lib/display/render.js +177 -0
  6. package/lib/display/shapes.js +91 -0
  7. package/lib/turtle/Signer.js +120 -0
  8. package/lib/turtle/Signer.test.js +38 -0
  9. package/lib/turtle/TurtleBranch.js +147 -0
  10. package/lib/turtle/TurtleBranch.test.js +89 -0
  11. package/lib/turtle/TurtleDictionary.js +157 -0
  12. package/lib/turtle/TurtleDictionary.test.js +331 -0
  13. package/lib/turtle/U8aTurtle.js +203 -0
  14. package/lib/turtle/U8aTurtle.test.js +60 -0
  15. package/lib/turtle/Workspace.js +62 -0
  16. package/lib/turtle/Workspace.test.js +63 -0
  17. package/lib/turtle/codecs/CodecType.js +36 -0
  18. package/lib/turtle/codecs/CodecTypeVersion.js +37 -0
  19. package/lib/turtle/codecs/Commit.js +10 -0
  20. package/lib/turtle/codecs/CompositeCodec.js +86 -0
  21. package/lib/turtle/codecs/TreeNode.js +38 -0
  22. package/lib/turtle/codecs/codec.js +441 -0
  23. package/lib/turtle/connections/AbstractUpdater.js +176 -0
  24. package/lib/turtle/connections/TurtleBranchMultiplexer.js +102 -0
  25. package/lib/turtle/connections/TurtleBranchMultiplexer.test.js +26 -0
  26. package/lib/turtle/connections/TurtleBranchUpdater.js +47 -0
  27. package/lib/turtle/connections/TurtleDB.js +165 -0
  28. package/lib/turtle/connections/TurtleDB.test.js +45 -0
  29. package/lib/turtle/connections/TurtleTalker.js +34 -0
  30. package/lib/turtle/connections/TurtleTalker.test.js +101 -0
  31. package/lib/turtle/utils.js +192 -0
  32. package/lib/turtle/utils.test.js +158 -0
  33. package/lib/utils/Assert.js +115 -0
  34. package/lib/utils/NestedSet.js +68 -0
  35. package/lib/utils/NestedSet.test.js +30 -0
  36. package/lib/utils/OWN_KEYS.js +1 -0
  37. package/lib/utils/Recaller.js +175 -0
  38. package/lib/utils/Recaller.test.js +75 -0
  39. package/lib/utils/TestRunner.js +200 -0
  40. package/lib/utils/TestRunner.test.js +144 -0
  41. package/lib/utils/TestRunnerConstants.js +13 -0
  42. package/lib/utils/combineUint8ArrayLikes.js +18 -0
  43. package/lib/utils/combineUint8Arrays.js +23 -0
  44. package/lib/utils/components.js +88 -0
  45. package/lib/utils/crypto.js +17 -0
  46. package/lib/utils/deepEqual.js +16 -0
  47. package/lib/utils/deepEqual.test.js +27 -0
  48. package/lib/utils/fileTransformer.js +16 -0
  49. package/lib/utils/handleRedirect.js +93 -0
  50. package/lib/utils/logger.js +24 -0
  51. package/lib/utils/nextTick.js +47 -0
  52. package/lib/utils/noble-secp256k1.js +602 -0
  53. package/lib/utils/proxyWithRecaller.js +51 -0
  54. package/lib/utils/toCombinedVersion.js +14 -0
  55. package/lib/utils/toSubVersions.js +14 -0
  56. package/lib/utils/toVersionCount.js +5 -0
  57. package/lib/utils/webSocketMuxFactory.js +123 -0
  58. package/lib/utils/zabacaba.js +25 -0
  59. package/package.json +24 -0
  60. package/src/ArchiveUpdater.js +99 -0
  61. package/src/S3Updater.js +99 -0
  62. package/src/archiveSync.js +28 -0
  63. package/src/fileSync.js +155 -0
  64. package/src/getExistenceLength.js +19 -0
  65. package/src/manageCert.js +36 -0
  66. package/src/originSync.js +75 -0
  67. package/src/outletSync.js +50 -0
  68. package/src/proxyFolder.js +195 -0
  69. package/src/s3Sync.js +32 -0
  70. 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
+ }