-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tunnel server: check and close inactive connections (#227)
- Loading branch information
Roy Razon
authored
Sep 19, 2023
1 parent
6daf2ce
commit 81650b1
Showing
22 changed files
with
589 additions
and
182 deletions.
There are no files selected for viewing
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
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 |
---|---|---|
@@ -1,6 +1,17 @@ | ||
/** @type {import('ts-jest').JestConfigWithTsJest} */ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
preset: 'ts-jest/presets/default-esm', | ||
testEnvironment: 'node', | ||
testMatch: ['!dist/', '**/*.test.ts'], | ||
}; | ||
extensionsToTreatAsEsm: ['.ts'], | ||
transform: { | ||
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` | ||
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` | ||
'^.+\\.tsx?$': [ | ||
'ts-jest', | ||
{ | ||
useESM: true, | ||
}, | ||
], | ||
}, | ||
} |
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,68 @@ | ||
import { EventEmitter } from 'tseep' | ||
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals' | ||
import { TimeoutError } from 'p-timeout' | ||
import { onceWithTimeout } from './events' | ||
|
||
describe('onceWithTimeout', () => { | ||
beforeAll(() => { | ||
jest.useFakeTimers() | ||
}) | ||
afterAll(() => { | ||
jest.useRealTimers() | ||
}) | ||
|
||
let emitter: EventEmitter<{ foo: () => void; error: (err: Error) => void }> | ||
beforeEach(() => { | ||
emitter = new EventEmitter() | ||
}) | ||
|
||
describe('when no timeout occurs', () => { | ||
let p: Promise<void> | ||
beforeEach(() => { | ||
p = onceWithTimeout(emitter, 'foo', { milliseconds: 10 }) | ||
emitter.emit('foo') | ||
}) | ||
it('resolves to undefined', async () => { | ||
await expect(p).resolves.toBeUndefined() | ||
}) | ||
}) | ||
|
||
describe('when an error is emitted', () => { | ||
let p: Promise<void> | ||
const e = new Error('boom') | ||
beforeEach(() => { | ||
p = onceWithTimeout(emitter, 'foo', { milliseconds: 10 }) | ||
emitter.emit('error', e) | ||
}) | ||
it('rejects with the error', async () => { | ||
await expect(p).rejects.toThrow(e) | ||
}) | ||
}) | ||
|
||
describe('when a timeout occurs', () => { | ||
describe('when no fallback is specified', () => { | ||
let p: Promise<void> | ||
beforeEach(() => { | ||
p = onceWithTimeout(emitter, 'foo', { milliseconds: 10 }) | ||
jest.advanceTimersByTime(10) | ||
}) | ||
|
||
it('rejects with a TimeoutError', async () => { | ||
await expect(p).rejects.toThrow(TimeoutError) | ||
await expect(p).rejects.toThrow('timed out after 10ms') | ||
}) | ||
}) | ||
|
||
describe('when a fallback is specified', () => { | ||
let p: Promise<12> | ||
beforeEach(() => { | ||
p = onceWithTimeout(emitter, 'foo', { milliseconds: 10, fallback: async () => 12 as const }) | ||
jest.advanceTimersByTime(10) | ||
}) | ||
|
||
it('resolves with the fallback', async () => { | ||
await expect(p).resolves.toBe(12) | ||
}) | ||
}) | ||
}) | ||
}) |
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,36 @@ | ||
import events from 'events' | ||
import { TimeoutError } from 'p-timeout' | ||
|
||
interface NodeEventTarget { | ||
once(eventName: string | symbol, listener: (...args: unknown[]) => void): this | ||
} | ||
|
||
export async function onceWithTimeout( | ||
target: NodeEventTarget, | ||
event: string | symbol, | ||
opts: { milliseconds: number }, | ||
): Promise<void> | ||
export async function onceWithTimeout <T = unknown>( | ||
target: NodeEventTarget, | ||
event: string | symbol, | ||
opts: { milliseconds: number; fallback: () => T | Promise<T> }, | ||
): Promise<T> | ||
export async function onceWithTimeout <T = unknown>( | ||
target: NodeEventTarget, | ||
event: string | symbol, | ||
{ milliseconds, fallback }: { milliseconds: number; fallback?: () => T | Promise<T> }, | ||
): Promise<T | void> { | ||
const signal = AbortSignal.timeout(milliseconds) | ||
return await events.once(target, event, { signal }).then( | ||
() => undefined, | ||
async e => { | ||
if (!signal.aborted || (e as Error).name !== 'AbortError') { | ||
throw e | ||
} | ||
if (fallback) { | ||
return await fallback() | ||
} | ||
throw new TimeoutError(`timed out after ${milliseconds}ms`) | ||
}, | ||
) | ||
} |
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 @@ | ||
export const idGenerator = () => { | ||
let nextId = 0 | ||
return { | ||
next: () => { | ||
const result = nextId | ||
nextId += 1 | ||
return result | ||
}, | ||
} | ||
} |
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,53 @@ | ||
import { Logger } from 'pino' | ||
import { IEventEmitter, EventEmitter } from 'tseep' | ||
import { nextTick } from 'process' | ||
import { idGenerator } from './id-generator' | ||
|
||
export class KeyAlreadyExistsError<V> extends Error { | ||
constructor(readonly key: string, readonly value: V) { | ||
super(`key already exists: "${key}"`) | ||
} | ||
} | ||
|
||
export type TransactionDescriptor = { readonly txId: number | string } | ||
|
||
type StoreEvents = { | ||
delete: () => void | ||
} | ||
|
||
export type EntryWatcher = { | ||
once: (event: 'delete', listener: () => void) => void | ||
} | ||
|
||
export const inMemoryStore = <V extends {}>({ log }: { log: Logger }) => { | ||
type MapValue = { value: V; watcher: IEventEmitter<StoreEvents>; setTx: TransactionDescriptor } | ||
const map = new Map<string, MapValue>() | ||
const txIdGen = idGenerator() | ||
return { | ||
get: async (key: string) => { | ||
const entry = map.get(key) | ||
return entry === undefined ? undefined : { value: entry.value, watcher: entry.watcher } | ||
}, | ||
set: async (key: string, value: V) => { | ||
const existing = map.get(key) | ||
if (existing !== undefined) { | ||
throw new KeyAlreadyExistsError<V>(key, existing.value) | ||
} | ||
const tx: TransactionDescriptor = { txId: txIdGen.next() } | ||
log.debug('setting key %s id %s: %j', key, tx.txId, value) | ||
const watcher = new EventEmitter<StoreEvents>() | ||
map.set(key, { value, watcher, setTx: tx }) | ||
return { tx, watcher: watcher as EntryWatcher } | ||
}, | ||
delete: async (key: string, setTx?: TransactionDescriptor) => { | ||
const value = map.get(key) | ||
if (value && (setTx === undefined || value.setTx.txId === setTx.txId) && map.delete(key)) { | ||
nextTick(() => { value.watcher.emit('delete') }) | ||
return true | ||
} | ||
return false | ||
}, | ||
} | ||
} | ||
|
||
export type Store<V extends {}> = ReturnType<typeof inMemoryStore<V>> |
Oops, something went wrong.