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,203 @@
1
+ import { codec } from './codecs/codec.js'
2
+ import { AS_REFS } from './codecs/CodecType.js'
3
+ import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
4
+ import { zabacaba } from '../utils/zabacaba.js'
5
+
6
+ /**
7
+ * @typedef {import('./codecs/CodecType.js').CodecOptions} CodecOptions
8
+ */
9
+
10
+ export class U8aTurtle {
11
+ /** @type {Array.<U8aTurtle>} */
12
+ seekLayers = []
13
+
14
+ /**
15
+ * @param {Uint8Array} uint8Array
16
+ * @param {U8aTurtle} parent
17
+ */
18
+ constructor (uint8Array, parent) {
19
+ if (!uint8Array) throw new Error('missing Uint8Array')
20
+ this.uint8Array = uint8Array
21
+ this.parent = parent
22
+ if (parent) {
23
+ this.offset = parent.length
24
+ this.index = parent.index + 1
25
+ this.length = parent.length + uint8Array.length
26
+ let seekLayer = parent
27
+ const seekCount = zabacaba(this.index)
28
+ for (let i = 0; i < seekCount; ++i) {
29
+ this.seekLayers.unshift(seekLayer)
30
+ seekLayer = seekLayer.seekLayers[0]
31
+ }
32
+ } else {
33
+ this.offset = 0
34
+ this.index = 0
35
+ this.length = uint8Array.length
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {number} start
41
+ * @param {number} end
42
+ * @returns {Array.<U8aTurtle>}
43
+ */
44
+ getAncestors (start = 0, end = this.index) {
45
+ if (start > this.index || start < 0) throw new Error('start out of range')
46
+ if (end > this.index || end < 0) throw new Error('end out of range')
47
+ const ancestors = new Array(end + 1 - start)
48
+ let index = end
49
+ let ancestor = this.getAncestorByIndex(index)
50
+ while (ancestor && index >= start) {
51
+ ancestors[index - start] = ancestor
52
+ --index
53
+ ancestor = ancestor.parent
54
+ }
55
+ return ancestors
56
+ }
57
+
58
+ /**
59
+ * @param {number} index
60
+ * @param {number} tooHigh
61
+ * @returns {U8aTurtle}
62
+ */
63
+ getAncestorByIndex (index, tooHigh) {
64
+ if (index < 0) index += this.index
65
+ if (index === this.index) return this
66
+ if (index < this.index) {
67
+ for (const seekLayer of this.seekLayers.filter(seekLayer => seekLayer !== tooHigh)) {
68
+ const found = seekLayer.getAncestorByIndex(index, tooHigh)
69
+ if (found) return found
70
+ tooHigh = seekLayer
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {U8aTurtle} u8aTurtle
77
+ * @returns {boolean}
78
+ */
79
+ hasAncestor (u8aTurtle) {
80
+ if (u8aTurtle === undefined) return true
81
+ return this.getAncestorByIndex(u8aTurtle.index) === u8aTurtle
82
+ }
83
+
84
+ /**
85
+ * @param {number} address
86
+ * @param {number} tooHigh
87
+ * @returns {U8aTurtle}
88
+ */
89
+ getAncestorByAddress (address, tooHigh) {
90
+ if (address < 0) address += this.length
91
+ if (address >= this.offset && address < this.length) return this
92
+ if (address < this.offset) {
93
+ for (const seekLayer of this.seekLayers.filter(seekLayer => seekLayer !== tooHigh)) {
94
+ const found = seekLayer.getAncestorByAddress(address, tooHigh)
95
+ if (found) return found
96
+ tooHigh = seekLayer
97
+ }
98
+ }
99
+ }
100
+
101
+ #remapAddress (address, isLengthOkay = false) {
102
+ if (address < 0) address += this.length
103
+ if (address < this.offset) throw new Error(`address (${address}) out of range: < offset (${this.offset})`)
104
+ if (address > this.length) throw new Error(`address (${address}) out of range: > length (${this.length})`)
105
+ if (!isLengthOkay && address === this.length) throw new Error(`address (${address}) out of range: === length (${this.length})`)
106
+ return address - this.offset
107
+ }
108
+
109
+ getByte (address = this.length - 1) {
110
+ return this.uint8Array[this.#remapAddress(address)]
111
+ }
112
+
113
+ slice (start = this.offset, end = this.length) {
114
+ return this.uint8Array.subarray(this.#remapAddress(start), this.#remapAddress(end, true))
115
+ }
116
+
117
+ getAddressAtPath (startingAddress = this.length - 1, ...path) {
118
+ if (!path.length) return startingAddress
119
+ const u8aTurtle = this.getAncestorByAddress(startingAddress)
120
+ const codecVersion = codec.getCodecTypeVersion(u8aTurtle.getByte(startingAddress))
121
+ const ref = codecVersion.decode(u8aTurtle, startingAddress, AS_REFS)
122
+ const key = path.shift()
123
+ if (!ref || !(key in ref)) return
124
+ return u8aTurtle.getAddressAtPath(ref[key], ...path)
125
+ }
126
+
127
+ /**
128
+ * @param {[optional_address:number, ...path:Array.<string>, optional_options:CodecOptions]} path
129
+ * @returns {any}
130
+ */
131
+ lookup (...path) {
132
+ let startingAddress = this.length - 1
133
+ if (typeof path[0] === 'number') startingAddress = path.shift()
134
+ else if (typeof path[0] === 'undefined') path.shift()
135
+ /** @type {CodecOptions} */
136
+ let options
137
+ if (/object|undefined/.test(typeof path[path.length - 1])) options = path.pop()
138
+ const address = this.getAddressAtPath(startingAddress, ...path)
139
+ if (address === undefined) return
140
+ if (address instanceof Uint8Array) return address
141
+ const u8aTurtle = this.getAncestorByAddress(address)
142
+ if (!u8aTurtle) {
143
+ console.warn('no u8aTurtle found for address', { address, path, startingAddress, length: this.length })
144
+ return
145
+ }
146
+ const codecVersion = codec.getCodecTypeVersion(u8aTurtle.getByte(address))
147
+ return codecVersion.decode(u8aTurtle, address, options)
148
+ }
149
+
150
+ getCodecType (address = this.length - 1) {
151
+ const footer = this.getAncestorByAddress(address).getByte(address)
152
+ return codec.getCodecTypeVersion(footer)?.codecType
153
+ }
154
+
155
+ /**
156
+ * @param {number} start
157
+ * @param {number} end
158
+ * @returns {Array.<Uint8Array>}
159
+ */
160
+ exportUint8Arrays (start = 0, end = this.index) {
161
+ return this.getAncestors(start, end).map(u8aTurtle => u8aTurtle.uint8Array)
162
+ }
163
+
164
+ clone () { return fromUint8Arrays(this.exportUint8Arrays().map(uint8Array => new Uint8Array(uint8Array))) }
165
+ }
166
+
167
+ /**
168
+ * @param {U8aTurtle} u8aTurtle
169
+ * @param {number} downToIndex
170
+ * @returns {U8aTurtle}
171
+ */
172
+ export function squashTurtle (u8aTurtle, downToIndex = 0) {
173
+ return new U8aTurtle(
174
+ combineUint8Arrays(u8aTurtle.exportUint8Arrays(downToIndex)),
175
+ u8aTurtle.getAncestorByIndex(downToIndex).parent
176
+ )
177
+ }
178
+
179
+ /**
180
+ * @param {U8aTurtle} a
181
+ * @param {U8aTurtle} b
182
+ * @returns {U8aTurtle}
183
+ */
184
+ export function findCommonAncestor (a, b) {
185
+ if (!a || !b) return
186
+ const minIndex = Math.min(a.index, b.index)
187
+ a = a.getAncestorByIndex(minIndex)
188
+ b = b.getAncestorByIndex(minIndex)
189
+ while (a !== b) {
190
+ a = a.parent
191
+ b = b.parent
192
+ }
193
+ return a
194
+ }
195
+
196
+ /**
197
+ * @param {Array.<Uint8Array>} uint8Arrays
198
+ * @returns {U8aTurtle}
199
+ */
200
+ export function fromUint8Arrays (uint8Arrays) {
201
+ if (!uint8Arrays?.length) throw new Error('empty uint8Arrays')
202
+ return uint8Arrays.reduce((u8aTurtle, uint8Array) => new U8aTurtle(uint8Array, u8aTurtle), undefined)
203
+ }
@@ -0,0 +1,60 @@
1
+ import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
2
+ import { findCommonAncestor, fromUint8Arrays, squashTurtle, U8aTurtle } from './U8aTurtle.js'
3
+
4
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
5
+ suite.it('constructs correctly', ({ assert }) => {
6
+ const a = new U8aTurtle(new Uint8Array([0, 1, 2, 3]))
7
+ assert.equal(a.index, 0, 'a.index')
8
+ assert.equal(a.length, 4, 'a.length')
9
+
10
+ const b = new U8aTurtle(new Uint8Array([4, 5, 6, 7]), a)
11
+ assert.equal(b.index, 1, 'b.index')
12
+ assert.equal(b.length, 8, 'b.length')
13
+
14
+ assert.equal(b.getAncestorByAddress(1), a, 'a is correct b.parent')
15
+ assert.equal(b.getAncestorByIndex(0), a, 'a is correct b.parent')
16
+ assert.equal(b.getByte(6), 6, '6th byte is 6')
17
+ assert.equal(b.getByte(7), 7, '7th byte is 7')
18
+ assert.equal(b.getByte(), 7, 'last byte is 7')
19
+ assert.throw(() => {
20
+ b.getByte(2)
21
+ }, 'no out of range bytes')
22
+
23
+ const c = new U8aTurtle(new Uint8Array([8, 9]), b)
24
+ assert.equal(c.getAncestorByAddress(1), a, 'a is correct c.parent')
25
+ assert.equal(c.getAncestorByIndex(0), a, 'a is correct c.parent')
26
+ assert.equal(c.getAncestorByAddress(6).getByte(6), 6, '6th byte is 6')
27
+
28
+ assert.equal(b.slice(7, 8), new Uint8Array([7]))
29
+ assert.equal(b.slice(4, 7), new Uint8Array([4, 5, 6]))
30
+ assert.equal(b.slice(4), new Uint8Array([4, 5, 6, 7]))
31
+ assert.equal(b.slice(-3, -2), new Uint8Array([5]))
32
+
33
+ let head = c
34
+ for (let i = 0; i < 10; ++i) {
35
+ head = new U8aTurtle(new Uint8Array(), head)
36
+ }
37
+
38
+ assert.equal(squashTurtle(head).uint8Array, new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
39
+ assert.equal(squashTurtle(head, 1).uint8Array, new Uint8Array([4, 5, 6, 7, 8, 9]))
40
+
41
+ assert.equal(findCommonAncestor(a, b), a)
42
+ assert.equal(findCommonAncestor(a, c), a)
43
+ assert.equal(findCommonAncestor(b, c), b)
44
+
45
+ const d = new U8aTurtle(new Uint8Array([8, 9]), b)
46
+ assert.equal(findCommonAncestor(d, c), b)
47
+ })
48
+ suite.it('clones from exported Uint8Arrays', ({ assert }) => {
49
+ let u8aTurtle = new U8aTurtle(new Uint8Array([0, 1, 2]))
50
+ u8aTurtle = new U8aTurtle(new Uint8Array([3, 4, 5]), u8aTurtle)
51
+ u8aTurtle = new U8aTurtle(new Uint8Array([6, 7]), u8aTurtle)
52
+ const copy = fromUint8Arrays(u8aTurtle.exportUint8Arrays())
53
+ const clone = u8aTurtle.clone()
54
+ assert.equal(u8aTurtle.exportUint8Arrays(), copy.exportUint8Arrays())
55
+ assert.equal(u8aTurtle.exportUint8Arrays(), clone.exportUint8Arrays())
56
+ copy.parent.uint8Array[1] = 9
57
+ assert.equal(u8aTurtle.exportUint8Arrays(), copy.exportUint8Arrays())
58
+ assert.notEqual(u8aTurtle.exportUint8Arrays(), clone.exportUint8Arrays())
59
+ })
60
+ })
@@ -0,0 +1,62 @@
1
+ import { IGNORE_MUTATE, Recaller } from '../utils/Recaller.js'
2
+ import { AS_REFS } from './codecs/CodecType.js'
3
+ import { TurtleBranch } from './TurtleBranch.js'
4
+ import { TurtleDictionary } from './TurtleDictionary.js'
5
+
6
+ /**
7
+ * @typedef {import('./Signer.js').Signer} Signer
8
+ * @typedef {import('./TurtleBranch.js').TurtleBranch} TurtleBranch
9
+ */
10
+
11
+ export class Workspace extends TurtleDictionary {
12
+ /**
13
+ * @param {string} name
14
+ * @param {Signer} signer
15
+ * @param {Recaller} [recaller=new Recaller(name)]
16
+ * @param {TurtleBranch} [committedBranch=new TurtleBranch(`${name}.committedBranch`, recaller)]
17
+ */
18
+ constructor (name, signer, recaller = new Recaller(name), committedBranch = new TurtleBranch(`${name}.committedBranch`, recaller)) {
19
+ super(name, recaller, committedBranch.u8aTurtle)
20
+ this.signer = signer
21
+ this.committedBranch = committedBranch
22
+ this.committedBranch.recaller.watch(`update Workspace with committed changes:${this.name}`, () => {
23
+ if (!this.committedBranch.u8aTurtle) return
24
+ if (this.committedBranch.u8aTurtle === this.u8aTurtle) return
25
+ if (this.committedBranch.u8aTurtle && this.u8aTurtle) {
26
+ if (this.u8aTurtle.hasAncestor(this.committedBranch.u8aTurtle)) return // uncommitted changes
27
+ }
28
+ this.u8aTurtle = this.committedBranch.u8aTurtle
29
+ this.lexicograph()
30
+ })
31
+ }
32
+
33
+ get lastCommit () { return this.committedBranch.lookup()?.document }
34
+ get lastCommitValue () { return this.lastCommit?.value }
35
+
36
+ async #queueCommit (value, message, asRef, lastQueuedCommit) {
37
+ await lastQueuedCommit
38
+ if (this.u8aTurtle && !this.u8aTurtle.hasAncestor(this.committedBranch.u8aTurtle)) {
39
+ throw new Error('committedBranch must be ancestor of workspace (merge required)')
40
+ }
41
+ const valueRef = asRef ? value : this.upsert(value)
42
+ const address = this.recaller.call(() => {
43
+ return this.upsert({
44
+ message: this.upsert(message),
45
+ name: this.upsert(this.name),
46
+ username: this.upsert(this.signer.username),
47
+ ts: this.upsert(new Date()),
48
+ value: valueRef
49
+ }, undefined, AS_REFS)
50
+ }, IGNORE_MUTATE)
51
+ this.append(await this.signer.signCommit(this.name, address, this.u8aTurtle, this.committedBranch.u8aTurtle))
52
+ this.squash((this.committedBranch?.index ?? -1) + 1)
53
+ this.committedBranch.u8aTurtle = this.u8aTurtle
54
+ return this
55
+ }
56
+
57
+ async commit (value, message, asRef = typeof value === 'number') {
58
+ if (asRef && typeof value !== 'number') throw new Error(`commit asRef must be number, received ${typeof value}`)
59
+ this._queuedCommit = this.#queueCommit(value, message, asRef, this._queuedCommit)
60
+ return this._queuedCommit
61
+ }
62
+ }
@@ -0,0 +1,63 @@
1
+ import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
2
+ import { AS_REFS } from './codecs/CodecType.js'
3
+ import { Signer } from './Signer.js'
4
+ import { TurtleBranch } from './TurtleBranch.js'
5
+ import { Workspace } from './Workspace.js'
6
+
7
+ const tics = async (count, ticLabel = '') => {
8
+ for (let i = 0; i < count; ++i) {
9
+ if (ticLabel) console.log(`${ticLabel}, tic: ${i}`)
10
+ await new Promise(resolve => setTimeout(resolve))
11
+ }
12
+ }
13
+
14
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
15
+ suite.it('handles commits', async ({ assert }) => {
16
+ const signer = new Signer('test1', 'password1')
17
+ const committedBranch1 = new TurtleBranch('committedBranch1')
18
+ const workspace1 = new Workspace('workspace1', signer, committedBranch1.recaller, committedBranch1)
19
+ const workspace2 = new Workspace('workspace2', signer, committedBranch1.recaller, committedBranch1)
20
+ await workspace1.commit('abcd', 'commit 1')
21
+ assert.notEqual(JSON.stringify(workspace1.lookup()), JSON.stringify(workspace2.lookup()))
22
+ await tics(2)
23
+ assert.equal(JSON.stringify(workspace1.lookup()), JSON.stringify(workspace2.lookup()))
24
+ await workspace2.commit('qwer', 'commit 2')
25
+ assert.notEqual(JSON.stringify(workspace1.lookup()), JSON.stringify(workspace2.lookup()))
26
+ await tics(2)
27
+ assert.equal(JSON.stringify(workspace1.lookup()), JSON.stringify(workspace2.lookup()))
28
+ const string1 = 'test string 1'
29
+ const address1 = workspace1.upsert(string1)
30
+ await workspace1.commit({ address1 }, 'commit 1')
31
+ await tics(2)
32
+ assert.equal(workspace2.lookup(workspace2.lastCommitValue.address1), string1)
33
+ })
34
+ suite.it('handles simultanous commits', async ({ assert }) => {
35
+ const signer = new Signer('test1', 'password1')
36
+ const committedBranch1 = new TurtleBranch('committedBranch1')
37
+ const workspace = new Workspace('workspace1', signer, committedBranch1.recaller, committedBranch1)
38
+ await Promise.all([
39
+ workspace.commit('one', 'commit 1'),
40
+ workspace.commit('two', 'commit 2')
41
+ ])
42
+ assert.equal(workspace.index, 1)
43
+ assert.equal(workspace.lookup().document.value, 'two')
44
+ })
45
+ suite.it('handles upsertFile and lookupFile', async ({ assert }) => {
46
+ const signer = new Signer('test1', 'password1')
47
+ const committedBranch1 = new TurtleBranch('committedBranch1')
48
+ const workspace = new Workspace('workspace1', signer, committedBranch1.recaller, committedBranch1)
49
+ const address1 = workspace.upsertFile('file1.txt', ['line 1', 'line 2', 'line 3'])
50
+ await workspace.commit(address1, 'commit 1')
51
+ assert.equal(workspace.lookupFile('file1.txt'), 'line 1\nline 2\nline 3')
52
+ const address2 = workspace.upsertFile('file2.json', { a: 1, b: 2, c: 3 })
53
+ const address3 = workspace.upsertFile('file3.bin', new Uint8Array([1, 2, 3, 4, 5]), address2)
54
+ await workspace.commit(address3, 'commit 3')
55
+ assert.equal(workspace.lookupFile('file2.json'), JSON.stringify({ a: 1, b: 2, c: 3 }, null, 2))
56
+ assert.equal(workspace.lookupFile('file3.bin'), new Uint8Array([1, 2, 3, 4, 5]))
57
+ const address4 = workspace.upsertFile('file1.txt', null)
58
+ await workspace.commit(address4, 'commit 4')
59
+ assert.equal(workspace.lookupFile('file1.txt'), undefined)
60
+ const refs = workspace.lookup('document', 'value', AS_REFS)
61
+ assert.equal(Object.keys(refs).length, 2)
62
+ })
63
+ })
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @typedef {import('./CodecTypeVersion.js').CodecTypeVersion} CodecTypeVersion
3
+ * @typedef {import('../U8aTurtle.js').U8aTurtle} U8aTurtle
4
+ * @typedef {import('../TurtleDictionary.js').TurtleDictionary} TurtleDictionary
5
+ */
6
+
7
+ /**
8
+ * @typedef CodecOptions
9
+ * @property {boolean} keysAsRefs
10
+ * @property {boolean} valuesAsRefs
11
+ */
12
+ export const AS_REFS = { keysAsRefs: false, valuesAsRefs: true }
13
+ export const DEREFERENCE = { keysAsRefs: false, valuesAsRefs: false }
14
+
15
+ export class CodecType {
16
+ /**
17
+ * @param {{
18
+ * name: string,
19
+ * test: (value:any) => boolean,
20
+ * decode: (uint8Array: Uint8Array, codecTypeVersion: CodecTypeVersion, u8aTurtle: U8aTurtle, options: CodecOptions) => any,
21
+ * encode: (value: any, dictionary: TurtleDictionary, options: CodecOptions) => Uint8Array,
22
+ * getWidth: (codecTypeVersion: CodecTypeVersion, u8aTurtle: U8aTurtle, index: number) => number,
23
+ * versionArrayCounts: Array.<number>,
24
+ * isOpaque: boolean
25
+ * }}
26
+ */
27
+ constructor ({ name, test, decode, encode, getWidth, versionArrayCounts, isOpaque }) {
28
+ this.name = name
29
+ this.test = test
30
+ this.decode = decode
31
+ this.encode = encode
32
+ this.getWidth = getWidth
33
+ this.versionArrayCounts = versionArrayCounts
34
+ this.isOpaque = isOpaque
35
+ }
36
+ }
@@ -0,0 +1,37 @@
1
+ import { toSubVersions } from '../../utils/toSubVersions.js'
2
+ import { DEREFERENCE } from './CodecType.js'
3
+
4
+ export class CodecTypeVersion {
5
+ /**
6
+ * @param {import('./CodecType.js').CodecType} codecType
7
+ * @param {number} combinedVersion
8
+ */
9
+ constructor (codecType, combinedVersion) {
10
+ /** @type {import('./CodecType.js').CodecType} */
11
+ this.codecType = codecType
12
+ this.combinedVersion = combinedVersion
13
+ this.versionArrays = toSubVersions(combinedVersion, codecType.versionArrayCounts)
14
+ }
15
+
16
+ /**
17
+ *
18
+ * @param {import('../U8aTurtle.js').U8aTurtle} u8aTurtle
19
+ * @param {number} [address=u8aTurtle.length - 1]
20
+ * @returns {number}
21
+ */
22
+ getWidth (u8aTurtle, address = u8aTurtle.length - 1) {
23
+ return this.codecType.getWidth(this, u8aTurtle, address)
24
+ }
25
+
26
+ /**
27
+ * @param {import('../U8aTurtle.js').U8aTurtle} u8aTurtle
28
+ * @param {number} [address=u8aTurtle.length - 1]
29
+ * @param {import('./CodecType.js').CodecOptions} [options=DEREFERENCE]
30
+ */
31
+ decode (u8aTurtle, address = u8aTurtle.length - 1, options = DEREFERENCE) {
32
+ const width = this.getWidth(u8aTurtle, address)
33
+ const uint8Array = u8aTurtle.slice(address - width, address)
34
+ const value = this.codecType.decode(uint8Array, this, u8aTurtle, options)
35
+ return value
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ export class Commit {
2
+ /**
3
+ * @param {Object} document
4
+ * @param {Uint8Array} signature
5
+ */
6
+ constructor (document, signature) {
7
+ this.document = document
8
+ this.signature = signature
9
+ }
10
+ }
@@ -0,0 +1,86 @@
1
+ import { logError } from '../../utils/logger.js'
2
+ import { toCombinedVersion } from '../../utils/toCombinedVersion.js'
3
+ import { toVersionCount } from '../../utils/toVersionCount.js'
4
+ import { DEREFERENCE } from './CodecType.js'
5
+ import { CodecTypeVersion } from './CodecTypeVersion.js'
6
+
7
+ /**
8
+ * @typedef {import('./CodecType.js').CodecType} CodecType
9
+ * @typedef {import('../U8aTurtle.js').U8aTurtle} U8aTurtle
10
+ */
11
+
12
+ export class CompositeCodec {
13
+ /** @type {Array.<CodecType>} */
14
+ codecTypes = []
15
+ /** @type {Object.<string, CodecType>} */
16
+ codecTypesByName = {}
17
+ /** @type {Array.<CodecTypeVersion>} */
18
+ codecTypeVersionsByFooter = []
19
+ /** @type {Map.<CodecType, Array} */
20
+ footerByCodecTypeAndCombinedVersions = new Map()
21
+
22
+ encodeValue (value, codecsArray = this.codecTypes, dictionary, options = DEREFERENCE) {
23
+ const codecType = codecsArray.find(codecType => codecType.test(value)) // first match wins
24
+ if (!codecType) {
25
+ logError(() => console.error('no match', value))
26
+ throw new Error('no encoder for value')
27
+ }
28
+ const uint8Array = codecType.encode(value, dictionary, options)
29
+ return { uint8Array, codecType }
30
+ }
31
+
32
+ getCodecTypeVersion (footer) {
33
+ const codecVersion = this.codecTypeVersionsByFooter[footer]
34
+ if (!codecVersion) {
35
+ throw new Error(`getCodecTypeVersion failed for footer: ${footer}`)
36
+ }
37
+ return codecVersion
38
+ }
39
+
40
+ getCodecType (name) { return this.codecTypesByName[name] }
41
+ deriveFooter (codecType, versionArrays) {
42
+ const footerByCombinedVersions = this.footerByCodecTypeAndCombinedVersions.get(codecType)
43
+ const combinedVersion = toCombinedVersion(versionArrays, codecType.versionArrayCounts)
44
+ return footerByCombinedVersions[combinedVersion]
45
+ }
46
+
47
+ /**
48
+ * @param {U8aTurtle} u8aTurtle
49
+ * @param {number} address
50
+ */
51
+ extractEncodedValue (u8aTurtle, address = u8aTurtle.length - 1) {
52
+ const codecVersion = this.extractCodecTypeVersion(u8aTurtle, address)
53
+ if (!codecVersion) {
54
+ logError(() => console.error({ address, footer: u8aTurtle.getByte(address) }))
55
+ throw new Error('no decoder for footer')
56
+ }
57
+ const width = codecVersion.getWidth(u8aTurtle, address)
58
+ return u8aTurtle.slice(address - width, address + 1) // include footer
59
+ }
60
+
61
+ /**
62
+ * @param {U8aTurtle} u8aTurtle
63
+ * @param {number} address
64
+ */
65
+ extractCodecTypeVersion (u8aTurtle, address = u8aTurtle.length - 1) {
66
+ const footer = u8aTurtle.getByte(address)
67
+ return this.getCodecTypeVersion(footer)
68
+ }
69
+
70
+ /**
71
+ * @param {CodecType} codecType
72
+ */
73
+ addCodecType (codecType, testFirst = false) {
74
+ const versionCount = toVersionCount(codecType.versionArrayCounts)
75
+ const footerByVersion = new Array(versionCount)
76
+ for (let combinedVersion = 0; combinedVersion < versionCount; ++combinedVersion) {
77
+ const footer = this.codecTypeVersionsByFooter.length
78
+ footerByVersion[combinedVersion] = footer
79
+ this.codecTypeVersionsByFooter.push(new CodecTypeVersion(codecType, combinedVersion))
80
+ }
81
+ this.footerByCodecTypeAndCombinedVersions.set(codecType, footerByVersion)
82
+ this.codecTypesByName[codecType.name] = codecType
83
+ if (testFirst) this.codecTypes.unshift(codecType)
84
+ else this.codecTypes.push(codecType)
85
+ }
86
+ }
@@ -0,0 +1,38 @@
1
+ import { codec, TREE_NODE } from './codec.js'
2
+
3
+ /**
4
+ * @typedef {import('../U8aTurtle.js').U8aTurtle} U8aTurtle
5
+ */
6
+
7
+ export class TreeNode {
8
+ /**
9
+ * @param {number} leftAddress
10
+ * @param {number} rightAddress
11
+ */
12
+ constructor (leftAddress, rightAddress) {
13
+ this.leftAddress = leftAddress
14
+ this.rightAddress = rightAddress
15
+ }
16
+
17
+ /**
18
+ * @param {U8aTurtle} u8aTurtle
19
+ */
20
+ * inOrder (u8aTurtle) {
21
+ const leftTurtle = u8aTurtle.getAncestorByAddress(this.leftAddress)
22
+ const leftFooter = leftTurtle.getByte(this.leftAddress)
23
+ if (codec.getCodecTypeVersion(leftFooter).codecType === TREE_NODE) {
24
+ const left = leftTurtle.lookup(this.leftAddress)
25
+ yield * left.inOrder(leftTurtle)
26
+ } else {
27
+ yield this.leftAddress
28
+ }
29
+ const rightTurtle = u8aTurtle.getAncestorByAddress(this.rightAddress)
30
+ const rightFooter = rightTurtle.getByte(this.rightAddress)
31
+ if (codec.getCodecTypeVersion(rightFooter).codecType === TREE_NODE) {
32
+ const right = rightTurtle.lookup(this.rightAddress)
33
+ yield * right.inOrder(rightTurtle)
34
+ } else {
35
+ yield this.rightAddress
36
+ }
37
+ }
38
+ }