From cf0f0a247b0ddfa0bb9cc23b3ff6ce355fb73d9e Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 9 Feb 2026 11:27:10 +0000 Subject: [PATCH 1/4] fix: small type fix --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 425ea8c..305c320 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import assert from 'node:assert' import { once } from 'node:events' import http from 'node:http' -import { type AddressInfo } from 'node:net' +import { type AddressInfo, type Socket } from 'node:net' import { createServerAdapter } from '@whatwg-node/server' import pDefer from 'p-defer' @@ -86,8 +86,8 @@ export function createServer(options: ServerOptions) { }) // Track connections for proper cleanup - const connections = new Set() - const onConnection = (socket: any) => { + const connections = new Set() + const onConnection = (socket: Socket) => { connections.add(socket) socket.once('close', () => { connections.delete(socket) From e92655820b289ff94f88ba666b1518edd3e3a339 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 9 Feb 2026 12:16:08 +0000 Subject: [PATCH 2/4] test: add failing test for server restart with changed ports The deferredListen promise is not reset when close() is called, causing getRemotePort() to return stale port values after server restart. Co-Authored-By: Claude Opus 4.5 --- test/server-restart.test.ts | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/server-restart.test.ts diff --git a/test/server-restart.test.ts b/test/server-restart.test.ts new file mode 100644 index 0000000..0d8f55d --- /dev/null +++ b/test/server-restart.test.ts @@ -0,0 +1,142 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' + +import { Agent as SecretStreamAgent } from 'secret-stream-http' +import { describe, it, expect } from 'vitest' + +import { createServer } from '../src/index.js' + +import { DEMOTILES_Z2, OSM_BRIGHT_Z6 } from './helpers.js' + +describe('Server Restart', () => { + it('should use new ports when restarted with different ports', async (t) => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'map-server-test-')) + const tmpCustomMapPath = path.join(tmpDir, 'custom-map.smp') + await fs.copyFile(OSM_BRIGHT_Z6, tmpCustomMapPath) + + t.onTestFinished(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}) + }) + + const keyPair = SecretStreamAgent.keyPair() + const receiverKeyPair = SecretStreamAgent.keyPair(Buffer.alloc(32, 1)) + const receiverDeviceId = Buffer.from(receiverKeyPair.publicKey).toString( + 'hex', + ) + + const server = createServer({ + defaultOnlineStyleUrl: 'https://demotiles.maplibre.org/style.json', + customMapPath: tmpCustomMapPath, + fallbackMapPath: DEMOTILES_Z2, + keyPair, + }) + + // First listen - use specific ports + const firstResult = await server.listen({ + localPort: 0, + remotePort: 0, + }) + const firstLocalPort = firstResult.localPort + const firstRemotePort = firstResult.remotePort + + // Create a map share to capture the remote port being used + const firstShareResponse = await fetch( + `http://127.0.0.1:${firstLocalPort}/mapShares`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mapId: 'custom', + receiverDeviceId, + }), + }, + ) + expect(firstShareResponse.status).toBe(201) + const firstShare = await firstShareResponse.json() + + // The mapShareUrls should contain the first remote port + expect(firstShare.mapShareUrls).toBeDefined() + expect(firstShare.mapShareUrls.length).toBeGreaterThan(0) + const firstUrl = new URL(firstShare.mapShareUrls[0]) + expect(firstUrl.port).toBe(String(firstRemotePort)) + + // Close the server + await server.close() + + // Listen again - ports will be different (since we use 0) + const secondResult = await server.listen({ + localPort: 0, + remotePort: 0, + }) + const secondLocalPort = secondResult.localPort + const secondRemotePort = secondResult.remotePort + + // Ports should be different (very likely with port 0) + // At minimum, the listen result should reflect the new ports + expect(secondResult.localPort).not.toBe(firstLocalPort) + + // Create another map share - this should use the NEW remote port + const secondShareResponse = await fetch( + `http://127.0.0.1:${secondLocalPort}/mapShares`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mapId: 'custom', + receiverDeviceId, + }), + }, + ) + expect(secondShareResponse.status).toBe(201) + const secondShare = await secondShareResponse.json() + + // The mapShareUrls should contain the SECOND remote port, not the first + expect(secondShare.mapShareUrls).toBeDefined() + expect(secondShare.mapShareUrls.length).toBeGreaterThan(0) + const secondUrl = new URL(secondShare.mapShareUrls[0]) + + // This is the key assertion - the URL should have the new port + expect(secondUrl.port).toBe(String(secondRemotePort)) + expect(secondUrl.port).not.toBe(String(firstRemotePort)) + + // Clean up + await server.close() + }) + + it('should work correctly after multiple restart cycles', async (t) => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'map-server-test-')) + const tmpCustomMapPath = path.join(tmpDir, 'custom-map.smp') + await fs.copyFile(OSM_BRIGHT_Z6, tmpCustomMapPath) + + t.onTestFinished(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}) + }) + + const server = createServer({ + defaultOnlineStyleUrl: 'https://demotiles.maplibre.org/style.json', + customMapPath: tmpCustomMapPath, + fallbackMapPath: DEMOTILES_Z2, + }) + + const ports: number[] = [] + + // Restart 3 times + for (let i = 0; i < 3; i++) { + const result = await server.listen({ localPort: 0, remotePort: 0 }) + ports.push(result.localPort) + + // Verify server is working + const response = await fetch( + `http://127.0.0.1:${result.localPort}/maps/custom/style.json`, + ) + expect(response.status).toBe(200) + + await server.close() + } + + // All ports should be different (extremely likely with port 0) + const uniquePorts = new Set(ports) + expect(uniquePorts.size).toBe(3) + }) +}) From f3a9a87bbdf13d06b01d7487b7e3584077bd4b42 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 9 Feb 2026 12:19:05 +0000 Subject: [PATCH 3/4] fix: reset deferredListen promise on server close Ensures that when the server is restarted with listen() after close(), the getRemotePort function returns the new port values instead of the stale values from the previous listen() call. Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 305c320..56e31ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,7 +56,7 @@ export function createServer(options: ServerOptions) { options.keyPair = Agent.keyPair() } - const deferredListen = pDefer() + let deferredListen = pDefer() const context = new Context({ ...options, keyPair: options.keyPair, @@ -124,6 +124,8 @@ export function createServer(options: ServerOptions) { once(localHttpServer, 'close'), once(secretStreamServer, 'close'), ]) + // Reset deferred listen for potential restart with different ports + deferredListen = pDefer() }, } } From 49a6317e9a52a46fcf65bbee0f80739624e7a9bd Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 9 Feb 2026 17:37:05 +0000 Subject: [PATCH 4/4] fix: increase test timeout on node 18 --- test/server-restart.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/server-restart.test.ts b/test/server-restart.test.ts index 0d8f55d..3b6665e 100644 --- a/test/server-restart.test.ts +++ b/test/server-restart.test.ts @@ -6,7 +6,6 @@ import { Agent as SecretStreamAgent } from 'secret-stream-http' import { describe, it, expect } from 'vitest' import { createServer } from '../src/index.js' - import { DEMOTILES_Z2, OSM_BRIGHT_Z6 } from './helpers.js' describe('Server Restart', () => { @@ -138,5 +137,5 @@ describe('Server Restart', () => { // All ports should be different (extremely likely with port 0) const uniquePorts = new Set(ports) expect(uniquePorts.size).toBe(3) - }) + }, 20_000) // On node 18 restarting can be slow. })