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
package/bin/turtb.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'fs'
4
+ import { start } from 'repl'
5
+ import { Option, program } from 'commander'
6
+ import { question, questionNewPassword } from 'readline-sync'
7
+ import { LOG_LEVELS, logError, logInfo, logSilly, setLogLevel } from '../lib/utils/logger.js'
8
+ import { Signer } from '../lib/turtle/Signer.js'
9
+ import { TurtleDB } from '../lib/turtle/connections/TurtleDB.js'
10
+ import { Recaller } from '../lib/utils/Recaller.js'
11
+ import { OURS, THEIRS, THROW, TurtleDictionary } from '../lib/turtle/TurtleDictionary.js'
12
+ import { Workspace } from '../lib/turtle/Workspace.js'
13
+ import { AS_REFS } from '../lib/turtle/codecs/CodecType.js'
14
+ import { archiveSync } from '../src/archiveSync.js'
15
+ import { fileSync } from '../src/fileSync.js'
16
+ import { s3Sync } from '../src/s3Sync.js'
17
+ import { originSync } from '../src/originSync.js'
18
+ import { outletSync } from '../src/outletSync.js'
19
+ import { webSync } from '../src/webSync.js'
20
+ import { config } from 'dotenv'
21
+
22
+ console.log('\n\n\n!!!!!')
23
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
24
+
25
+ const defaultWebPort = 8080
26
+ const defaultRemoteHost = 'turtledb.com'
27
+ const defaultRemotePort = 1024
28
+ const defaultSyncPort = 1024
29
+
30
+ const makeParserWithOptions = (...options) => value => {
31
+ if (options.length) {
32
+ if (value === true) return options[0]
33
+ if (value === '') return options[0]
34
+ if (value === 'true') return options[0]
35
+ }
36
+ if (value === 'false') return false
37
+ if (!isNaN(+value)) value = +value
38
+ if (options.length <= 1 || options.includes(value)) return value
39
+ throw new Error(`value must be one of: ${options.join(', ')}`)
40
+ }
41
+
42
+ program
43
+ .name('turtledb-com')
44
+ .version(version)
45
+
46
+ .option('--env-file <path>', 'path to .env file')
47
+
48
+ .addOption(
49
+ new Option('--username <string>', 'username to use for Signer')
50
+ .env('TURTLEDB_USERNAME')
51
+ )
52
+ .addOption(
53
+ new Option('--password <string>', 'password to use for Signer')
54
+ .env('TURTLEDB_PASSWORD')
55
+ )
56
+ .addOption(
57
+ new Option('--turtlename <string>', 'name for dataset')
58
+ .env('TURTLEDB_TURTLENAME')
59
+ )
60
+
61
+ .addOption(
62
+ new Option('-f, --fs-mirror [resolve]', 'mirror files locally and handle')
63
+ .default(false)
64
+ .preset(THROW)
65
+ .choices([OURS, THEIRS, THROW, ''])
66
+ .argParser(makeParserWithOptions(THROW, OURS, THEIRS))
67
+ .env('TURTLEDB_FS_MIRROR')
68
+ )
69
+ .addOption(
70
+ new Option('-i, --interactive', 'flag to start repl')
71
+ .default(false)
72
+ .env('TURTLEDB_INTERACTIVE')
73
+ )
74
+ .addOption(
75
+ new Option('-a, --archive', 'save all turtles to files by public key')
76
+ .default(false)
77
+ .env('TURTLEDB_ARCHIVE')
78
+ )
79
+ .addOption(
80
+ new Option('-v, --verbose [level]', 'log data flows')
81
+ .default(0)
82
+ .preset(1)
83
+ .choices(Object.values(LOG_LEVELS).map(v => v.toString()))
84
+ .argParser(makeParserWithOptions(1, ...Object.values(LOG_LEVELS)))
85
+ .env('TURTLEDB_VERBOSE')
86
+ )
87
+ .addOption(
88
+ new Option('--turtleDB-folder <path>', 'path to folder for TurtleDB files')
89
+ .default('.turtleDB')
90
+ .env('TURTLEDB_FOLDER')
91
+ )
92
+
93
+ .addOption(
94
+ new Option('-w, --web-port [number]', 'web port to sync from')
95
+ .default(false)
96
+ .preset(defaultWebPort)
97
+ .argParser(makeParserWithOptions(defaultWebPort))
98
+ .env('TURTLEDB_WEB_PORT')
99
+ .helpGroup('Web Server:')
100
+ )
101
+ .addOption(
102
+ new Option('--web-fallback <string>', 'project public key to use as fallback for web')
103
+ .env('TURTLEDB_WEB_FALLBACK')
104
+ .helpGroup('Web Server:')
105
+ )
106
+ .addOption(
107
+ new Option('--web-certpath <string>', 'path to self-cert for web')
108
+ .env('TURTLEDB_WEB_CERTPATH')
109
+ .helpGroup('Web Server:')
110
+ )
111
+ .addOption(
112
+ new Option('--web-insecure', '(local dev) allow unauthorized for web')
113
+ .env('TURTLEDB_WEB_INSECURE')
114
+ .helpGroup('Web Server:')
115
+ )
116
+
117
+ .addOption(
118
+ new Option('--remote-host [string]', 'remote host to sync to')
119
+ .default(false)
120
+ .preset(defaultRemoteHost)
121
+ .argParser(makeParserWithOptions(defaultRemoteHost))
122
+ .env('TURTLEDB_REMOTE_HOST')
123
+ .helpGroup('TurtleDB Syncing:')
124
+ )
125
+ .addOption(
126
+ new Option('-r, --remote-port [number]', 'remote port to sync to')
127
+ .default(false)
128
+ .preset(defaultRemotePort)
129
+ .argParser(makeParserWithOptions(defaultRemotePort))
130
+ .env('TURTLEDB_REMOTE_PORT')
131
+ .helpGroup('TurtleDB Syncing:')
132
+ )
133
+
134
+ .addOption(
135
+ new Option('-p, --sync-port [number]', 'local port to sync from')
136
+ .default(false)
137
+ .preset(defaultSyncPort)
138
+ .argParser(makeParserWithOptions(defaultSyncPort))
139
+ .env('TURTLEDB_PORT')
140
+ .helpGroup('TurtleDB Syncing:')
141
+ )
142
+
143
+ .addOption(
144
+ new Option('--s3-end-point <string>', 'endpoint for s3 (like "https://sfo3.digitaloceanspaces.com")')
145
+ .default(false)
146
+ .argParser(makeParserWithOptions())
147
+ .env('TURTLEDB_S3_END_POINT')
148
+ .helpGroup('S3-like Service Syncing:')
149
+ )
150
+ .addOption(
151
+ new Option('--s3-region <string>', 'region for s3 (like "sfo3")')
152
+ .env('TURTLEDB_S3_REGION')
153
+ .helpGroup('S3-like Service Syncing:')
154
+ )
155
+ .addOption(
156
+ new Option('--s3-access-key-id <string>', 'accessKeyId for s3')
157
+ .env('TURTLEDB_S3_ACCESS_KEY_ID')
158
+ .helpGroup('S3-like Service Syncing:')
159
+ )
160
+ .addOption(
161
+ new Option('--s3-secret-access-key <string>', 'secretAccessKey for s3')
162
+ .env('TURTLEDB_S3_SECRET_ACCESS_KEY')
163
+ .helpGroup('S3-like Service Syncing:')
164
+ )
165
+ .addOption(
166
+ new Option('--s3-bucket <string>', 'bucket for s3')
167
+ .env('TURTLEDB_S3_BUCKET')
168
+ .helpGroup('S3-like Service Syncing:')
169
+ )
170
+
171
+ .parse()
172
+
173
+ const options = program.opts()
174
+ if (options.envFile) {
175
+ config({ path: options.envFile })
176
+ program.parse() // re-parse with new env vars
177
+ Object.assign(options, program.opts()) // update options with new env vars
178
+ }
179
+ setLogLevel(options.verbose)
180
+ let username = options.username
181
+ let turtlename = options.turtlename
182
+ let signer
183
+ if (options.fsMirror !== false) {
184
+ username ||= question('Username: ')
185
+ turtlename ||= question('Turtlename: ')
186
+ signer = new Signer(username, options.password || questionNewPassword('Password [ATTENTION!: Backspace won\'t work here]: ', { min: 4, max: 999 }))
187
+ } else if (username && turtlename && options.password) {
188
+ signer = new Signer(username, options.password)
189
+ }
190
+ const publicKey = (await signer.makeKeysFor(turtlename)).publicKey
191
+ logInfo(() => console.log({ username, turtlename, publicKey }))
192
+
193
+ logSilly(() => console.log({ options }))
194
+ // console.log({ options })
195
+ // process.exit(0)
196
+
197
+ const recaller = new Recaller('turtledb-com')
198
+ const turtleDB = new TurtleDB('turtledb-com', recaller)
199
+
200
+ if (options.syncPort !== false) {
201
+ const syncPort = +options.syncPort || defaultSyncPort
202
+ logInfo(() => console.log(`listening for local connections on port ${syncPort}`))
203
+ outletSync(turtleDB, syncPort)
204
+ }
205
+
206
+ if (options.remoteHost !== false || options.remotePort !== false) {
207
+ const remoteHost = options.remoteHost || defaultRemoteHost
208
+ const remotePort = +options.remotePort || defaultRemotePort
209
+ logInfo(() => console.log(`connecting to remote at ${remoteHost}:${remotePort}`))
210
+ originSync(turtleDB, remoteHost, remotePort)
211
+ }
212
+
213
+ if (options.s3EndPoint !== false) {
214
+ s3Sync(turtleDB, recaller, options.s3EndPoint, options.s3Region, options.s3AccessKeyId, options.s3SecretAccessKey, options.s3Bucket)
215
+ }
216
+
217
+ if (options.archive) {
218
+ const archivePath = `${options.turtleDBFolder}/archive`
219
+ logInfo(() => console.log(`archiving to ${archivePath}`))
220
+ archiveSync(turtleDB, recaller, archivePath)
221
+ }
222
+
223
+ if (options.fsMirror !== false) {
224
+ if (![OURS, THEIRS, THROW].includes(options.fsMirror)) {
225
+ logError(() => console.error(`fs-mirror resolve option must be "${OURS}", "${THEIRS}" or "${THROW}" (you provided: "${options.fsMirror}")`))
226
+ process.exit(1)
227
+ }
228
+ logInfo(() => console.log('mirroring to file system'))
229
+ fileSync(turtlename, turtleDB, signer, undefined, options.fsMirror, options.turtleDBFolder)
230
+ }
231
+
232
+ if (options.webPort !== false) {
233
+ const webPort = +options.webPort
234
+ const insecure = !!options.webInsecure
235
+ const https = insecure || !!options.webCertpath
236
+ const certpath = options.webCertpath || `${options.turtleDBFolder}/dev/cert.json`
237
+ logInfo(() => console.log(`listening for web connections on port ${webPort} (https: ${https}, insecure: ${insecure}, certpath: ${certpath})`))
238
+ webSync(webPort, publicKey, turtleDB, https, insecure, certpath, options.turtleDBFolder)
239
+ }
240
+
241
+ if (options.interactive) {
242
+ global.username = username
243
+ global.turtlename = turtlename
244
+ global.signer = signer
245
+ global.publicKey = publicKey
246
+ global.recaller = recaller
247
+ global.turtleDB = turtleDB
248
+ global.workspace = await turtleDB.makeWorkspace(signer, turtlename)
249
+ global.TurtleDictionary = TurtleDictionary
250
+ global.Signer = Signer
251
+ global.Workspace = Workspace
252
+ global.setLogLevel = setLogLevel
253
+ global.AS_REFS = AS_REFS
254
+ const replServer = start({ breakEvalOnSigint: true })
255
+ replServer.setupHistory('.node_repl_history', err => {
256
+ if (err) logError(() => console.error(err))
257
+ })
258
+ replServer.on('exit', process.exit)
259
+ }
@@ -0,0 +1,204 @@
1
+ const _voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
2
+ const END = Symbol('end')
3
+
4
+ function _assertChar (arr, regex) {
5
+ if (!arr[arr.i].match(regex)) {
6
+ throw new Error(`expected ${regex}. got ${arr[arr.i]} at i=${arr.i}`)
7
+ }
8
+ arr.i++
9
+ }
10
+
11
+ function _readValue (arr) {
12
+ if (arr[arr.i].isValue) {
13
+ return arr[arr.i++]
14
+ }
15
+ }
16
+
17
+ function _readEscaped (arr) {
18
+ _assertChar(arr, /&/)
19
+ if (_readIf(arr, 'amp;')) {
20
+ return '&'
21
+ } else if (_readIf(arr, 'apos;')) {
22
+ return '\''
23
+ } else if (_readIf(arr, 'gt;')) {
24
+ return '>'
25
+ } else if (_readIf(arr, 'lt;')) {
26
+ return '<'
27
+ } else if (_readIf(arr, 'quot;')) {
28
+ return '"'
29
+ } else if (_readIf(arr, 'nbsp;')) {
30
+ return ' '
31
+ } else {
32
+ throw new Error('unhandled escape sequence')
33
+ }
34
+ }
35
+
36
+ function _readTo (arr, regex) {
37
+ const ss = []
38
+ while (arr.i < arr.length) {
39
+ const c = arr[arr.i]
40
+ if (c.isValue || c.match(regex)) {
41
+ return ss.join('')
42
+ } else if (c === '&') {
43
+ ss.push(_readEscaped(arr))
44
+ } else {
45
+ ss.push(c)
46
+ arr.i++
47
+ }
48
+ }
49
+ return ss.join('')
50
+ }
51
+
52
+ function _skipWhiteSpace (arr) {
53
+ _readTo(arr, /\S/)
54
+ }
55
+
56
+ function _readIf (arr, str) {
57
+ if (!str.length) {
58
+ str = [str]
59
+ }
60
+ const out = []
61
+ for (let i = 0; i < str.length; i++) {
62
+ const char = arr[arr.i + i]
63
+ if (!char || !char.match || !char.match(str[i])) {
64
+ return false
65
+ }
66
+ out.push(char)
67
+ }
68
+ arr.i += str.length
69
+ return out.join('')
70
+ }
71
+
72
+ function _readValueParts (arr, regex) {
73
+ const out = []
74
+ let ss = []
75
+ while (arr.i < arr.length) {
76
+ const c = arr[arr.i]
77
+ if (c.isValue) {
78
+ if (ss.length) {
79
+ out.push({ type: 'part', value: ss.join('') })
80
+ ss = []
81
+ }
82
+ out.push(c.value)
83
+ arr.i++
84
+ } else if (c.match(regex)) {
85
+ if (ss.length) {
86
+ out.push({ type: 'part', value: ss.join('') })
87
+ }
88
+ return out
89
+ } else if (c === '&') {
90
+ ss.push(_readEscaped(arr))
91
+ } else {
92
+ ss.push(c)
93
+ arr.i++
94
+ }
95
+ }
96
+ }
97
+
98
+ function _decodeAttribute (arr) {
99
+ _skipWhiteSpace(arr)
100
+ const c = arr[arr.i]
101
+ if (c === '/' || c === '>') {
102
+ return END
103
+ }
104
+ let name = _readValue(arr)
105
+ if (name && name.isValue) {
106
+ return name.value
107
+ }
108
+ name = _readTo(arr, /[\s=/>]/)
109
+ if (!name) {
110
+ throw new Error('attribute must have a name (dynamic attributes okay, dynamic names... sorry)')
111
+ }
112
+ _skipWhiteSpace(arr)
113
+ const equalSign = _readIf(arr, '=')
114
+ if (equalSign) {
115
+ _skipWhiteSpace(arr)
116
+ let value = _readValue(arr)
117
+ if (value) {
118
+ value = value.value
119
+ } else {
120
+ const quote = _readIf(arr, /['"]/)
121
+ if (quote) {
122
+ value = _readValueParts(arr, quote)
123
+ _assertChar(arr, quote)
124
+ } else {
125
+ value = _readTo(arr, /[\s=/>]/)
126
+ }
127
+ }
128
+ return { type: 'attribute', name, value }
129
+ } else {
130
+ return { type: 'attribute', name }
131
+ }
132
+ }
133
+
134
+ function _decodeAttributes (arr) {
135
+ const attributes = []
136
+ while (true) {
137
+ const attribute = _decodeAttribute(arr)
138
+ if (attribute !== END) {
139
+ attributes.push(attribute)
140
+ } else {
141
+ return attributes
142
+ }
143
+ }
144
+ }
145
+
146
+ function _decodeTag (arr) {
147
+ _skipWhiteSpace(arr)
148
+ const c = arr[arr.i]
149
+ if (c.isValue) {
150
+ arr.i++
151
+ return c.value
152
+ }
153
+ return _readTo(arr, /[\s/>]/)
154
+ }
155
+
156
+ function _decodeElement (arr) {
157
+ const c = arr[arr.i]
158
+ if (c.isValue) {
159
+ arr.i++
160
+ return c.value
161
+ } else if (c === '<') {
162
+ _assertChar(arr, /</)
163
+ const isClosing = _readIf(arr, '/')
164
+ const tag = _decodeTag(arr)
165
+ const isVoid = _voidElements.has(tag)
166
+ const attributes = _decodeAttributes(arr)
167
+ const isEmpty = _readIf(arr, '/') || isVoid
168
+ _assertChar(arr, />/)
169
+ const children = (isClosing || isEmpty) ? [] : _decodeElements(arr, tag)
170
+ if (isVoid && isClosing) return null
171
+ return { type: 'node', tag, attributes, children, isClosing }
172
+ } else {
173
+ return { type: 'textnode', value: _readTo(arr, /</) }
174
+ }
175
+ }
176
+
177
+ function _decodeElements (arr, closingTag) {
178
+ const nodes = []
179
+ while (arr.i < arr.length) {
180
+ const node = _decodeElement(arr)
181
+ if (node != null) {
182
+ if (node.isClosing) {
183
+ if (closingTag != null) {
184
+ return nodes
185
+ }
186
+ } else {
187
+ delete node.isClosing
188
+ nodes.push(node)
189
+ }
190
+ }
191
+ }
192
+ return [].concat.apply([], nodes)
193
+ }
194
+
195
+ export function h (strings, ...values) {
196
+ const ss = [strings[0].split('')]
197
+ for (let i = 0; i < values.length; i++) {
198
+ ss.push({ value: values[i], isValue: true })
199
+ ss.push(strings[i + 1].split(''))
200
+ }
201
+ const arr = [].concat.apply([], ss)
202
+ arr.i = 0
203
+ return _decodeElements(arr, null)
204
+ }
@@ -0,0 +1,78 @@
1
+ // takes a function that evaluates to true or false and static if and else responses. return function that evaluates condition and returns appropriate response
2
+ export function showIfElse (condition, a, b = []) {
3
+ return () => condition() ? a : b
4
+ }
5
+
6
+ // takes an object and a mapping-function. returns a function that maps the object through the function and returns previously calculated mappings on future calls
7
+ export function mapEntries (object, f) {
8
+ let cache = {}
9
+ return () => {
10
+ const o = (typeof object === 'function' ? object() : object) || {}
11
+ const newCache = {}
12
+ const mappedEntries = Object.entries(o).map(([name, input]) => {
13
+ if (cache[name] && cache[name].input === input) {
14
+ newCache[name] = cache[name]
15
+ } else {
16
+ newCache[name] = {
17
+ input,
18
+ output: f(input, name)
19
+ }
20
+ }
21
+ return newCache[name].output
22
+ })
23
+ cache = newCache
24
+ return mappedEntries
25
+ }
26
+ }
27
+
28
+ // TODO: this only works for unique outputs... fix that
29
+ /*
30
+ export function looselyMapEntries(object, f) {
31
+ const map = new Map()
32
+ return () => {
33
+ const o = (typeof object === 'function' ? object() : object) || {}
34
+ map.forEach(indexedValues => {
35
+ indexedValues.index = 0
36
+ })
37
+ const mappedEntries = Object.entries(o).map(([name, value]) => {
38
+ if (!map.has(value)) {
39
+ map.set(value, { index: 0, values: [] })
40
+ }
41
+ const indexedValues = map.get(value)
42
+ if (indexedValues.values.length <= indexedValues.index) {
43
+ indexedValues.values.push(f(value, name))
44
+ }
45
+ return indexedValues.values[indexedValues.index++]
46
+ })
47
+ // cleanup any unmapped-to values
48
+ map.forEach((indexedValues, key) => {
49
+ if (indexedValues.index) {
50
+ indexedValues.values.splice(indexedValues.index)
51
+ } else {
52
+ map.delete(key)
53
+ }
54
+ })
55
+ return mappedEntries
56
+ }
57
+ }
58
+ */
59
+
60
+ export function mapSwitch (expression, f) {
61
+ const map = new Map()
62
+ return () => {
63
+ const expr = expression()
64
+ if (!map.has(expr)) {
65
+ map.set(expr, f(expr))
66
+ }
67
+ return map.get(expr)
68
+ }
69
+ }
70
+
71
+ export function objToDeclarations (obj = {}) {
72
+ return Object.entries(obj).map(([name, value]) => `${name}: ${value};`).join('')
73
+ }
74
+
75
+ // people just don't like writing 'el => e => {...}'
76
+ export function handle (handler, ...args) {
77
+ return el => e => handler(e, el, ...args)
78
+ }