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,177 @@
1
+ function _constructValue (parts, node) {
2
+ if (parts == null) return null
3
+ if (typeof parts === 'function') {
4
+ parts = parts(node)
5
+ }
6
+ if (Array.isArray(parts)) {
7
+ const mappedParts = parts.map(part => {
8
+ if (typeof part === 'function') {
9
+ part = part(node)
10
+ }
11
+ if (part == null) return ''
12
+ if (part && part.type === 'part') {
13
+ return part.value
14
+ } else {
15
+ return part
16
+ }
17
+ })
18
+ if (mappedParts.length === 1) {
19
+ return mappedParts[0]
20
+ }
21
+ return mappedParts.join('')
22
+ }
23
+ return parts
24
+ }
25
+
26
+ function _renderAttributes (attributes, node) {
27
+ let obj = {}
28
+ attributes.forEach(attribute => {
29
+ if (typeof attribute === 'function') {
30
+ attribute = attribute(node)
31
+ }
32
+ if (attribute == null) return
33
+ if (attribute && attribute.type === 'attribute') {
34
+ const name = attribute.name
35
+ if (name in obj) return
36
+ const value = _constructValue(attribute.value, node)
37
+ if (value == null) {
38
+ obj[name] = name
39
+ } else {
40
+ obj[name] = value
41
+ }
42
+ } else if (Array.isArray(attribute)) {
43
+ obj = Object.assign(_renderAttributes(attribute, node), obj)
44
+ } else if (typeof attribute === 'object') {
45
+ Object.entries(attribute).forEach(([name, value]) => {
46
+ if (name in obj) return
47
+ obj[name] = value
48
+ })
49
+ } else {
50
+ const name = attribute.toString()
51
+ if (name in obj) return
52
+ obj[name] = name
53
+ }
54
+ })
55
+ return obj
56
+ }
57
+
58
+ function _setAttribute (element, name, value) {
59
+ if (element[name] !== value) {
60
+ try {
61
+ element[name] = value
62
+ } catch (e) {
63
+ // SVGs don't like getting their properties set and that's okay...
64
+ }
65
+ }
66
+ if (!(typeof value).match(/(?:boolean|number|string)/)) {
67
+ value = name
68
+ }
69
+ const str = value.toString()
70
+ if (element.getAttribute(name) !== str) {
71
+ element.setAttribute(name, str)
72
+ }
73
+ return element
74
+ }
75
+
76
+ function _setAttributes (element, attributes) {
77
+ Object.entries(attributes).forEach(([name, value]) => {
78
+ _setAttribute(element, name, value)
79
+ })
80
+ return element
81
+ }
82
+
83
+ function _pruneAttributes (element, newAttributes, oldAttributes) {
84
+ const orphans = new Set(Object.keys(oldAttributes))
85
+ Object.keys(newAttributes).forEach(attribute => orphans.delete(attribute))
86
+ orphans.forEach(attribute => {
87
+ element.removeAttribute(attribute)
88
+ delete element[attribute]
89
+ })
90
+ }
91
+
92
+ const _descriptionMap = new Map()
93
+ function _descriptionsToNodes (descriptions, xmlns, recaller, debugString) {
94
+ if (!Array.isArray(descriptions)) {
95
+ descriptions = [descriptions]
96
+ }
97
+ const nodes = []
98
+ descriptions.forEach(description => {
99
+ if (typeof description === 'function') {
100
+ description = description()
101
+ }
102
+ if (description != null) {
103
+ if (Array.isArray(description)) {
104
+ nodes.push(..._descriptionsToNodes(description, xmlns, recaller, debugString))
105
+ } else {
106
+ if (description.tag === null || description.tag === '') {
107
+ nodes.push(..._descriptionsToNodes(description.children, xmlns, recaller, debugString))
108
+ } else if (typeof description.tag === 'function') {
109
+ const attributes = _renderAttributes(description.attributes)
110
+ nodes.push(..._descriptionsToNodes(description.tag(attributes, description.children, description), attributes.xmlns || xmlns, recaller, debugString))
111
+ } else if (description.type) {
112
+ if (!_descriptionMap.has(description)) {
113
+ let node
114
+ if (description.type === 'textnode') {
115
+ node = document.createTextNode(description.value)
116
+ } else {
117
+ let oldAttributes = {}
118
+ let newAttributes = _renderAttributes(description.attributes, node)
119
+ node = document.createElementNS(newAttributes.xmlns || xmlns, description.tag, { is: newAttributes.is })
120
+ _setAttributes(node, newAttributes)
121
+ render(node, description.children, recaller, debugString, newAttributes.xmlns || xmlns)
122
+ recaller.watch(debugString, () => {
123
+ newAttributes = _renderAttributes(description.attributes, node)
124
+ _setAttributes(node, newAttributes)
125
+ _pruneAttributes(node, newAttributes, oldAttributes)
126
+ oldAttributes = newAttributes
127
+ })
128
+ }
129
+ _descriptionMap.set(description, node)
130
+ }
131
+ nodes.push(_descriptionMap.get(description))
132
+ } else {
133
+ nodes.push(document.createTextNode(description.toString()))
134
+ }
135
+ }
136
+ }
137
+ })
138
+ return nodes
139
+ }
140
+
141
+ function _setChildren (element, descriptions, xmlns, recaller, debugString) {
142
+ const newNodes = _descriptionsToNodes(descriptions, xmlns, recaller, debugString)
143
+ newNodes.forEach((newNode, index) => {
144
+ while (element.childNodes[index] !== newNode) {
145
+ const oldNode = element.childNodes[index]
146
+ if (!oldNode) {
147
+ element.appendChild(newNode)
148
+ } else if (newNodes.indexOf(oldNode) > index) {
149
+ element.insertBefore(newNode, oldNode)
150
+ } else {
151
+ element.removeChild(oldNode)
152
+ }
153
+ }
154
+ })
155
+ while (element.childNodes.length > newNodes.length) {
156
+ element.removeChild(element.lastChild)
157
+ }
158
+ return element
159
+ }
160
+
161
+ /**
162
+ * @param {HTMLElement} element
163
+ * @param {array|object|function|null} descriptions
164
+ * @param {import('../utils/Recaller.js').Recaller} recaller
165
+ * @param {string} debugString
166
+ * @param {string} [xmlns='http://www.w3.org/1999/xhtml']
167
+ */
168
+ export function render (element, descriptions, recaller, debugString, xmlns = 'http://www.w3.org/1999/xhtml') {
169
+ if (!descriptions) {
170
+ return _descriptionsToNodes(element, xmlns, recaller, debugString)
171
+ }
172
+ function f () {
173
+ return _setChildren(element, descriptions, xmlns, recaller, debugString)
174
+ }
175
+ recaller.watch(debugString, f)
176
+ return f
177
+ }
@@ -0,0 +1,91 @@
1
+ import { h } from './h.js'
2
+
3
+ export const cog = (attributes = {}, children = []) => {
4
+ attributes.viewBox ??= '-1 -1 2 2'
5
+ attributes.width ??= attributes.height ?? '1em'
6
+ attributes.height ??= attributes.width ?? '1em'
7
+ attributes.xmlns = 'http://www.w3.org/2000/svg'
8
+ const teeth = +attributes.teeth || 6
9
+ const holeR = +attributes.holeR || 0.25
10
+ const innerR = +attributes.innerR || 0.60
11
+ const outerR = +attributes.outerR || 0.90
12
+ const outerWeight = +attributes.outerWeight || 6
13
+ const innerWeight = +attributes.innerWeight || outerWeight * outerR / innerR
14
+ const transWeight = +attributes.transWeight || 3
15
+ const totalWeight = outerWeight + innerWeight + transWeight * 2
16
+ const toXY = (outerness, aroundness) => {
17
+ const r = outerR * outerness + innerR * (1 - outerness)
18
+ const theta = (aroundness - 0.25 - outerWeight / totalWeight / (2 * teeth)) * 2 * Math.PI
19
+ return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
20
+ }
21
+ return h`<svg ${attributes}>
22
+ <path d="
23
+ M ${toXY(1, 0)}
24
+ ${[...Array(teeth).keys().map(tooth =>
25
+ `
26
+ A ${outerR} ${outerR} 0 0 1 ${toXY(1, (tooth + outerWeight / totalWeight) / teeth)}
27
+ L ${toXY(0, (tooth + (outerWeight + transWeight) / totalWeight) / teeth)}
28
+ A ${innerR} ${innerR} 0 0 1 ${toXY(0, (tooth + (outerWeight + transWeight + innerWeight) / totalWeight) / teeth)}
29
+ L ${toXY(1, (tooth + 1) / teeth)}
30
+ `
31
+ )].join('')}
32
+ Z
33
+ M 0 -${holeR}
34
+ A ${holeR} ${holeR} 0 1 0 0 ${holeR}
35
+ A ${holeR} ${holeR} 0 1 0 0 -${holeR}
36
+ Z
37
+ "/>
38
+ ${children}
39
+ </svg>`
40
+ }
41
+
42
+ export const star = (attributes = {}, children = []) => {
43
+ attributes.viewBox ??= '-1 -1 2 2'
44
+ attributes.width ??= attributes.height ?? '1em'
45
+ attributes.height ??= attributes.width ?? '1em'
46
+ attributes.xmlns = 'http://www.w3.org/2000/svg'
47
+ const points = +attributes.points || 5
48
+ const innerR = +attributes.innerR || 0.60
49
+ const outerR = +attributes.outerR || 0.90
50
+ const toXY = (outerness, aroundness) => {
51
+ const r = outerR * outerness + innerR * (1 - outerness)
52
+ const theta = (aroundness - 0.25) * 2 * Math.PI
53
+ return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
54
+ }
55
+ return h`<svg ${attributes}>
56
+ <path d="
57
+ M ${toXY(1, 0)}
58
+ ${[...Array(points).keys().map(point =>
59
+ `
60
+ L ${toXY(0, (point + 0.5) / points)}
61
+ L ${toXY(1, (point + 1) / points)}
62
+ `
63
+ )].join('')}
64
+ Z
65
+ "/>
66
+ ${children}
67
+ </svg>`
68
+ }
69
+
70
+ export const equilateral = (attributes = {}, children = []) => {
71
+ attributes.viewBox ??= '-1 -1 2 2'
72
+ attributes.width ??= attributes.height ?? '1em'
73
+ attributes.height ??= attributes.width ?? '1em'
74
+ attributes.xmlns = 'http://www.w3.org/2000/svg'
75
+ const corners = +attributes.corners || 3
76
+ const r = +attributes.r || 0.90
77
+ const toXY = (aroundness) => {
78
+ const theta = (aroundness - 0.25) * 2 * Math.PI
79
+ return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
80
+ }
81
+ return h`<svg ${attributes}>
82
+ <path d="
83
+ M ${toXY(0)}
84
+ ${[...Array(corners).keys().map(corner =>
85
+ `L ${toXY((corner + 1) / corners)}`
86
+ )].join('')}
87
+ Z
88
+ "/>
89
+ ${children}
90
+ </svg>`
91
+ }
@@ -0,0 +1,120 @@
1
+ import { cryptoPromise, hashNameAndPassword } from '../utils/crypto.js'
2
+ import { getPublicKey, signAsync, verify } from '../utils/noble-secp256k1.js'
3
+ import { codec, COMMIT, splitEncodedCommit } from './codecs/codec.js'
4
+ import { AS_REFS } from './codecs/CodecType.js'
5
+ import { Commit } from './codecs/Commit.js'
6
+ import { b36ToUint8Array, uint8ArrayToB36 } from './utils.js'
7
+ import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
8
+ import { U8aTurtle } from './U8aTurtle.js'
9
+ import { logError } from '../utils/logger.js'
10
+
11
+ /**
12
+ * @typedef {import('./U8aTurtle.js').U8aTurtle} U8aTurtle
13
+ * @typedef {import('./codecs/Commit.js').Commit} Commit
14
+ */
15
+
16
+ /**
17
+ * @typedef KeyPair
18
+ * @property {string} privateKey
19
+ * @property {string} publicKey
20
+ */
21
+
22
+ export class Signer {
23
+ /** @type {Object.<string, KeyPair>} */
24
+ keysByName = {}
25
+
26
+ constructor (username, password) {
27
+ this.username = username
28
+ this.hashwordPromise = hashNameAndPassword(username, password)
29
+ }
30
+
31
+ /**
32
+ * @param {string} turtlename
33
+ * @returns {KeyPair}
34
+ */
35
+ async makeKeysFor (turtlename) {
36
+ if (!(this.keysByName[turtlename])) {
37
+ const hashword = await this.hashwordPromise
38
+ const privateKey = await hashNameAndPassword(turtlename, hashword)
39
+ const publicKey = uint8ArrayToB36(getPublicKey(privateKey))
40
+ this.keysByName[turtlename] = { privateKey, publicKey }
41
+ }
42
+ return this.keysByName[turtlename]
43
+ }
44
+
45
+ /**
46
+ * @param {string} turtleName
47
+ * @param {Uint8Array} uint8Array
48
+ * @returns {Uint8Array}
49
+ */
50
+ async sign (turtleName, uint8Array) {
51
+ const { privateKey } = await this.makeKeysFor(turtleName)
52
+ const hash = await digestData(uint8Array)
53
+ const signature = await signAsync(hash, privateKey)
54
+ return signature.toCompactRawBytes()
55
+ }
56
+
57
+ /**
58
+ * @param {string} turtlename
59
+ * @param {number} commitAddress
60
+ * @param {U8aTurtle} u8aTurtle
61
+ * @param {U8aTurtle} committedTurtle
62
+ * @return {Uint8Array}
63
+ */
64
+ async signCommit (turtlename, commitAddress, u8aTurtle, committedTurtle) {
65
+ const uint8Arrays = u8aTurtle.exportUint8Arrays((committedTurtle?.index ?? -1) + 1)
66
+ if (committedTurtle) {
67
+ const previousEncodedCommit = splitEncodedCommit(committedTurtle)[1]
68
+ uint8Arrays.unshift(previousEncodedCommit)
69
+ }
70
+ const signature = await this.sign(turtlename, combineUint8Arrays(uint8Arrays))
71
+ const commit = new Commit(commitAddress, signature)
72
+ return COMMIT.encode(commit, undefined, AS_REFS)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @param {U8aTurtle} u8aTurtle
78
+ * @param {string} publicKey
79
+ */
80
+ export async function verifyTurtleCommit (u8aTurtle, publicKey) {
81
+ try {
82
+ const footer = u8aTurtle.getByte()
83
+ const codecVersion = codec.getCodecTypeVersion(footer)
84
+ if (codecVersion.codecType !== COMMIT) {
85
+ throw new Error('last value must be Commit')
86
+ }
87
+ /** @type {Commit} */
88
+ const commit = codecVersion.decode(u8aTurtle, undefined, AS_REFS)
89
+ let uint8Array = u8aTurtle.slice(undefined, -codecVersion.getWidth(u8aTurtle) - 1)
90
+ if (u8aTurtle.parent) {
91
+ const previousEncodedCommit = splitEncodedCommit(u8aTurtle.parent)[1]
92
+ uint8Array = combineUint8Arrays([previousEncodedCommit, uint8Array])
93
+ }
94
+ const hash = await digestData(uint8Array)
95
+ return verify(commit.signature, hash, b36ToUint8Array(publicKey))
96
+ } catch (error) {
97
+ logError(() => console.error(error))
98
+ return false
99
+ }
100
+ }
101
+
102
+ /**
103
+ *
104
+ * @param {string} publicKey
105
+ * @param {Uint8Array} uint8Array
106
+ * @param {Uint8Array} [previousUint8Array]
107
+ */
108
+ export function verifyCommitU8a (publicKey, uint8Array, previousUint8Array) {
109
+ const u8aTurtle = new U8aTurtle(
110
+ uint8Array,
111
+ previousUint8Array && new U8aTurtle(previousUint8Array)
112
+ )
113
+ return verifyTurtleCommit(u8aTurtle, publicKey)
114
+ }
115
+
116
+ const digestData = async uint8Array => {
117
+ const { subtle } = await cryptoPromise
118
+ const digested = await subtle.digest('SHA-256', uint8Array)
119
+ return new Uint8Array(digested)
120
+ }
@@ -0,0 +1,38 @@
1
+ import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
2
+ import { Signer, verifyCommitU8a, verifyTurtleCommit } from './Signer.js'
3
+ import { squashTurtle, U8aTurtle } from './U8aTurtle.js'
4
+ import { OPAQUE_UINT8ARRAY } from './codecs/codec.js'
5
+
6
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
7
+ suite.it('signs and verifies commits', async ({ assert }) => {
8
+ const signer = new Signer('signer1', 'password1')
9
+ const name = 'branch1'
10
+ const u8Array = new Uint8Array([1, 2, 3])
11
+ let u8aTurtle = new U8aTurtle(OPAQUE_UINT8ARRAY.encode(u8Array))
12
+ const u8aAddress = u8aTurtle.length - 1
13
+ const signedCommit = await signer.signCommit(name, u8aAddress, u8aTurtle)
14
+
15
+ u8aTurtle = new U8aTurtle(signedCommit, u8aTurtle)
16
+ u8aTurtle = squashTurtle(u8aTurtle, 0)
17
+ const committedTurtle = u8aTurtle
18
+ assert.equal(committedTurtle.lookup().document, u8Array)
19
+ const keys = await signer.makeKeysFor(name)
20
+ const verification0 = await verifyCommitU8a(keys.publicKey, u8aTurtle.getAncestorByIndex(0).uint8Array)
21
+ assert.assert(verification0)
22
+ const verification = await verifyTurtleCommit(committedTurtle, keys.publicKey)
23
+ assert.assert(verification)
24
+
25
+ const u8Array2 = new Uint8Array([4, 5, 6, 7])
26
+ u8aTurtle = new U8aTurtle(OPAQUE_UINT8ARRAY.encode(u8Array2), u8aTurtle)
27
+ const u8aAddress2 = u8aTurtle.length - 1
28
+ const signedCommit2 = await signer.signCommit(name, u8aAddress2, u8aTurtle, committedTurtle)
29
+ u8aTurtle = new U8aTurtle(signedCommit2, u8aTurtle)
30
+ u8aTurtle = squashTurtle(u8aTurtle, committedTurtle.index + 1)
31
+ assert.equal(u8aTurtle.lookup().document, u8Array2)
32
+ const verification2 = await verifyTurtleCommit(u8aTurtle, keys.publicKey)
33
+ assert.assert(verification2)
34
+
35
+ const verification3 = await verifyCommitU8a(keys.publicKey, u8aTurtle.uint8Array, u8aTurtle.parent.uint8Array)
36
+ assert.assert(verification3)
37
+ })
38
+ })
@@ -0,0 +1,147 @@
1
+ import { Recaller } from '../utils/Recaller.js'
2
+ import { squashTurtle, U8aTurtle } from './U8aTurtle.js'
3
+ import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
4
+ import { combineUint8ArrayLikes } from '../utils/combineUint8ArrayLikes.js'
5
+ import { logInfo, logWarn } from '../utils/logger.js'
6
+ import { JSON_FILE, pathToType, TEXT_FILE } from '../utils/fileTransformer.js'
7
+
8
+ /**
9
+ * @typedef {import('./codecs/CodecType.js').CodecOptions} CodecOptions
10
+ */
11
+
12
+ const _encodeUint8Array = uint8Array => {
13
+ const encodedLength = new Uint32Array([uint8Array.length])
14
+ return combineUint8ArrayLikes([encodedLength, uint8Array])
15
+ }
16
+
17
+ export class TurtleBranch {
18
+ /** @type {U8aTurtle} */
19
+ #u8aTurtle
20
+ #resolveNextUint8Array
21
+ #setNextUint8Array = uint8Array => {
22
+ const prevResolve = this.#resolveNextUint8Array
23
+ this.nextUint8Array = new Promise(resolve => { this.#resolveNextUint8Array = resolve })
24
+ prevResolve?.(uint8Array)
25
+ }
26
+
27
+ /**
28
+ * @param {string} name
29
+ * @param {Recaller} recaller
30
+ * @param {U8aTurtle} u8aTurtle
31
+ */
32
+ constructor (name, recaller = new Recaller(name), u8aTurtle) {
33
+ if (!name) throw new Error('please name your branches')
34
+ this.name = name
35
+ this.recaller = recaller
36
+ this.#u8aTurtle = u8aTurtle
37
+ this.nextUint8Array = new Promise(resolve => { this.#resolveNextUint8Array = resolve })
38
+ }
39
+
40
+ /** @type {U8aTurtle} */
41
+ get u8aTurtle () {
42
+ this.recaller.reportKeyAccess(this, 'u8aTurtle', 'get', this.name)
43
+ return this.#u8aTurtle
44
+ }
45
+
46
+ set u8aTurtle (u8aTurtle) {
47
+ if (u8aTurtle === this.#u8aTurtle) return
48
+ if (u8aTurtle !== undefined) {
49
+ if (u8aTurtle.hasAncestor(this.#u8aTurtle)) {
50
+ const uint8Arrays = u8aTurtle.exportUint8Arrays((this.index ?? -1) + 1)
51
+ uint8Arrays.forEach(uint8Array => {
52
+ this.#setNextUint8Array(uint8Array)
53
+ })
54
+ } else {
55
+ logWarn(() => console.warn(`TurtleBranch, ${this.name}.u8aTurtle set to non-descendant (generators are broken now)`))
56
+ }
57
+ }
58
+ this.recaller.reportKeyMutation(this, 'u8aTurtle', 'set', this.name)
59
+ this.#u8aTurtle = u8aTurtle
60
+ }
61
+
62
+ append (uint8Array) {
63
+ if (!uint8Array?.length) {
64
+ throw new Error('bad Uint8Array')
65
+ }
66
+ this.u8aTurtle = new U8aTurtle(uint8Array, this.u8aTurtle)
67
+ return this.length - 1
68
+ }
69
+
70
+ async * u8aTurtleGenerator () {
71
+ let lastIndex = -1
72
+ while (true) {
73
+ while (lastIndex < this.index) {
74
+ ++lastIndex
75
+ yield this.u8aTurtle.getAncestorByIndex(lastIndex)
76
+ }
77
+ await this.nextUint8Array
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @returns {ReadableStream}
83
+ */
84
+ makeReadableStream () {
85
+ let _controller
86
+ const turtleBranch = this
87
+ return new ReadableStream({
88
+ async start (controller) {
89
+ _controller = controller
90
+ for await (const u8aTurtle of turtleBranch.u8aTurtleGenerator()) {
91
+ _controller.enqueue(_encodeUint8Array(u8aTurtle.uint8Array))
92
+ }
93
+ },
94
+ cancel (reason) {
95
+ logInfo(() => console.log('stream cancelled', { reason }))
96
+ },
97
+ type: 'bytes'
98
+ })
99
+ }
100
+
101
+ /**
102
+ * @returns {WritableStream}
103
+ */
104
+ makeWritableStream () {
105
+ let u8aProgress = new Uint8Array()
106
+ let u8aLength
107
+ const turtleBranch = this
108
+ return new WritableStream({
109
+ write (chunk) {
110
+ u8aProgress = combineUint8Arrays([u8aProgress, chunk])
111
+ while (u8aProgress.length > 4 && u8aProgress.length > (u8aLength = new Uint32Array(u8aProgress.slice(0, 4).buffer)[0])) {
112
+ const uint8Array = u8aProgress.slice(4, u8aLength + 4)
113
+ turtleBranch.append(uint8Array)
114
+ u8aProgress = u8aProgress.slice(u8aLength + 4)
115
+ }
116
+ }
117
+ })
118
+ }
119
+
120
+ get length () { return this.u8aTurtle?.length ?? 0 }
121
+ get index () { return this.u8aTurtle?.index ?? -1 }
122
+ getByte (address) { return this.u8aTurtle?.getAncestorByAddress?.(address)?.getByte?.(address) }
123
+ slice (start, end) { return this.u8aTurtle?.getAncestorByAddress?.(start)?.slice?.(start, end) }
124
+ squash (downToIndex) {
125
+ if (this.index === downToIndex) return
126
+ this.#u8aTurtle = squashTurtle(this.u8aTurtle, downToIndex)
127
+ this.recaller.reportKeyMutation(this, 'u8aTurtle', 'squash', this.name)
128
+ }
129
+
130
+ /**
131
+ * @param {[optional_address:number, ...path:Array.<string>, optional_options:CodecOptions]} path
132
+ * @returns {any}
133
+ */
134
+ lookup (...path) { return this.u8aTurtle?.lookup?.(...path) }
135
+
136
+ lookupFile (filename, asStored = false, address) {
137
+ const type = pathToType(filename)
138
+ const storedContent = address ? this.lookup(address) : this.lookup('document', 'value', filename)
139
+ if (asStored || !storedContent) return storedContent
140
+ if (storedContent?.symlink) return storedContent
141
+ if (storedContent instanceof Uint8Array) return storedContent
142
+ if (typeof storedContent === 'string') return storedContent
143
+ if (type === JSON_FILE) return JSON.stringify(storedContent, null, 2)
144
+ if (type === TEXT_FILE) return storedContent.join('\n')
145
+ return undefined
146
+ }
147
+ }
@@ -0,0 +1,89 @@
1
+ import { handleNextTick, tics } from '../utils/nextTick.js'
2
+ import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
3
+ import { TurtleBranch } from './TurtleBranch.js'
4
+
5
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
6
+ suite.it('passes through U8aTurtle changes', ({ assert }) => {
7
+ const branch = new TurtleBranch('branch')
8
+ const indices = []
9
+ branch.recaller.watch('indices', () => {
10
+ indices.push(branch.index)
11
+ })
12
+ assert.equal(indices, [-1])
13
+ branch.append(new Uint8Array([0, 1, 2]))
14
+ handleNextTick()
15
+ assert.equal(indices, [-1, 0])
16
+ branch.append(new Uint8Array([3, 4, 5]))
17
+ handleNextTick()
18
+ assert.equal(indices, [-1, 0, 1])
19
+ branch.append(new Uint8Array([6, 7, 8]))
20
+ handleNextTick()
21
+ assert.equal(indices, [-1, 0, 1, 2])
22
+ branch.squash(1)
23
+ handleNextTick()
24
+ assert.equal(indices, [-1, 0, 1, 2, 1])
25
+ assert.equal(branch.u8aTurtle.uint8Array, new Uint8Array([3, 4, 5, 6, 7, 8]))
26
+ })
27
+ suite.it('streams append', async ({ assert }) => {
28
+ const primary = new TurtleBranch('primary')
29
+ const secondary = new TurtleBranch('secondary')
30
+ const readableStream = primary.makeReadableStream()
31
+ const writableStream = secondary.makeWritableStream()
32
+ readableStream.pipeTo(writableStream)
33
+ primary.append(new Uint8Array([1, 2, 3]))
34
+ await new Promise(resolve => setTimeout(resolve))
35
+ assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [new Uint8Array([1, 2, 3])])
36
+ })
37
+ suite.it('streams set u8aTurtle', async ({ assert }) => {
38
+ const primary = new TurtleBranch('primary')
39
+ const secondary = new TurtleBranch('secondary')
40
+ const readableStream = primary.makeReadableStream()
41
+ const writableStream = secondary.makeWritableStream()
42
+ readableStream.pipeTo(writableStream)
43
+ primary.append(new Uint8Array([1, 2, 3]))
44
+ await new Promise(resolve => setTimeout(resolve))
45
+ assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [new Uint8Array([1, 2, 3])])
46
+
47
+ const primaryBranched = new TurtleBranch('branched', undefined, primary.u8aTurtle)
48
+ primaryBranched.append(new Uint8Array([4, 5, 6]))
49
+ primaryBranched.append(new Uint8Array([7, 8, 9]))
50
+ primary.u8aTurtle = primaryBranched.u8aTurtle
51
+ await new Promise(resolve => setTimeout(resolve))
52
+ assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [
53
+ new Uint8Array([1, 2, 3]),
54
+ new Uint8Array([4, 5, 6]),
55
+ new Uint8Array([7, 8, 9])
56
+ ])
57
+ })
58
+ suite.it('streams established U8aTurtles', async ({ assert }) => {
59
+ const primary = new TurtleBranch('primary')
60
+ primary.append(new Uint8Array([1, 2, 3]))
61
+ primary.append(new Uint8Array([4, 5, 6]))
62
+ primary.append(new Uint8Array([7, 8, 9]))
63
+ const secondary = new TurtleBranch('secondary')
64
+ primary.makeReadableStream().pipeTo(secondary.makeWritableStream())
65
+ await tics(1)
66
+ assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [
67
+ new Uint8Array([1, 2, 3]),
68
+ new Uint8Array([4, 5, 6]),
69
+ new Uint8Array([7, 8, 9])
70
+ ])
71
+ })
72
+ suite.it('async generators', async ({ assert }) => {
73
+ const branch = new TurtleBranch('branch')
74
+ branch.append(new Uint8Array([1]))
75
+ branch.append(new Uint8Array([2]))
76
+ const uint8Arrays = []
77
+ ;(async () => {
78
+ for await (const u8aTurtle of branch.u8aTurtleGenerator()) {
79
+ uint8Arrays.push(u8aTurtle.uint8Array)
80
+ }
81
+ })()
82
+ await tics(1)
83
+ assert.equal(uint8Arrays, [new Uint8Array([1]), new Uint8Array([2])])
84
+ branch.append(new Uint8Array([3]))
85
+ branch.append(new Uint8Array([4]))
86
+ await tics(1)
87
+ assert.equal(uint8Arrays, [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3]), new Uint8Array([4])])
88
+ })
89
+ })