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,192 @@
1
+ /**
2
+ * @param {Uint8Array} a
3
+ * @param {Uint8Array} b
4
+ * @returns boolean
5
+ */
6
+ export function deepEqualUint8Arrays (a, b) {
7
+ return a === b || (a.length === b.length && a.every((value, index) => value === b[index]))
8
+ }
9
+
10
+ export const encodeNumberToU8a = (number, minimumLength = 2) => {
11
+ if (typeof number !== 'number') throw new Error('addresses are numbers')
12
+ if (Number.isNaN(number)) throw new Error('addresses are not NaN')
13
+ const asBytes = [...new Uint8Array(new BigUint64Array([BigInt(number)]).buffer)]
14
+ while (asBytes.length > minimumLength && !asBytes[asBytes.length - 1]) asBytes.pop()
15
+ return new Uint8Array(asBytes)
16
+ }
17
+
18
+ export const decodeNumberFromU8a = uint8Array => {
19
+ const asBytes = new Uint8Array(8)
20
+ asBytes.set(uint8Array)
21
+ return Number(new BigUint64Array(asBytes.buffer)[0])
22
+ }
23
+
24
+ export class ValueByUint8Array {
25
+ /** @type {number} */
26
+ value
27
+ /** @type {Uint8Array} */
28
+ #appendix
29
+ /** @type {Object.<string, ValueByUint8Array>} */
30
+ #childrenByBit = {}
31
+ /**
32
+ * @param {Uint8Array} uint8Array
33
+ * @returns any
34
+ */
35
+ get (uint8Array) {
36
+ if (!uint8Array.length) return this.value
37
+ else if (this.#appendix) {
38
+ const { bytes, bits } = ValueByUint8Array.commonPrefix(this.#appendix, uint8Array)
39
+ return this.#childrenByBit[bits]?.get?.(uint8Array.slice(bytes))
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @param {Uint8Array} uint8Array
45
+ * @param {any} value
46
+ */
47
+ set (uint8Array, value) {
48
+ if (!uint8Array.length) {
49
+ if (this.value !== undefined) throw new Error('Existing value')
50
+ this.value = value
51
+ return
52
+ }
53
+ this.#appendix ??= uint8Array
54
+ if (!this.#appendix) this.#appendix = uint8Array
55
+ const { bytes, bits } = ValueByUint8Array.commonPrefix(this.#appendix, uint8Array)
56
+ this.#childrenByBit[bits] ??= new ValueByUint8Array()
57
+ this.#childrenByBit[bits].set(uint8Array.slice(bytes), value)
58
+ }
59
+
60
+ toObj () {
61
+ if (!this.#appendix) return { value: this.value }
62
+ return {
63
+ value: this.value,
64
+ u8a: [...this.#appendix].map(v => `0000000${v.toString(2)}`.slice(-8)).join('.'),
65
+ byBit: Object.fromEntries(Object.entries(this.#childrenByBit).map(([key, value]) => [key, value.toObj()]))
66
+ }
67
+ }
68
+
69
+ toString () {
70
+ return JSON.stringify(this.toObj(), null, 2)
71
+ }
72
+
73
+ /**
74
+ * @param {Uint8Array} a
75
+ * @param {Uint8Array} b
76
+ * @returns {number}
77
+ */
78
+ static commonPrefix (a, b) {
79
+ let bytes = 0
80
+ while (bytes < a.length && bytes < b.length && a[bytes] === b[bytes]) ++bytes
81
+ if (bytes === a.length || bytes === b.length) return { bytes, bits: bytes * 8 }
82
+ const bits = 8 * bytes + Math.clz32(a[bytes] ^ b[bytes]) - 24
83
+ return { bytes, bits }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string} b36
89
+ * @returns {bigint}
90
+ */
91
+ export function b36ToBigInt (b36) {
92
+ return b36.split('').reduce(
93
+ (acc, char) => acc * 36n + BigInt(parseInt(char, 36)),
94
+ 0n
95
+ )
96
+ }
97
+
98
+ /**
99
+ * @param {BigInt} bigInt
100
+ * @returns {Uint8Array}
101
+ */
102
+ export function bigIntToUint8Array (bigInt) {
103
+ let hex = bigInt.toString(16)
104
+ if (hex.length % 2) hex = `0${hex}`
105
+ return new Uint8Array(hex.match(/../g).map(hexByte => parseInt(hexByte, 16)))
106
+ }
107
+
108
+ /**
109
+ * @param {Uint8Array} uint8Array
110
+ * @returns {BigInt}
111
+ */
112
+ export function uint8ArrayToBigInt (uint8Array) {
113
+ return uint8Array.reduce((acc, byte) => acc * 256n + BigInt(byte), 0n)
114
+ }
115
+
116
+ /**
117
+ * @param {string} b36
118
+ * @returns {Uint8Array}
119
+ */
120
+ export function b36ToUint8Array (b36) {
121
+ return bigIntToUint8Array(b36ToBigInt(b36))
122
+ }
123
+
124
+ /**
125
+ * @param {Uint8Array} uint8Array
126
+ * @returns {string}
127
+ */
128
+ export function uint8ArrayToB36 (uint8Array) {
129
+ return uint8ArrayToBigInt(uint8Array).toString(36)
130
+ }
131
+
132
+ /**
133
+ * @param {Object} a
134
+ * @param {Object} b
135
+ */
136
+ export function softAssign (a, b) {
137
+ let changed = false
138
+ const aKeys = Object.keys(a)
139
+ for (const i of aKeys) {
140
+ if (i in b) { // softAssign any overlapping attributes
141
+ if (a[i] !== b[i]) {
142
+ if (!a[i] || !b[i]) {
143
+ a[i] = b[i]
144
+ changed = true
145
+ } if (a[i] instanceof Uint8Array && b[i] instanceof Uint8Array) {
146
+ changed = deepEqualUint8Arrays(a[i], b[i]) && changed
147
+ } else if (
148
+ typeof a[i] === 'object' && typeof b[i] === 'object' &&
149
+ Array.isArray(a[i]) === Array.isArray(b[i])
150
+ ) {
151
+ changed = softAssign(a[i], b[i]) && changed
152
+ } else {
153
+ a[i] = b[i]
154
+ changed = true
155
+ }
156
+ }
157
+ } else { // remove any extra attributes
158
+ delete a[i]
159
+ changed = true
160
+ }
161
+ }
162
+ for (const i in b) { // add missing attributes
163
+ if (!(i in a)) {
164
+ a[i] = b[i]
165
+ changed = true
166
+ }
167
+ }
168
+ if (Array.isArray(b) && a.length !== b.length) {
169
+ a.length = b.length
170
+ changed = true
171
+ }
172
+ return changed
173
+ }
174
+
175
+ export const defaultHostname = 'turtledb.com'
176
+
177
+ export const pathToCpkBaleHost = path => {
178
+ const parts = path.split(/\//)
179
+ const cpk = parts.pop()
180
+ const balename = parts.pop() ?? cpk
181
+ const hostname = parts.pop() ?? defaultHostname
182
+ return [cpk, balename, hostname]
183
+ }
184
+
185
+ export const cpkBaleHostToPath = (cpk, balename = cpk, hostname = defaultHostname) => {
186
+ const parts = [cpk]
187
+ if (balename !== cpk || hostname !== defaultHostname) {
188
+ parts.unshift(balename)
189
+ if (hostname !== defaultHostname) parts.unshift(hostname)
190
+ }
191
+ return parts.join('/')
192
+ }
@@ -0,0 +1,158 @@
1
+ import { handleNextTick } from '../utils/nextTick.js'
2
+ import { proxyWithRecaller } from '../utils/proxyWithRecaller.js'
3
+ import { Recaller } from '../utils/Recaller.js'
4
+ import { b36ToBigInt, b36ToUint8Array, bigIntToUint8Array, cpkBaleHostToPath, decodeNumberFromU8a, encodeNumberToU8a, pathToCpkBaleHost, softAssign, uint8ArrayToB36, uint8ArrayToBigInt, ValueByUint8Array } from './utils.js'
5
+ import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
6
+ import { combineUint8ArrayLikes } from '../utils/combineUint8ArrayLikes.js'
7
+ import { toCombinedVersion } from '../utils/toCombinedVersion.js'
8
+ import { toSubVersions } from '../utils/toSubVersions.js'
9
+ import { toVersionCount } from '../utils/toVersionCount.js'
10
+ import { zabacaba } from '../utils/zabacaba.js'
11
+ import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
12
+
13
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
14
+ suite.it('goes from path to host/bale/cpk and back', ({ assert }) => {
15
+ ; [
16
+ ['turtledb.com/a/a', ['a', 'a', 'turtledb.com'], 'a'],
17
+ ['b.com/a/a', ['a', 'a', 'b.com'], 'b.com/a/a'],
18
+ ['a/a', ['a', 'a', 'turtledb.com'], 'a'],
19
+ ['b/a', ['a', 'b', 'turtledb.com'], 'b/a'],
20
+ ['a', ['a', 'a', 'turtledb.com'], 'a']
21
+ ].forEach(vector => {
22
+ const [path, cpkBaleHost, repath] = vector
23
+ assert.equal(pathToCpkBaleHost(path), cpkBaleHost)
24
+ assert.equal(cpkBaleHostToPath(...cpkBaleHost), repath)
25
+ })
26
+ })
27
+ suite.it('softAssigns completely', ({ assert }) => {
28
+ const obj = { a: 1, b: 2 }
29
+ ;[
30
+ { b: 3, c: [1, 2, 3] },
31
+ { c: [1, 2, 3,,,] }, // eslint-disable-line no-sparse-arrays
32
+ { c: [, 2] } // eslint-disable-line no-sparse-arrays
33
+ ].forEach(vector => {
34
+ softAssign(obj, vector)
35
+ assert.equal(obj, vector)
36
+ })
37
+ })
38
+ suite.it('returns expected values for a zabacaba function', ({ assert }) => {
39
+ const expectedResults = [
40
+ 0,
41
+ 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1, 5,
42
+ 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1, 6,
43
+ 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1, 5,
44
+ 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1, 7,
45
+ 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1, 5
46
+ ]
47
+ for (let i = 0; i < expectedResults.length; ++i) {
48
+ const expected = expectedResults[i]
49
+ const actual = zabacaba(i)
50
+ assert.equal(actual, expected, `zabacaba(${i}) expected: ${expected}, actual: ${actual}`)
51
+ }
52
+ })
53
+ suite.it('combines uint8Arrays', ({ assert }) => {
54
+ assert.equal(new Uint8Array([1, 2, 3, 4, 5, 6]), combineUint8Arrays([
55
+ new Uint8Array([1]),
56
+ new Uint8Array([2, 3]),
57
+ new Uint8Array([]),
58
+ new Uint8Array([4, 5, 6])
59
+ ]))
60
+ })
61
+ suite.it('combines uint8ArrayLikes', ({ assert }) => {
62
+ assert.equal(new Uint8Array([1, 2, 3, 4, 5, 6]), combineUint8ArrayLikes([
63
+ 1,
64
+ new Uint16Array((new Uint8Array([2, 3])).buffer),
65
+ new Uint8Array([]),
66
+ new Uint8Array([4, 5, 6])
67
+ ]))
68
+ })
69
+ suite.it('turns versions to subversion-arrays and back', ({ assert }) => {
70
+ const versionParts = [2, 3, 4]
71
+ const versionCount = toVersionCount(versionParts)
72
+ for (let combinedVersion = 0; combinedVersion < versionCount; ++combinedVersion) {
73
+ const versionArrays = toSubVersions(combinedVersion, versionParts)
74
+ const _combinedVersion = toCombinedVersion(versionArrays, versionParts)
75
+ // console.log(versionArrays, combinedVersion)
76
+ assert.equal(combinedVersion, _combinedVersion, `combinedVersion: ${combinedVersion} to versionArrays${versionArrays} and back ${_combinedVersion}`)
77
+ }
78
+ })
79
+ suite.it('stores and retrieves values by Uint8Array', ({ assert }) => {
80
+ const valueByUint8Array = new ValueByUint8Array()
81
+ const uint8Arrays = [
82
+ new Uint8Array([1, 2, 3]),
83
+ new Uint8Array([1, 2]),
84
+ new Uint8Array([1, 2, 3, 4]),
85
+ new Uint8Array([]),
86
+ new Uint8Array([3]),
87
+ new Uint8Array([3, 2]),
88
+ new Uint8Array([3, 1])
89
+ ]
90
+ for (let i = 0; i < uint8Arrays.length; ++i) {
91
+ const uint8Array = uint8Arrays[i]
92
+ const unsetValue = valueByUint8Array.get(uint8Array)
93
+ assert.equal(unsetValue, undefined, `unset value for ${uint8Array} should be undefined`)
94
+ valueByUint8Array.set(uint8Array, i)
95
+ const setValue = valueByUint8Array.get(uint8Array)
96
+ assert.equal(setValue, i, `set value for ${uint8Array} should be ${i}`)
97
+ }
98
+ for (let i = 0; i < uint8Arrays.length; ++i) {
99
+ const uint8Array = uint8Arrays[i]
100
+ const setValue = valueByUint8Array.get(uint8Array)
101
+ assert.equal(setValue, i, `set value for ${uint8Array} should be ${i}`)
102
+ }
103
+ })
104
+ suite.it('encodes and decodes numbers to and from Uint8Arrays', ({ assert }) => {
105
+ const u8aZero = encodeNumberToU8a(0, 2)
106
+ assert.equal(u8aZero.length, 2, 'zero should have minimum length')
107
+ assert.equal(decodeNumberFromU8a(u8aZero), 0)
108
+ const u8aFFFFFF = encodeNumberToU8a(0xFFFFFF, 2)
109
+ assert.equal(u8aFFFFFF.length, 3)
110
+ assert.equal(decodeNumberFromU8a(u8aFFFFFF), 0xFFFFFF)
111
+ })
112
+ suite.it('translates big ints', ({ assert }) => {
113
+ const bigInt = 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890n
114
+ const b36BigInt = bigInt.toString(36)
115
+ const recoveredBigInt = b36ToBigInt(b36BigInt)
116
+ assert.equal(bigInt, recoveredBigInt)
117
+ const uint8Array = bigIntToUint8Array(bigInt)
118
+ const recoveredFromUint8Array = uint8ArrayToBigInt(uint8Array)
119
+ assert.equal(bigInt, recoveredFromUint8Array)
120
+ const uint8Array2 = b36ToUint8Array(b36BigInt)
121
+ assert.equal(uint8Array, uint8Array2)
122
+ const b36 = uint8ArrayToB36(uint8Array2)
123
+ assert.equal(b36, b36BigInt)
124
+ })
125
+ suite.it('proxyWithRecaller', ({ assert }) => {
126
+ const recaller = new Recaller('proxyWithRecaller')
127
+ const proxiedArray = proxyWithRecaller([1, 2, 3], recaller)
128
+ const arrayIndex1Updates = []
129
+ const arrayLengthUpdates = []
130
+ recaller.watch('proxiedArray index 1', () => {
131
+ arrayIndex1Updates.push(proxiedArray[1])
132
+ })
133
+ recaller.watch('proxiedArray length', () => {
134
+ arrayLengthUpdates.push(proxiedArray.length)
135
+ })
136
+ assert.equal(arrayIndex1Updates, [2])
137
+ assert.equal(arrayLengthUpdates, [3])
138
+ handleNextTick()
139
+ assert.equal(arrayIndex1Updates, [2])
140
+ assert.equal(arrayLengthUpdates, [3])
141
+ proxiedArray[1] = 4
142
+ handleNextTick()
143
+ assert.equal(arrayIndex1Updates, [2, 4])
144
+ assert.equal(arrayLengthUpdates, [3])
145
+ proxiedArray[2] = 4
146
+ handleNextTick()
147
+ assert.equal(arrayIndex1Updates, [2, 4])
148
+ assert.equal(arrayLengthUpdates, [3])
149
+ proxiedArray[5] = 4
150
+ handleNextTick()
151
+ assert.equal(arrayIndex1Updates, [2, 4])
152
+ assert.equal(arrayLengthUpdates, [3, 6])
153
+ proxiedArray.pop()
154
+ handleNextTick()
155
+ assert.equal(arrayIndex1Updates, [2, 4])
156
+ assert.equal(arrayLengthUpdates, [3, 6, 5])
157
+ })
158
+ })
@@ -0,0 +1,115 @@
1
+ import { AS_REFS } from '../turtle/codecs/CodecType.js'
2
+ import { logInfo } from './logger.js'
3
+ import { TestRunnerError } from './TestRunner.js'
4
+ import { ASSERTION, FAILED } from './TestRunnerConstants.js'
5
+
6
+ const TON = {
7
+ replacer: function (_key, value) {
8
+ if (value instanceof Object.getPrototypeOf(Uint8Array)) return { [value.constructor.name]: [...value.values()] }
9
+ if (typeof value === 'bigint') return { BigInt: value.toString() }
10
+ if (value instanceof Date) return { Date: value.toISOString() }
11
+ return value
12
+ },
13
+ stringify: function (value, space = 2) {
14
+ return JSON.stringify(TON.replacer(undefined, value), TON.replacer, space)
15
+ }
16
+ }
17
+
18
+ export class Assert {
19
+ /**
20
+ * @param {import ('./TestRunner.js').TestRunner} runner
21
+ */
22
+ constructor (runner) {
23
+ this.runner = runner
24
+ }
25
+
26
+ async assert (valueToCheck, passMessage = 'truthy', failMessage = `NOT ${passMessage}`, cause) {
27
+ if (valueToCheck) {
28
+ return this.runner.appendChild(passMessage, () => cause, ASSERTION)
29
+ } else {
30
+ return this.runner.appendChild(failMessage, () => { throw new TestRunnerError(failMessage, { cause }) }, ASSERTION)
31
+ }
32
+ }
33
+
34
+ async equal (actual, expected, message, debug = true) {
35
+ const passMessage = `${TON.stringify(actual)} === ${TON.stringify(expected)}`
36
+ if (actual === expected) return this.assert(true, passMessage)
37
+ const expectedAddress = this.runner.upserter.upsert(expected)
38
+ const actualAddress = this.runner.upserter.upsert(actual)
39
+ const failMessage = message ?? `${TON.stringify(actual)} !== ${TON.stringify(expected)}`
40
+ const isEqual = await this.assert(
41
+ expectedAddress === actualAddress,
42
+ passMessage,
43
+ failMessage,
44
+ { runner: this.runner, expectedAddress, actualAddress }
45
+ )
46
+ if (isEqual.runState === FAILED) {
47
+ printDiff(this.runner.upserter, expectedAddress, actualAddress)
48
+ }
49
+ return isEqual
50
+ }
51
+
52
+ async notEqual (actual, expected, message) {
53
+ const passMessage = `${TON.stringify(actual)} !== ${TON.stringify(expected)}`
54
+ const failMessage = message ?? `${TON.stringify(actual)} === ${TON.stringify(expected)}`
55
+ if (expected === actual) this.assert(false, null, failMessage)
56
+ const expectedAddress = this.runner.upserter.upsert(expected)
57
+ const actualAddress = this.runner.upserter.upsert(actual)
58
+ return this.assert(
59
+ expectedAddress !== actualAddress,
60
+ passMessage,
61
+ failMessage,
62
+ { runner: this.runner, expectedAddress, actualAddress }
63
+ )
64
+ }
65
+
66
+ async isAbove (valueToCheck, valueToBeAbove, message) {
67
+ return this.assert(valueToCheck > valueToBeAbove, message)
68
+ }
69
+
70
+ async isBelow (valueToCheck, valueToBeBelow, message) {
71
+ return this.assert(valueToCheck < valueToBeBelow, message)
72
+ }
73
+
74
+ async throw (f, message) {
75
+ let threw = false
76
+ try {
77
+ f()
78
+ } catch (error) {
79
+ threw = true
80
+ }
81
+ return this.assert(threw, message)
82
+ }
83
+ }
84
+
85
+ /**
86
+ *
87
+ * @param {import('../turtle/TurtleBranch.js').TurtleBranch} turtleBranch
88
+ * @param {number} a
89
+ * @param {number} b
90
+ * @param {string} prefix
91
+ */
92
+ export function printDiff (turtleBranch, a, b, indent = '') {
93
+ if (a === undefined || b === undefined) {
94
+ if (a) {
95
+ logInfo(() => console.log(`${indent}${JSON.stringify(turtleBranch.lookup(a))} !== undefined`))
96
+ } else {
97
+ logInfo(() => console.log(`${indent}undefined !== ${JSON.stringify(turtleBranch.lookup(b))}`))
98
+ }
99
+ return
100
+ }
101
+ if (indent.length > 20) return
102
+ const aRefs = turtleBranch.lookup(a, AS_REFS)
103
+ const bRefs = turtleBranch.lookup(b, AS_REFS)
104
+ if (aRefs && bRefs && typeof aRefs === 'object' && typeof bRefs === 'object') {
105
+ const attributes = new Set([...Object.keys(aRefs), ...Object.keys(bRefs)])
106
+ for (const attribute of attributes) {
107
+ if (aRefs[attribute] !== bRefs[attribute]) {
108
+ logInfo(() => console.log(`${indent}${attribute} - a:${a}, b:${b}`))
109
+ printDiff(turtleBranch, aRefs[attribute], bRefs[attribute], `${indent} `)
110
+ }
111
+ }
112
+ } else {
113
+ logInfo(() => console.log(`${indent}${JSON.stringify(turtleBranch.lookup(a))} !== ${JSON.stringify(turtleBranch.lookup(b))}`))
114
+ }
115
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @typedef {Object} NestedSetAsObject
3
+ * @property {Array} set
4
+ * @property {Array.<[any,NestedSetAsObject]>} map
5
+ */
6
+
7
+ export class NestedSet {
8
+ /** @property {Map.<any,NestedSet>} */
9
+ #map
10
+ /** @property {Set} */
11
+ #set
12
+ /**
13
+ * @param {NestedSetAsObject} nestedSetAsObject
14
+ */
15
+ constructor (nestedSetAsObject) {
16
+ if (nestedSetAsObject) {
17
+ this.#set = new Set(nestedSetAsObject.set)
18
+ this.#map = new Map(nestedSetAsObject.map.map(([key, value]) => [key, new NestedSet(value)]))
19
+ } else {
20
+ this.#map = new Map()
21
+ this.#set = new Set()
22
+ }
23
+ }
24
+
25
+ get size () {
26
+ return this.values().length
27
+ }
28
+
29
+ add (root, ...rest) {
30
+ if (rest.length) return this.#map.set(root, this.#map.get(root) ?? new NestedSet()).get(root).add(...rest)
31
+ return this.#set.add(root)
32
+ }
33
+
34
+ get (root, ...rest) {
35
+ if (rest.length) return this.#map.get(root)?.get(...rest)
36
+ return this.#map.get(root)
37
+ }
38
+
39
+ delete (root, ...rest) {
40
+ if (rest.length) {
41
+ this.#map.get(root)?.delete?.(...rest)
42
+ } else {
43
+ this.#set.delete(root)
44
+ this.#map.forEach(nestedSet => nestedSet.delete(root))
45
+ }
46
+ if (!this.#map.get(root)?.size) this.#map.delete(root)
47
+ }
48
+
49
+ deleteBranch (root, ...rest) {
50
+ if (rest.length) return this.#map.get(root)?.deleteBranch(...rest)
51
+ return this.#map.delete(root)
52
+ }
53
+
54
+ values (...path) {
55
+ if (path.length) return this.#map.get(path[0])?.values?.(...path.slice(1)) ?? []
56
+ return [...new Set([...this.#set, ...[...this.#map.keys()].map(key => this.values(key)).flat()])]
57
+ }
58
+
59
+ /**
60
+ * return {NestedSetAsObject}
61
+ */
62
+ get asObject () {
63
+ return {
64
+ set: [...this.#set],
65
+ map: [...this.#map].map(([key, value]) => [key, value.asObject])
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,30 @@
1
+ import { NestedSet } from './NestedSet.js'
2
+ import { globalTestRunner, urlToName } from './TestRunner.js'
3
+
4
+ globalTestRunner.describe(urlToName(import.meta.url), suite => {
5
+ suite.it('adds, gets values, and calculates size', ({ assert }) => {
6
+ const nestedSet = new NestedSet()
7
+ nestedSet.add(1, 2, 3)
8
+ nestedSet.add(1, 2, 3, 3)
9
+ nestedSet.add(1, 4, 3)
10
+ nestedSet.add(1, 4, 4)
11
+ nestedSet.add(2, 2, 5)
12
+ nestedSet.add(3, 2, 3)
13
+
14
+ const nestedSetCopy = new NestedSet(nestedSet.asObject)
15
+ assert.equal(nestedSetCopy, nestedSet)
16
+
17
+ assert.equal(nestedSet.size, 3)
18
+ assert.equal(nestedSet.values(), [3, 4, 5])
19
+ assert.equal(nestedSet.values(1), [3, 4])
20
+ assert.equal(nestedSet.values(1, 2), [3])
21
+ assert.equal(nestedSet.values(1, 2, 3), [3])
22
+ assert.equal(nestedSet.values(2), [5])
23
+
24
+ nestedSet.delete(1, 2, 3)
25
+ assert.equal(nestedSet.values(), [3, 4, 5])
26
+ assert.equal(nestedSet.values(1, 2), [])
27
+ nestedSet.delete(1, 3)
28
+ assert.equal(nestedSet.values(1), [4])
29
+ })
30
+ })
@@ -0,0 +1 @@
1
+ export const OWN_KEYS = Symbol('ownKeys')