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,75 @@
|
|
|
1
|
+
import { createConnection } from 'net'
|
|
2
|
+
import { TurtleBranchMultiplexer } from '../lib/turtle/connections/TurtleBranchMultiplexer.js'
|
|
3
|
+
import { logError, logInfo, logWarn, logDebug } from '../lib/utils/logger.js'
|
|
4
|
+
|
|
5
|
+
export async function originSync (turtleDB, host, port) {
|
|
6
|
+
let t = 100
|
|
7
|
+
let connectionCount = 0
|
|
8
|
+
while (true) {
|
|
9
|
+
logInfo(() => console.log('-- creating new origin connection'))
|
|
10
|
+
logInfo(() => console.time('-- origin connection lifespan'))
|
|
11
|
+
const tbMux = new TurtleBranchMultiplexer(`origin_#${host}`, false, turtleDB)
|
|
12
|
+
for (const publicKey of turtleDB.getPublicKeys()) {
|
|
13
|
+
await tbMux.getTurtleBranchUpdater(tbMux.name, publicKey)
|
|
14
|
+
}
|
|
15
|
+
const tbMuxBinding = async (/** @type {TurtleBranchStatus} */ status) => {
|
|
16
|
+
try {
|
|
17
|
+
// logDebug(() => console.log('tbMuxBinding about to get next', status.publicKey))
|
|
18
|
+
const updater = await tbMux.getTurtleBranchUpdater(tbMux.name, status.publicKey, status.turtleBranch)
|
|
19
|
+
logDebug(() => console.log('updater about to await settle', updater.name, updater.turtleBranch.length))
|
|
20
|
+
await updater.settle
|
|
21
|
+
logDebug(() => console.log('updater settled', updater.turtleBranch.length))
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logError(() => console.error(error))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
turtleDB.bind(tbMuxBinding)
|
|
27
|
+
let _connectionCount
|
|
28
|
+
try {
|
|
29
|
+
const socket = createConnection(port, host)
|
|
30
|
+
socket.on('connect', () => {
|
|
31
|
+
;(async () => {
|
|
32
|
+
try {
|
|
33
|
+
for await (const chunk of tbMux.makeReadableStream()) {
|
|
34
|
+
if (socket.closed) break
|
|
35
|
+
if (socket.write(chunk)) {
|
|
36
|
+
// logDebug(() => console.log('host outgoing data', chunk))
|
|
37
|
+
} else {
|
|
38
|
+
logWarn(() => console.warn('socket failed to write', chunk.length, 'bytes, ending connection', chunk))
|
|
39
|
+
// break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logError(() => console.error(error))
|
|
44
|
+
}
|
|
45
|
+
})()
|
|
46
|
+
t = 100
|
|
47
|
+
_connectionCount = ++connectionCount
|
|
48
|
+
logDebug(() => console.log('-- onopen', { _connectionCount }))
|
|
49
|
+
})
|
|
50
|
+
const streamWriter = tbMux.makeWritableStream().getWriter()
|
|
51
|
+
socket.on('data', buffer => {
|
|
52
|
+
// logDebug(() => console.log('host incoming data', buffer))
|
|
53
|
+
streamWriter.write(buffer)
|
|
54
|
+
})
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
socket.on('close', resolve)
|
|
57
|
+
socket.on('error', reject)
|
|
58
|
+
})
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error?.code === 'ECONNREFUSED') {
|
|
61
|
+
logWarn(() => console.log('-- connection refused'))
|
|
62
|
+
} else {
|
|
63
|
+
logError(() => console.error(error))
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
tbMux.stop()
|
|
68
|
+
turtleDB.unbind(tbMuxBinding)
|
|
69
|
+
logInfo(() => console.timeEnd('-- origin connection lifespan'))
|
|
70
|
+
t = Math.min(t, 2 * 60 * 1000) // 2 minutes max (unjittered)
|
|
71
|
+
t = t * (1 + Math.random()) // exponential backoff and some jitter
|
|
72
|
+
logInfo(() => console.log(`-- waiting ${(t / 1000).toFixed(2)}s`))
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, t))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createServer } from 'net'
|
|
2
|
+
import { TurtleBranchMultiplexer } from '../lib/turtle/connections/TurtleBranchMultiplexer.js'
|
|
3
|
+
import { logError, logWarn, logInfo } from '../lib/utils/logger.js'
|
|
4
|
+
|
|
5
|
+
export async function outletSync (turtleDB, port) {
|
|
6
|
+
let connectionCount = 0
|
|
7
|
+
const server = createServer(async socket => {
|
|
8
|
+
let tbMux
|
|
9
|
+
const _connectionCount = ++connectionCount
|
|
10
|
+
try {
|
|
11
|
+
logInfo(() => console.log('turtle connection', _connectionCount))
|
|
12
|
+
tbMux = new TurtleBranchMultiplexer(`outlet_#${_connectionCount}`, true, turtleDB)
|
|
13
|
+
;(async () => {
|
|
14
|
+
try {
|
|
15
|
+
for await (const chunk of tbMux.makeReadableStream()) {
|
|
16
|
+
if (socket.closed) break
|
|
17
|
+
if (socket.write(chunk)) {
|
|
18
|
+
// logDebug(() => console.log('origin.host outgoing data', chunk))
|
|
19
|
+
} else {
|
|
20
|
+
await new Promise(resolve => socket.once('drain', resolve))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logError(() => console.error(error))
|
|
25
|
+
}
|
|
26
|
+
})()
|
|
27
|
+
const streamWriter = tbMux.makeWritableStream().getWriter()
|
|
28
|
+
socket.on('data', buffer => {
|
|
29
|
+
// logDebug(() => console.log('turtleHost incoming data', buffer))
|
|
30
|
+
streamWriter.write(buffer)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
socket.on('close', resolve)
|
|
35
|
+
socket.on('error', reject)
|
|
36
|
+
})
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code === 'ECONNRESET') {
|
|
39
|
+
logWarn(() => console.warn('ECONNRESET', _connectionCount))
|
|
40
|
+
} else {
|
|
41
|
+
logError(() => console.error(error))
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
tbMux?.stop?.()
|
|
46
|
+
})
|
|
47
|
+
server.listen(port, () => {
|
|
48
|
+
logInfo(() => console.log('opened outlet.port:', port))
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync, readlinkSync, lstatSync, rmdirSync } from 'fs'
|
|
2
|
+
import { dirname, join, relative } from 'path'
|
|
3
|
+
import { logError } from '../lib/utils/logger.js'
|
|
4
|
+
import { deepEqual } from '../lib/utils/deepEqual.js'
|
|
5
|
+
import { Recaller } from '../lib/utils/Recaller.js'
|
|
6
|
+
import { subscribe } from '@parcel/watcher'
|
|
7
|
+
|
|
8
|
+
export const isLinesOfTextExtension = path => path.match(/\.(html|css|js|svg|txt|gitignore|env|node_repl_history)$/)
|
|
9
|
+
export const isJSONExtension = path => path.match(/\.(json)$/)
|
|
10
|
+
|
|
11
|
+
export const encodeTextFile = object => {
|
|
12
|
+
if (!object || typeof object !== 'object') throw new Error('encodeFile requires an object')
|
|
13
|
+
if (Array.isArray(object) && object.every(value => typeof value === 'string')) return object.join('\n')
|
|
14
|
+
return JSON.stringify(object, undefined, 2)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const decodeBufferAsFileObject = (buffer, path) => {
|
|
18
|
+
if (!buffer?.isBuffer?.() && !(buffer instanceof Uint8Array)) return buffer
|
|
19
|
+
const uint8Array = new Uint8Array(buffer)
|
|
20
|
+
const isLinesOfText = isLinesOfTextExtension(path)
|
|
21
|
+
const isJSON = isJSONExtension(path)
|
|
22
|
+
if (!isLinesOfText && !isJSON) return uint8Array
|
|
23
|
+
const decoder = new TextDecoder('utf-8')
|
|
24
|
+
const str = decoder.decode(uint8Array)
|
|
25
|
+
if (isLinesOfText) return str.split(/\r?\n/)
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(str)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logError(() => console.error(`error parsing (file:${path})`))
|
|
30
|
+
return str
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const equalFileObjects = (a, b, path) => {
|
|
35
|
+
const decodedA = decodeBufferAsFileObject(a, path)
|
|
36
|
+
const decodedB = decodeBufferAsFileObject(b, path)
|
|
37
|
+
return deepEqual(decodedA, decodedB)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const UPDATES_HANDLER = Symbol('UPDATES_HANDLER')
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} folder
|
|
44
|
+
* @param {Recaller} recaller
|
|
45
|
+
* @param {function(string, any, any):void)}
|
|
46
|
+
* @returns {Proxy}
|
|
47
|
+
*/
|
|
48
|
+
export const proxyFolder = (folder, recaller = new Recaller(folder), updatesHandler) => {
|
|
49
|
+
const cleanEmptyDir = path => {
|
|
50
|
+
if (['', '.', '/'].includes(path)) return
|
|
51
|
+
const childPath = join(folder, path)
|
|
52
|
+
if (readdirSync(childPath).length) return
|
|
53
|
+
rmdirSync(childPath)
|
|
54
|
+
const parentDirname = dirname(path)
|
|
55
|
+
cleanEmptyDir(parentDirname)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const target = {}
|
|
59
|
+
|
|
60
|
+
const writeFileObject = (path, newFileObject) => {
|
|
61
|
+
const oldFileObject = target[path]
|
|
62
|
+
if (equalFileObjects(oldFileObject, newFileObject, path)) return true
|
|
63
|
+
const childPath = join(folder, path)
|
|
64
|
+
if (newFileObject) {
|
|
65
|
+
const folderPath = dirname(childPath)
|
|
66
|
+
if (folderPath) mkdirSync(folderPath, { recursive: true })
|
|
67
|
+
}
|
|
68
|
+
if (oldFileObject) unlinkSync(childPath)
|
|
69
|
+
if (!newFileObject) {
|
|
70
|
+
// no such thing, remove it
|
|
71
|
+
delete target[path]
|
|
72
|
+
cleanEmptyDir(dirname(path))
|
|
73
|
+
} else {
|
|
74
|
+
target[path] = newFileObject
|
|
75
|
+
if (newFileObject.symlink) {
|
|
76
|
+
symlinkSync(newFileObject.symlink, childPath)
|
|
77
|
+
} else if (newFileObject instanceof Uint8Array) {
|
|
78
|
+
writeFileSync(childPath, Buffer.from(newFileObject))
|
|
79
|
+
} else if (typeof newFileObject === 'object') {
|
|
80
|
+
writeFileSync(childPath, encodeTextFile(newFileObject), { encoding: 'utf8' })
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error('encodeFile requires an object')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const readFileObject = path => {
|
|
89
|
+
const childPath = join(folder, path)
|
|
90
|
+
let stats
|
|
91
|
+
try {
|
|
92
|
+
stats = lstatSync(childPath)
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.code !== 'ENOENT') throw err
|
|
95
|
+
}
|
|
96
|
+
let changed = ''
|
|
97
|
+
if (stats && (stats.isFile() || stats.isSymbolicLink())) {
|
|
98
|
+
let value
|
|
99
|
+
if (stats.isSymbolicLink()) {
|
|
100
|
+
const symlink = readlinkSync(childPath)
|
|
101
|
+
value = { symlink }
|
|
102
|
+
} else {
|
|
103
|
+
value = decodeBufferAsFileObject(readFileSync(childPath), childPath)
|
|
104
|
+
}
|
|
105
|
+
if (!equalFileObjects(value, target[path], path)) {
|
|
106
|
+
target[path] = value
|
|
107
|
+
changed = 'set'
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
if (target[path]) {
|
|
111
|
+
delete target[path]
|
|
112
|
+
changed = 'delete'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (changed) {
|
|
116
|
+
recaller.reportKeyMutation(target, path, changed, `proxyFolder(${folder})`)
|
|
117
|
+
for (let parentDirname = dirname(path); !['', '.', '/'].includes(parentDirname); parentDirname = dirname(parentDirname)) {
|
|
118
|
+
recaller.reportKeyMutation(target, parentDirname, changed, `proxyFolder(${folder})`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return target[path]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const readFileObjects = folder => {
|
|
125
|
+
// don't use recursive option because it follows symlinks
|
|
126
|
+
readdirSync(folder, { withFileTypes: true }).forEach(dirent => {
|
|
127
|
+
if (dirent.name.startsWith('.git/') || dirent.name.startsWith('node_modules/')) return
|
|
128
|
+
if (!dirent.isDirectory()) {
|
|
129
|
+
readFileObject(join(dirent.parentPath, dirent.name))
|
|
130
|
+
} else {
|
|
131
|
+
readFileObjects(join(folder, dirent.name))
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
readFileObjects(folder)
|
|
137
|
+
|
|
138
|
+
const proxy = new Proxy(target, {
|
|
139
|
+
get: (target, name) => {
|
|
140
|
+
recaller.reportKeyAccess(target, name, 'get', `proxyFolder(${folder})`)
|
|
141
|
+
if (name === UPDATES_HANDLER) {
|
|
142
|
+
return updatesHandler
|
|
143
|
+
} else if (target[name]) {
|
|
144
|
+
return target[name]
|
|
145
|
+
} else {
|
|
146
|
+
const matchingEntries = Object.entries(target).filter(([key]) => (key.startsWith(name + '/') || key === name))
|
|
147
|
+
if (!matchingEntries.length) return
|
|
148
|
+
return Object.fromEntries(matchingEntries)
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
set: (target, name, value) => {
|
|
152
|
+
if (name === UPDATES_HANDLER) {
|
|
153
|
+
updatesHandler = value
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
return writeFileObject(name, value)
|
|
157
|
+
},
|
|
158
|
+
deleteProperty: (target, name) => {
|
|
159
|
+
recaller.reportKeyMutation(target, name, 'delete', `proxyFolder(${folder})`)
|
|
160
|
+
for (let parentDirname = dirname(name); !['', '.', '/'].includes(parentDirname); parentDirname = dirname(parentDirname)) {
|
|
161
|
+
recaller.reportKeyMutation(target, parentDirname, 'delete', `proxyFolder(${folder})`)
|
|
162
|
+
}
|
|
163
|
+
return writeFileObject(name)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
let timeout
|
|
168
|
+
const modifiedFiles = new Set()
|
|
169
|
+
const handleFileChange = filename => {
|
|
170
|
+
clearTimeout(timeout)
|
|
171
|
+
modifiedFiles.add(filename)
|
|
172
|
+
timeout = setTimeout(() => {
|
|
173
|
+
const changes = {}
|
|
174
|
+
modifiedFiles.forEach(filename => {
|
|
175
|
+
const oldValue = target[filename]
|
|
176
|
+
const newValue = readFileObject(filename)
|
|
177
|
+
if (!equalFileObjects(oldValue, newValue, filename)) {
|
|
178
|
+
changes[filename] = { oldValue, newValue }
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
updatesHandler?.(changes)
|
|
182
|
+
modifiedFiles.clear()
|
|
183
|
+
}, 500)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
subscribe(folder, (err, events) => {
|
|
187
|
+
if (err) throw err
|
|
188
|
+
events.forEach(({ path }) => {
|
|
189
|
+
const filename = relative(folder, path)
|
|
190
|
+
handleFileChange(filename)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return proxy
|
|
195
|
+
}
|
package/src/s3Sync.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3'
|
|
2
|
+
import { S3Updater } from './S3Updater.js'
|
|
3
|
+
import { TurtleBranchUpdater } from '../lib/turtle/connections/TurtleBranchUpdater.js'
|
|
4
|
+
import { logDebug } from '../lib/utils/logger.js'
|
|
5
|
+
|
|
6
|
+
export async function s3Sync (turtleDB, recaller, endpoint, region, accessKeyId, secretAccessKey, bucket) {
|
|
7
|
+
/** @type {import('@aws-sdk/client-s3').S3ClientConfig} */
|
|
8
|
+
const s3Client = new S3Client({
|
|
9
|
+
requestHandler: { httpsAgent: { maxSockets: 500 } },
|
|
10
|
+
endpoint,
|
|
11
|
+
forcePathStyle: false,
|
|
12
|
+
region,
|
|
13
|
+
credentials: {
|
|
14
|
+
accessKeyId,
|
|
15
|
+
secretAccessKey
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
const tbMuxBinding = async (/** @type {TurtleBranchStatus} */ status) => {
|
|
19
|
+
const turtleBranch = status.turtleBranch
|
|
20
|
+
const name = turtleBranch.name
|
|
21
|
+
const publicKey = status.publicKey
|
|
22
|
+
const s3Updater = new S3Updater(`to_S3_#${name}`, publicKey, recaller, s3Client, bucket)
|
|
23
|
+
const tbUpdater = new TurtleBranchUpdater(`from_S3_#${name}`, turtleBranch, publicKey, false, recaller)
|
|
24
|
+
s3Updater.connect(tbUpdater)
|
|
25
|
+
s3Updater.start()
|
|
26
|
+
tbUpdater.start()
|
|
27
|
+
logDebug(() => console.log('tbUpdater about to await settle', tbUpdater.name))
|
|
28
|
+
if (!status.bindings.has(tbMuxBinding)) await tbUpdater.settle
|
|
29
|
+
logDebug(() => console.log('tbUpdater settled'))
|
|
30
|
+
}
|
|
31
|
+
turtleDB.bind(tbMuxBinding)
|
|
32
|
+
}
|
package/src/webSync.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { manageCert } from './manageCert.js'
|
|
4
|
+
import { createServer as createHttpsServer } from 'https'
|
|
5
|
+
import { createServer as createHttpServer } from 'http'
|
|
6
|
+
import { WebSocketServer } from 'ws'
|
|
7
|
+
import { TurtleBranchMultiplexer } from '../lib/turtle/connections/TurtleBranchMultiplexer.js'
|
|
8
|
+
import { randomUUID } from 'crypto'
|
|
9
|
+
import { logDebug, logInfo, logError } from '../lib/utils/logger.js'
|
|
10
|
+
import { handleRedirect } from '../lib/utils/handleRedirect.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('../lib/turtle/connections/TurtleDB.js').TurtleDB} TurtleDB
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const uuid = randomUUID()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} port
|
|
20
|
+
* @param {string} basePublicKey
|
|
21
|
+
* @param {TurtleDB} turtleDB
|
|
22
|
+
* @param {boolean} https
|
|
23
|
+
* @param {boolean} insecure
|
|
24
|
+
* @param {string} certpath
|
|
25
|
+
* @param {string} [turtleDBFolder='.turtleDB']
|
|
26
|
+
*/
|
|
27
|
+
export async function webSync (port, basePublicKey, turtleDB, https, insecure, certpath, turtleDBFolder = '.turtleDB') {
|
|
28
|
+
const root = join(process.cwd(), basePublicKey)
|
|
29
|
+
const app = express()
|
|
30
|
+
app.use((req, _res, next) => {
|
|
31
|
+
logDebug(() => console.log(req.method, req.url, req.originalUrl))
|
|
32
|
+
next()
|
|
33
|
+
})
|
|
34
|
+
app.use(async (req, res, next) => {
|
|
35
|
+
if (req.url === '/.well-known/appspecific/com.chrome.devtools.json') {
|
|
36
|
+
res.type('application/json')
|
|
37
|
+
res.send(JSON.stringify({ workspace: { uuid, root } }))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
handleRedirect(req.url, +req.params.address, turtleDB, basePublicKey, turtleDBFolder, href => {
|
|
41
|
+
res.redirect(302, href)
|
|
42
|
+
}, (type, body) => {
|
|
43
|
+
if (!body) return next()
|
|
44
|
+
res.type(type)
|
|
45
|
+
res.send(body)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
// app.use(express.static(root))
|
|
49
|
+
|
|
50
|
+
let server
|
|
51
|
+
if (https) {
|
|
52
|
+
if (insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
53
|
+
const fullcertpath = join(process.cwd(), certpath)
|
|
54
|
+
const certOptions = await manageCert(fullcertpath)
|
|
55
|
+
server = createHttpsServer(certOptions, app)
|
|
56
|
+
} else {
|
|
57
|
+
server = createHttpServer(app)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const wss = new WebSocketServer({ server })
|
|
61
|
+
let connectionCount = 0
|
|
62
|
+
|
|
63
|
+
wss.on('connection', async ws => {
|
|
64
|
+
++connectionCount
|
|
65
|
+
const _connectionCount = connectionCount
|
|
66
|
+
logDebug(() => console.log('new connection', _connectionCount))
|
|
67
|
+
// keep alive
|
|
68
|
+
const intervalId = setInterval(() => {
|
|
69
|
+
if (_connectionCount !== connectionCount) clearInterval(intervalId)
|
|
70
|
+
else ws.send(new Uint8Array())
|
|
71
|
+
}, 20000)
|
|
72
|
+
const tbMux = new TurtleBranchMultiplexer(`ws_connection_#${connectionCount}`, true, turtleDB)
|
|
73
|
+
;(async () => {
|
|
74
|
+
for await (const u8aTurtle of tbMux.outgoingBranch.u8aTurtleGenerator()) {
|
|
75
|
+
if (ws.readyState !== ws.OPEN) break
|
|
76
|
+
ws.send(u8aTurtle.uint8Array)
|
|
77
|
+
}
|
|
78
|
+
})()
|
|
79
|
+
ws.on('message', buffer => tbMux.incomingBranch.append(new Uint8Array(buffer)))
|
|
80
|
+
ws.on('close', (code, reason) => logDebug(() => console.log('connection closed', _connectionCount)))
|
|
81
|
+
ws.on('error', error => logError(() => console.error('connection error', { name: error.name, message: error.message })))
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
ws.onclose = resolve
|
|
84
|
+
ws.onerror = reject
|
|
85
|
+
})
|
|
86
|
+
clearInterval(intervalId)
|
|
87
|
+
tbMux.stop()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
server.listen(port, () => {
|
|
91
|
+
logInfo(() => console.log(`local webserver started: ${https ? 'https' : 'http'}://localhost:${port}
|
|
92
|
+
|
|
93
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
94
|
+
!!! FUN-FACT: Self-signed certificates break service-workers !!!
|
|
95
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
96
|
+
(HINT): On MacOS and in a browser started with this command ──╮
|
|
97
|
+
a service-worker can use a self-signed cert. │
|
|
98
|
+
╭─────────────────────────────────────────────────────────────────╯
|
|
99
|
+
╰─▶ open '/Applications/Google Chrome Canary.app' --args --ignore-certificate-errors https://localhost:${port}/`))
|
|
100
|
+
})
|
|
101
|
+
}
|