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,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,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
|
+
}
|