-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding self-hosted servers (#952)
This adds support for adding self-hosted servers. It adds the following functions: - `project.$member.addServerPeer()` - `project.$sync.connectServers()` - `project.$sync.disconnectServers()` This change doesn't include end-to-end tests for the server. That's deliberate! We can't (easily) add those tests without the server being released, but we can't (easily) release the server without this change. Once this change is released, we can release the server, and then we should be able to add end-to-end tests. See [#886](#886) for more. Co-Authored-By: Gregor MacLennan <[email protected]>
- Loading branch information
1 parent
4a7bf54
commit 4f33bd0
Showing
16 changed files
with
1,081 additions
and
367 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* Create an `Error` with a `code` property. | ||
* | ||
* @example | ||
* const err = new ErrorWithCode('INVALID_DATA', 'data was invalid') | ||
* err.message | ||
* // => 'data was invalid' | ||
* err.code | ||
* // => 'INVALID_DATA' | ||
*/ | ||
export class ErrorWithCode extends Error { | ||
/** | ||
* @param {string} code | ||
* @param {string} message | ||
* @param {object} [options] | ||
* @param {unknown} [options.cause] | ||
*/ | ||
constructor(code, message, options) { | ||
super(message, options) | ||
/** @readonly */ this.code = code | ||
} | ||
} | ||
|
||
/** | ||
* Get the error message from an object if possible. | ||
* Otherwise, stringify the argument. | ||
* | ||
* @param {unknown} maybeError | ||
* @returns {string} | ||
* @example | ||
* try { | ||
* // do something | ||
* } catch (err) { | ||
* console.error(getErrorMessage(err)) | ||
* } | ||
*/ | ||
export function getErrorMessage(maybeError) { | ||
if (maybeError && typeof maybeError === 'object' && 'message' in maybeError) { | ||
try { | ||
const { message } = maybeError | ||
if (typeof message === 'string') return message | ||
} catch (_err) { | ||
// Ignored | ||
} | ||
} | ||
return 'unknown error' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* @template {object} T | ||
* @template {keyof T} K | ||
* @param {T} obj | ||
* @param {K} key | ||
* @returns {undefined | T[K]} | ||
*/ | ||
export function getOwn(obj, key) { | ||
return Object.hasOwn(obj, key) ? obj[key] : undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { isIPv4, isIPv6 } from 'node:net' | ||
|
||
/** | ||
* Is this hostname an IP address? | ||
* | ||
* @param {string} hostname | ||
* @returns {boolean} | ||
* @example | ||
* isHostnameIpAddress('100.64.0.42') | ||
* // => false | ||
* | ||
* isHostnameIpAddress('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]') | ||
* // => true | ||
* | ||
* isHostnameIpAddress('example.com') | ||
* // => false | ||
*/ | ||
export function isHostnameIpAddress(hostname) { | ||
if (isIPv4(hostname)) return true | ||
|
||
if (hostname.startsWith('[') && hostname.endsWith(']')) { | ||
return isIPv6(hostname.slice(1, -1)) | ||
} | ||
|
||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { pipeline } from 'node:stream/promises' | ||
import { Transform } from 'node:stream' | ||
import { createWebSocketStream } from 'ws' | ||
/** @import { WebSocket } from 'ws' */ | ||
/** @import { ReplicationStream } from '../types.js' */ | ||
|
||
/** | ||
* @param {WebSocket} ws | ||
* @param {ReplicationStream} replicationStream | ||
* @returns {Promise<void>} | ||
*/ | ||
export function wsCoreReplicator(ws, replicationStream) { | ||
// This is purely to satisfy typescript at its worst. `pipeline` expects a | ||
// NodeJS ReadWriteStream, but our replicationStream is a streamx Duplex | ||
// stream. The difference is that streamx does not implement the | ||
// `setEncoding`, `unpipe`, `wrap` or `isPaused` methods. The `pipeline` | ||
// function does not depend on any of these methods (I have read through the | ||
// NodeJS source code at cebf21d (v22.9.0) to confirm this), so we can safely | ||
// cast the stream to a NodeJS ReadWriteStream. | ||
const _replicationStream = /** @type {NodeJS.ReadWriteStream} */ ( | ||
/** @type {unknown} */ (replicationStream) | ||
) | ||
return pipeline( | ||
_replicationStream, | ||
wsSafetyTransform(ws), | ||
createWebSocketStream(ws), | ||
_replicationStream | ||
) | ||
} | ||
|
||
/** | ||
* Avoid writing data to a closing or closed websocket, which would result in an | ||
* error. Instead we drop the data and wait for the stream close/end events to | ||
* propagate and close the streams cleanly. | ||
* | ||
* @param {WebSocket} ws | ||
*/ | ||
function wsSafetyTransform(ws) { | ||
return new Transform({ | ||
transform(chunk, encoding, callback) { | ||
if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { | ||
return callback() | ||
} | ||
callback(null, chunk) | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.