Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/lib/download-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ export class DownloadRequest extends TypedEventTarget<
mapShareUrls,
stream,
}: {
mapShareUrls: string[]
mapShareUrls: readonly [string, ...string[]]
stream: WritableStream<Uint8Array>
remotePublicKey: Uint8Array
keyPair: { publicKey: Uint8Array; secretKey: Uint8Array }
}) {
const downloadUrls = mapShareUrls.map(
(baseUrl) => new URL('download', addTrailingSlash(baseUrl)),
)
) as unknown as [URL, ...URL[]] // grrrr TS
const response = await secretStreamFetch(downloadUrls, {
dispatcher: this.#dispatcher,
})
Expand Down
4 changes: 2 additions & 2 deletions src/lib/map-share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type MapShareOptions = MapInfo & {
* are supported because the server might have multiple network interfaces
* with different IP addresses
*/
baseUrls: string[]
baseUrls: readonly [string, ...string[]]
/** The device ID of the receiver */
receiverDeviceId: string
}
Expand All @@ -36,7 +36,7 @@ export class MapShare extends TypedEventTarget<
shareId,
mapShareUrls: baseUrls.map(
(baseUrl) => new URL(`${shareId}`, addTrailingSlash(baseUrl)).href,
),
) as unknown as readonly [string, ...string[]], // grrrr TS
receiverDeviceId,
mapShareCreatedAt: Date.now(),
status: 'pending',
Expand Down
5 changes: 3 additions & 2 deletions src/lib/secret-stream-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { fetch as secretStreamFetchOrig } from 'secret-stream-http'

import { errors } from './errors.js'
import { isArrayReadonly } from './utils.js'

/**
* A wrapper around secret-stream-http's fetch that tries multiple URLs until one works.
* This is useful when the server has multiple IPs for different network interfaces.
*/
export async function secretStreamFetch(
urls: string | URL | Array<string | URL>,
urls: string | URL | readonly [string | URL, ...Array<string | URL>],
options: Parameters<typeof secretStreamFetchOrig>[1],
) {
if (!Array.isArray(urls)) {
if (!isArrayReadonly(urls)) {
urls = [urls]
}
let response: Response | undefined
Expand Down
8 changes: 8 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,11 @@ function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
export function addTrailingSlash(url: string): string {
return url.endsWith('/') ? url : url + '/'
}

// Typescript's Array.isArray definition does not work as a type guard for
// readonly arrays, so we need to define our own type guard for that
export function isArrayReadonly<T, U>(
value: U | readonly T[],
): value is readonly T[] {
return Array.isArray(value)
}
12 changes: 11 additions & 1 deletion src/routes/map-shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ export function MapSharesRouter(
/**
* Get the base URLs for downloads for all non-internal IPv4 addresses of the machine
*/
function getRemoteBaseUrls(requestUrl: string, remotePort: number): string[] {
function getRemoteBaseUrls(
requestUrl: string,
remotePort: number,
): readonly [string, ...string[]] {
requestUrl = addTrailingSlash(requestUrl)
const interfaces = os.networkInterfaces()
const baseUrls: string[] = []
Expand All @@ -242,5 +245,12 @@ function getRemoteBaseUrls(requestUrl: string, remotePort: number): string[] {
}
}
}
if (!arrayAtLeastOne(baseUrls)) {
throw new Error('No non-internal IPv4 addresses found')
}
return baseUrls
}

function arrayAtLeastOne<T>(arr: readonly T[]): arr is readonly [T, ...T[]] {
return arr.length >= 1
}
12 changes: 7 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ export type DownloadStateUpdate = Extract<
{ status: 'downloading' | 'completed' | 'error' | 'canceled' | 'aborted' }
>

export const MapShareUrls = T.Array(T.String({ format: 'uri' }), {
minItems: 1,
description:
'List of map share URLs (for each network interface of the sharer)',
})
export const MapShareUrls = T.Unsafe<readonly [string, ...string[]]>(
T.Array(T.String({ format: 'uri' }), {
minItems: 1,
description:
'List of map share URLs (for each network interface of the sharer)',
}),
)
export const ShareId = T.String({
minLength: 1,
description: 'The ID of the map share',
Expand Down