fake-generation.client.ts 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. /** Test-local programmable Connection generation source. */
  2. import type { ConnectionGenerationSource } from '../src/client/connection.ts'
  3. type StreamItem = { kind: 'end' } | { kind: 'fail'; error: unknown }
  4. interface StreamConnection {
  5. feed(item: StreamItem): void
  6. }
  7. /** Hand-pumped generation source for Connection lifecycle tests. */
  8. export class FakeGenerationSource {
  9. private readonly connections: StreamConnection[] = []
  10. /** When true, the source never reports ready. */
  11. suppressReady = false
  12. /** When true, ready callbacks remain parked until the test releases them. */
  13. holdReady = false
  14. private heldReady: Array<() => void> = []
  15. /** Open one generation. */
  16. readonly source: ConnectionGenerationSource = (signal, ready) => this.open(signal, ready)
  17. /** Release every generation currently parked before readiness. */
  18. releaseReady(): void {
  19. const held = this.heldReady
  20. this.heldReady = []
  21. for (const fire of held) fire()
  22. }
  23. /** End every active generation normally. */
  24. end(): void {
  25. for (const connection of [...this.connections]) connection.feed({ kind: 'end' })
  26. }
  27. /** Fail every active generation. */
  28. fail(error: unknown): void {
  29. for (const connection of [...this.connections]) connection.feed({ kind: 'fail', error })
  30. }
  31. /** Number of currently active generations. */
  32. get activeCount(): number {
  33. return this.connections.length
  34. }
  35. private async open(
  36. signal: AbortSignal,
  37. onReady: (host: { readonly home: string }) => void,
  38. ): Promise<void> {
  39. const inbox: StreamItem[] = []
  40. let wake: (() => void) | null = null
  41. const connection: StreamConnection = {
  42. feed: (item) => {
  43. inbox.push(item)
  44. wake?.()
  45. },
  46. }
  47. this.connections.push(connection)
  48. const ready = (): void => { onReady({ home: '/h' }) }
  49. if (this.holdReady) this.heldReady.push(ready)
  50. else if (!this.suppressReady) ready()
  51. try {
  52. while (!signal.aborted) {
  53. while (inbox.length > 0) {
  54. const item = inbox.shift() as StreamItem
  55. if (item.kind === 'end') return
  56. if (item.kind === 'fail') throw item.error
  57. }
  58. await new Promise<void>((resolve) => {
  59. wake = resolve
  60. signal.addEventListener('abort', () => { resolve() }, { once: true })
  61. })
  62. wake = null
  63. }
  64. } finally {
  65. this.connections.splice(this.connections.indexOf(connection), 1)
  66. }
  67. }
  68. }