orez 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/cli-entry.js +0 -0
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +14 -11
  10. package/dist/index.js.map +1 -1
  11. package/dist/pg-proxy.d.ts.map +1 -1
  12. package/dist/pg-proxy.js +8 -4
  13. package/dist/pg-proxy.js.map +1 -1
  14. package/dist/pglite-manager.d.ts +12 -0
  15. package/dist/pglite-manager.d.ts.map +1 -1
  16. package/dist/pglite-manager.js +81 -0
  17. package/dist/pglite-manager.js.map +1 -1
  18. package/dist/recovery.js +2 -2
  19. package/dist/recovery.js.map +1 -1
  20. package/dist/replication/change-tracker.js +9 -9
  21. package/dist/replication/change-tracker.js.map +1 -1
  22. package/dist/replication/handler.d.ts +12 -0
  23. package/dist/replication/handler.d.ts.map +1 -1
  24. package/dist/replication/handler.js +34 -6
  25. package/dist/replication/handler.js.map +1 -1
  26. package/dist/worker/browser-build-config.d.ts +59 -0
  27. package/dist/worker/browser-build-config.d.ts.map +1 -0
  28. package/dist/worker/browser-build-config.js +101 -0
  29. package/dist/worker/browser-build-config.js.map +1 -0
  30. package/dist/worker/browser-embed.d.ts +58 -0
  31. package/dist/worker/browser-embed.d.ts.map +1 -0
  32. package/dist/worker/browser-embed.js +195 -0
  33. package/dist/worker/browser-embed.js.map +1 -0
  34. package/dist/worker/cf-patches.d.ts +20 -0
  35. package/dist/worker/cf-patches.d.ts.map +1 -0
  36. package/dist/worker/cf-patches.js +94 -0
  37. package/dist/worker/cf-patches.js.map +1 -0
  38. package/dist/worker/index.d.ts +12 -0
  39. package/dist/worker/index.d.ts.map +1 -0
  40. package/dist/worker/index.js +105 -0
  41. package/dist/worker/index.js.map +1 -0
  42. package/dist/worker/shims/fastify.d.ts +80 -0
  43. package/dist/worker/shims/fastify.d.ts.map +1 -0
  44. package/dist/worker/shims/fastify.js +223 -0
  45. package/dist/worker/shims/fastify.js.map +1 -0
  46. package/dist/worker/shims/http-service.d.ts +104 -0
  47. package/dist/worker/shims/http-service.d.ts.map +1 -0
  48. package/dist/worker/shims/http-service.js +198 -0
  49. package/dist/worker/shims/http-service.js.map +1 -0
  50. package/dist/worker/shims/node-stub.d.ts +147 -0
  51. package/dist/worker/shims/node-stub.d.ts.map +1 -0
  52. package/dist/worker/shims/node-stub.js +204 -0
  53. package/dist/worker/shims/node-stub.js.map +1 -0
  54. package/dist/worker/shims/postgres.d.ts +115 -0
  55. package/dist/worker/shims/postgres.d.ts.map +1 -0
  56. package/dist/worker/shims/postgres.js +1181 -0
  57. package/dist/worker/shims/postgres.js.map +1 -0
  58. package/dist/worker/shims/sqlite-browser.d.ts +54 -0
  59. package/dist/worker/shims/sqlite-browser.d.ts.map +1 -0
  60. package/dist/worker/shims/sqlite-browser.js +144 -0
  61. package/dist/worker/shims/sqlite-browser.js.map +1 -0
  62. package/dist/worker/shims/sqlite.d.ts +126 -0
  63. package/dist/worker/shims/sqlite.d.ts.map +1 -0
  64. package/dist/worker/shims/sqlite.js +599 -0
  65. package/dist/worker/shims/sqlite.js.map +1 -0
  66. package/dist/worker/shims/stream-browser.d.ts +9 -0
  67. package/dist/worker/shims/stream-browser.d.ts.map +1 -0
  68. package/dist/worker/shims/stream-browser.js +13 -0
  69. package/dist/worker/shims/stream-browser.js.map +1 -0
  70. package/dist/worker/shims/ws-browser.d.ts +50 -0
  71. package/dist/worker/shims/ws-browser.d.ts.map +1 -0
  72. package/dist/worker/shims/ws-browser.js +105 -0
  73. package/dist/worker/shims/ws-browser.js.map +1 -0
  74. package/dist/worker/shims/ws.d.ts +62 -0
  75. package/dist/worker/shims/ws.d.ts.map +1 -0
  76. package/dist/worker/shims/ws.js +310 -0
  77. package/dist/worker/shims/ws.js.map +1 -0
  78. package/dist/worker/types.d.ts +57 -0
  79. package/dist/worker/types.d.ts.map +1 -0
  80. package/dist/worker/types.js +9 -0
  81. package/dist/worker/types.js.map +1 -0
  82. package/dist/worker/zero-cache-embed-cf.d.ts +63 -0
  83. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -0
  84. package/dist/worker/zero-cache-embed-cf.js +268 -0
  85. package/dist/worker/zero-cache-embed-cf.js.map +1 -0
  86. package/dist/worker/zero-cache-embed.d.ts +66 -0
  87. package/dist/worker/zero-cache-embed.d.ts.map +1 -0
  88. package/dist/worker/zero-cache-embed.js +200 -0
  89. package/dist/worker/zero-cache-embed.js.map +1 -0
  90. package/package.json +62 -3
  91. package/src/cli-entry.ts +0 -0
  92. package/src/cli.ts +8 -1
  93. package/src/config.ts +2 -0
  94. package/src/index.ts +15 -10
  95. package/src/integration/integration.test.ts +1 -1
  96. package/src/integration/restore-live-stress.test.ts +2 -2
  97. package/src/pg-proxy.ts +9 -4
  98. package/src/pglite-manager.ts +111 -0
  99. package/src/recovery.ts +2 -2
  100. package/src/replication/change-tracker.test.ts +1 -1
  101. package/src/replication/change-tracker.ts +9 -9
  102. package/src/replication/handler.test.ts +37 -0
  103. package/src/replication/handler.ts +46 -6
  104. package/src/wasm-sqlite.test.ts +2 -1
  105. package/src/worker/browser-build-config.test.ts +59 -0
  106. package/src/worker/browser-build-config.ts +105 -0
  107. package/src/worker/browser-embed.ts +306 -0
  108. package/src/worker/cf-patches.ts +114 -0
  109. package/src/worker/embed-integration.test.ts +321 -0
  110. package/src/worker/index.ts +138 -0
  111. package/src/worker/shims/fastify.test.ts +255 -0
  112. package/src/worker/shims/fastify.ts +292 -0
  113. package/src/worker/shims/http-service.test.ts +355 -0
  114. package/src/worker/shims/http-service.ts +293 -0
  115. package/src/worker/shims/node-stub.ts +223 -0
  116. package/src/worker/shims/postgres.test.ts +364 -0
  117. package/src/worker/shims/postgres.ts +1434 -0
  118. package/src/worker/shims/sqlite-browser.test.ts +233 -0
  119. package/src/worker/shims/sqlite-browser.ts +178 -0
  120. package/src/worker/shims/sqlite.test.ts +641 -0
  121. package/src/worker/shims/sqlite.ts +731 -0
  122. package/src/worker/shims/ws-browser.test.ts +184 -0
  123. package/src/worker/shims/ws-browser.ts +125 -0
  124. package/src/worker/shims/ws.test.ts +288 -0
  125. package/src/worker/shims/ws.ts +367 -0
  126. package/src/worker/types.ts +75 -0
  127. package/src/worker/worker-integration.test.ts +223 -0
  128. package/src/worker/worker.test.ts +136 -0
  129. package/src/worker/zero-cache-embed-cf.ts +367 -0
  130. package/src/worker/zero-cache-embed.ts +277 -0
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+
3
+ import { messagePortToWs, browserWsToWs, createInProcessPair } from './ws-browser.js'
4
+
5
+ describe('messagePortToWs', () => {
6
+ // create a mock MessagePort pair
7
+ function createMockPorts() {
8
+ type Handler = (event: MessageEvent) => void
9
+ let handler1: Handler | null = null
10
+ let handler2: Handler | null = null
11
+ let closed1 = false
12
+ let closed2 = false
13
+
14
+ const port1 = {
15
+ set onmessage(h: Handler | null) {
16
+ handler1 = h
17
+ },
18
+ get onmessage() {
19
+ return handler1
20
+ },
21
+ onmessageerror: null as Handler | null,
22
+ postMessage(data: unknown) {
23
+ if (closed2) return
24
+ // deliver to port2's handler
25
+ handler2?.({ data } as MessageEvent)
26
+ },
27
+ close() {
28
+ closed1 = true
29
+ },
30
+ }
31
+
32
+ const port2 = {
33
+ set onmessage(h: Handler | null) {
34
+ handler2 = h
35
+ },
36
+ get onmessage() {
37
+ return handler2
38
+ },
39
+ onmessageerror: null as Handler | null,
40
+ postMessage(data: unknown) {
41
+ if (closed1) return
42
+ handler1?.({ data } as MessageEvent)
43
+ },
44
+ close() {
45
+ closed2 = true
46
+ },
47
+ }
48
+
49
+ return [port1, port2] as [typeof port1, typeof port2]
50
+ }
51
+
52
+ it('wraps a MessagePort as WebSocket-like', () => {
53
+ const [port1] = createMockPorts()
54
+ const ws = messagePortToWs(port1 as any)
55
+ expect(ws.readyState).toBe(1) // OPEN
56
+ expect(ws.send).toBeInstanceOf(Function)
57
+ expect(ws.close).toBeInstanceOf(Function)
58
+ })
59
+
60
+ it('send() calls port.postMessage', () => {
61
+ const [port1] = createMockPorts()
62
+ const spy = vi.spyOn(port1, 'postMessage')
63
+ const ws = messagePortToWs(port1 as any)
64
+
65
+ ws.send('hello')
66
+ expect(spy).toHaveBeenCalledWith('hello')
67
+ })
68
+
69
+ it('forwards port messages as ws message events', () => {
70
+ const [port1, port2] = createMockPorts()
71
+ const ws1 = messagePortToWs(port1 as any)
72
+
73
+ const handler = vi.fn()
74
+ ws1.addEventListener('message', handler)
75
+
76
+ // send from port2 → should arrive at ws1
77
+ port2.postMessage('world')
78
+ expect(handler).toHaveBeenCalledWith({ data: 'world' })
79
+ })
80
+
81
+ it('close() sets readyState to CLOSED and fires event', () => {
82
+ const [port1] = createMockPorts()
83
+ const ws = messagePortToWs(port1 as any)
84
+
85
+ const closeHandler = vi.fn()
86
+ ws.addEventListener('close', closeHandler)
87
+
88
+ ws.close(1000)
89
+ expect(ws.readyState).toBe(3) // CLOSED
90
+ expect(closeHandler).toHaveBeenCalledWith(
91
+ expect.objectContaining({ code: 1000, wasClean: true })
92
+ )
93
+ })
94
+
95
+ it('send() is no-op after close', () => {
96
+ const [port1] = createMockPorts()
97
+ const spy = vi.spyOn(port1, 'postMessage')
98
+ const ws = messagePortToWs(port1 as any)
99
+
100
+ ws.close()
101
+ ws.send('ignored')
102
+ // postMessage should not be called after close
103
+ expect(spy).not.toHaveBeenCalled()
104
+ })
105
+
106
+ it('removeEventListener works', () => {
107
+ const [port1] = createMockPorts()
108
+ const ws = messagePortToWs(port1 as any)
109
+
110
+ const handler = vi.fn()
111
+ ws.addEventListener('message', handler)
112
+ ws.removeEventListener('message', handler)
113
+
114
+ // simulate message — handler should not be called
115
+ ;(port1 as any).onmessage?.({ data: 'test' })
116
+ // the handler was removed from ws listeners but port.onmessage still fires
117
+ // however ws.addEventListener wraps the handler, so the original shouldn't fire
118
+ // ... actually the port.onmessage fires the ws internal handler which checks listeners
119
+ // this is an implementation detail. just verify no error is thrown.
120
+ })
121
+ })
122
+
123
+ describe('browserWsToWs', () => {
124
+ it('wraps a browser WebSocket', () => {
125
+ const mockWs = {
126
+ readyState: 1,
127
+ send: vi.fn(),
128
+ close: vi.fn(),
129
+ addEventListener: vi.fn(),
130
+ removeEventListener: vi.fn(),
131
+ }
132
+
133
+ const ws = browserWsToWs(mockWs as any)
134
+ expect(ws.readyState).toBe(1)
135
+
136
+ ws.send('test')
137
+ expect(mockWs.send).toHaveBeenCalledWith('test')
138
+
139
+ ws.close(1000, 'bye')
140
+ expect(mockWs.close).toHaveBeenCalledWith(1000, 'bye')
141
+ })
142
+ })
143
+
144
+ describe('createInProcessPair', () => {
145
+ it('creates a connected pair', () => {
146
+ const [client, server] = createInProcessPair()
147
+ expect(client.readyState).toBe(1)
148
+ expect(server.readyState).toBe(1)
149
+ })
150
+
151
+ it('messages flow between client and server', async () => {
152
+ const [client, server] = createInProcessPair()
153
+
154
+ // MessageChannel delivers messages asynchronously in Node.js
155
+ const serverReceived = new Promise<string>((resolve) => {
156
+ server.addEventListener('message', (event: any) => {
157
+ resolve(event.data)
158
+ })
159
+ })
160
+
161
+ client.send('hello from client')
162
+ expect(await serverReceived).toBe('hello from client')
163
+
164
+ const clientReceived = new Promise<string>((resolve) => {
165
+ client.addEventListener('message', (event: any) => {
166
+ resolve(event.data)
167
+ })
168
+ })
169
+
170
+ server.send('hello from server')
171
+ expect(await clientReceived).toBe('hello from server')
172
+ })
173
+
174
+ it('close sets readyState', () => {
175
+ const [client] = createInProcessPair()
176
+
177
+ const closeHandler = vi.fn()
178
+ client.addEventListener('close', closeHandler)
179
+
180
+ client.close(1000)
181
+ expect(client.readyState).toBe(3)
182
+ expect(closeHandler).toHaveBeenCalled()
183
+ })
184
+ })
@@ -0,0 +1,125 @@
1
+ /**
2
+ * browser WebSocket/MessagePort adapter for the ws shim.
3
+ *
4
+ * wraps a browser WebSocket or MessagePort to match the interface
5
+ * that the existing ws shim (orez/worker/shims/ws) expects. this lets
6
+ * zero-cache handle WebSocket connections in browser Web Workers.
7
+ *
8
+ * usage:
9
+ * import { messagePortToWs, browserWsToWs } from 'orez/worker/shims/ws-browser'
10
+ *
11
+ * // from a MessageChannel (worker ↔ main thread)
12
+ * const channel = new MessageChannel()
13
+ * const serverWs = messagePortToWs(channel.port2)
14
+ *
15
+ * // from a browser WebSocket
16
+ * const ws = new WebSocket('ws://...')
17
+ * const shimWs = browserWsToWs(ws)
18
+ */
19
+
20
+ // the interface that the ws shim expects (same as CF WebSocket)
21
+ interface WsCompatible {
22
+ readyState: number
23
+ send(data: string | ArrayBuffer | ArrayBufferView): void
24
+ close(code?: number, reason?: string): void
25
+ addEventListener(type: string, handler: (event: any) => void): void
26
+ removeEventListener(type: string, handler: (event: any) => void): void
27
+ }
28
+
29
+ /**
30
+ * wrap a MessagePort to look like a WebSocket.
31
+ *
32
+ * MessagePort uses postMessage/onmessage, while the ws shim expects
33
+ * send/addEventListener('message'). this bridges the two.
34
+ */
35
+ export function messagePortToWs(port: MessagePort): WsCompatible {
36
+ const listeners = new Map<string, Set<(event: any) => void>>()
37
+ let closed = false
38
+
39
+ function addListener(type: string, handler: (event: any) => void) {
40
+ if (!listeners.has(type)) listeners.set(type, new Set())
41
+ listeners.get(type)!.add(handler)
42
+ }
43
+
44
+ function removeListener(type: string, handler: (event: any) => void) {
45
+ listeners.get(type)?.delete(handler)
46
+ }
47
+
48
+ function emit(type: string, event: any) {
49
+ for (const h of listeners.get(type) || []) h(event)
50
+ }
51
+
52
+ // forward port messages → ws 'message' events
53
+ port.onmessage = (event: MessageEvent) => {
54
+ emit('message', { data: event.data })
55
+ }
56
+
57
+ port.onmessageerror = (event: MessageEvent) => {
58
+ emit('error', { message: 'MessagePort error', error: event })
59
+ }
60
+
61
+ return {
62
+ get readyState() {
63
+ return closed ? 3 : 1 // CLOSED or OPEN
64
+ },
65
+
66
+ send(data: string | ArrayBuffer | ArrayBufferView) {
67
+ if (closed) return
68
+ // MessagePort uses postMessage (structured clone)
69
+ port.postMessage(data)
70
+ },
71
+
72
+ close(code?: number, _reason?: string) {
73
+ if (closed) return
74
+ closed = true
75
+ port.close()
76
+ emit('close', { code: code ?? 1000, reason: '', wasClean: true })
77
+ },
78
+
79
+ addEventListener: addListener,
80
+ removeEventListener: removeListener,
81
+ }
82
+ }
83
+
84
+ /**
85
+ * wrap a browser WebSocket to match the ws shim's expected interface.
86
+ *
87
+ * browser WebSocket and the ws shim's CFWebSocket interface are very
88
+ * similar but have subtle differences. this normalizes them.
89
+ */
90
+ export function browserWsToWs(ws: WebSocket): WsCompatible {
91
+ return {
92
+ get readyState() {
93
+ return ws.readyState
94
+ },
95
+
96
+ send(data: string | ArrayBuffer | ArrayBufferView) {
97
+ ws.send(data)
98
+ },
99
+
100
+ close(code?: number, reason?: string) {
101
+ ws.close(code, reason)
102
+ },
103
+
104
+ addEventListener(type: string, handler: (event: any) => void) {
105
+ ws.addEventListener(type, handler)
106
+ },
107
+
108
+ removeEventListener(type: string, handler: (event: any) => void) {
109
+ ws.removeEventListener(type, handler)
110
+ },
111
+ }
112
+ }
113
+
114
+ /**
115
+ * create a connected pair of WebSocket-like objects for in-process
116
+ * communication (e.g., when zero-cache and Zero client are in the
117
+ * same browser context). uses MessageChannel internally.
118
+ *
119
+ * returns [client, server] — client goes to Zero client, server
120
+ * goes to the browser embed's handleWebSocket().
121
+ */
122
+ export function createInProcessPair(): [WsCompatible, WsCompatible] {
123
+ const channel = new MessageChannel()
124
+ return [messagePortToWs(channel.port1), messagePortToWs(channel.port2)]
125
+ }
@@ -0,0 +1,288 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+
3
+ import WebSocket, { WebSocketServer, createWebSocketStream } from './ws.js'
4
+
5
+ /** mock CF WebSocket — mimics the server side of a WebSocketPair */
6
+ function createMockCFWebSocket() {
7
+ const listeners = new Map<string, Array<(event: any) => void>>()
8
+
9
+ const ws = {
10
+ readyState: 1, // OPEN
11
+ send: vi.fn(),
12
+ close: vi.fn(() => {
13
+ ws.readyState = 3 // CLOSED
14
+ }),
15
+ accept: vi.fn(),
16
+ addEventListener: vi.fn((type: string, handler: (event: any) => void) => {
17
+ if (!listeners.has(type)) listeners.set(type, [])
18
+ listeners.get(type)!.push(handler)
19
+ }),
20
+ removeEventListener: vi.fn((type: string, handler: (event: any) => void) => {
21
+ const arr = listeners.get(type)
22
+ if (arr) {
23
+ const idx = arr.indexOf(handler)
24
+ if (idx >= 0) arr.splice(idx, 1)
25
+ }
26
+ }),
27
+
28
+ // helper to fire events in tests
29
+ _fire(type: string, event: any) {
30
+ for (const h of listeners.get(type) || []) h(event)
31
+ },
32
+ }
33
+
34
+ return ws
35
+ }
36
+
37
+ describe('WebSocket shim', () => {
38
+ let cfWs: ReturnType<typeof createMockCFWebSocket>
39
+ let ws: InstanceType<typeof WebSocket>
40
+
41
+ beforeEach(() => {
42
+ cfWs = createMockCFWebSocket()
43
+ ws = new WebSocket(cfWs as any)
44
+ })
45
+
46
+ describe('constructor', () => {
47
+ it('wraps a CF WebSocket', () => {
48
+ expect(ws).toBeInstanceOf(WebSocket)
49
+ })
50
+
51
+ it('handles string URL for localhost (in-process)', () => {
52
+ // no longer throws — localhost URLs use the in-process path
53
+ // without a fastify instance it emits close instead of throwing
54
+ const ws = new WebSocket('ws://localhost')
55
+ expect(ws).toBeInstanceOf(WebSocket)
56
+ })
57
+
58
+ it('sets up event listeners on CF WebSocket', () => {
59
+ expect(cfWs.addEventListener).toHaveBeenCalledWith('message', expect.any(Function))
60
+ expect(cfWs.addEventListener).toHaveBeenCalledWith('close', expect.any(Function))
61
+ expect(cfWs.addEventListener).toHaveBeenCalledWith('error', expect.any(Function))
62
+ expect(cfWs.addEventListener).toHaveBeenCalledWith('open', expect.any(Function))
63
+ })
64
+ })
65
+
66
+ describe('static constants', () => {
67
+ it('has readyState constants', () => {
68
+ expect(WebSocket.CONNECTING).toBe(0)
69
+ expect(WebSocket.OPEN).toBe(1)
70
+ expect(WebSocket.CLOSING).toBe(2)
71
+ expect(WebSocket.CLOSED).toBe(3)
72
+ })
73
+
74
+ it('has instance readyState constants', () => {
75
+ expect(ws.CONNECTING).toBe(0)
76
+ expect(ws.OPEN).toBe(1)
77
+ expect(ws.CLOSING).toBe(2)
78
+ expect(ws.CLOSED).toBe(3)
79
+ })
80
+ })
81
+
82
+ describe('readyState', () => {
83
+ it('reflects CF WebSocket readyState', () => {
84
+ cfWs.readyState = 1
85
+ expect(ws.readyState).toBe(1)
86
+ cfWs.readyState = 3
87
+ expect(ws.readyState).toBe(3)
88
+ })
89
+ })
90
+
91
+ describe('send()', () => {
92
+ it('sends string data', () => {
93
+ ws.send('hello')
94
+ expect(cfWs.send).toHaveBeenCalledWith('hello')
95
+ })
96
+
97
+ it('sends ArrayBuffer data', () => {
98
+ const buf = new ArrayBuffer(4)
99
+ ws.send(buf)
100
+ expect(cfWs.send).toHaveBeenCalledWith(buf)
101
+ })
102
+
103
+ it('sends Uint8Array data', () => {
104
+ const arr = new Uint8Array([1, 2, 3])
105
+ ws.send(arr)
106
+ expect(cfWs.send).toHaveBeenCalledWith(expect.any(Uint8Array))
107
+ })
108
+
109
+ it('sends Buffer data as Uint8Array', () => {
110
+ const buf = Buffer.from('hello')
111
+ ws.send(buf)
112
+ expect(cfWs.send).toHaveBeenCalledWith(expect.any(Uint8Array))
113
+ })
114
+
115
+ it('calls callback on success', () => {
116
+ const cb = vi.fn()
117
+ ws.send('test', cb)
118
+ expect(cb).toHaveBeenCalledWith()
119
+ })
120
+
121
+ it('calls callback with error on failure', () => {
122
+ cfWs.send.mockImplementation(() => {
123
+ throw new Error('send failed')
124
+ })
125
+ const cb = vi.fn()
126
+ ws.send('test', cb)
127
+ expect(cb).toHaveBeenCalledWith(expect.any(Error))
128
+ })
129
+ })
130
+
131
+ describe('close()', () => {
132
+ it('closes the CF WebSocket', () => {
133
+ ws.close(1000, 'normal')
134
+ expect(cfWs.close).toHaveBeenCalledWith(1000, 'normal')
135
+ })
136
+
137
+ it('does not throw if already closed', () => {
138
+ cfWs.close.mockImplementation(() => {
139
+ throw new Error('already closed')
140
+ })
141
+ expect(() => ws.close()).not.toThrow()
142
+ })
143
+ })
144
+
145
+ describe('terminate()', () => {
146
+ it('closes with code 1000', () => {
147
+ ws.terminate()
148
+ expect(cfWs.close).toHaveBeenCalled()
149
+ expect(cfWs.close.mock.calls[0][0]).toBe(1000)
150
+ })
151
+ })
152
+
153
+ describe('ping()', () => {
154
+ it('emits pong (CF handles ping at platform level)', () => {
155
+ const pongHandler = vi.fn()
156
+ ws.on('pong', pongHandler)
157
+ ws.ping()
158
+ expect(pongHandler).toHaveBeenCalled()
159
+ })
160
+ })
161
+
162
+ describe('event forwarding', () => {
163
+ it('emits message events from CF WebSocket', () => {
164
+ const handler = vi.fn()
165
+ ws.on('message', handler)
166
+ cfWs._fire('message', { data: 'hello' })
167
+ // ws npm API: message handler gets (data, isBinary)
168
+ expect(handler).toHaveBeenCalledWith('hello', false)
169
+ })
170
+
171
+ it('emits close events from CF WebSocket', () => {
172
+ const handler = vi.fn()
173
+ ws.on('close', handler)
174
+ cfWs._fire('close', { code: 1000, reason: 'normal', wasClean: true })
175
+ // ws npm API: close handler gets (code, reason)
176
+ expect(handler).toHaveBeenCalledWith(1000, 'normal')
177
+ })
178
+
179
+ it('emits error events from CF WebSocket', () => {
180
+ const handler = vi.fn()
181
+ ws.on('error', handler)
182
+ cfWs._fire('error', { message: 'oops', error: new Error('oops') })
183
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }))
184
+ })
185
+
186
+ it('supports addEventListener/removeEventListener', () => {
187
+ const handler = vi.fn()
188
+ ws.addEventListener('message', handler)
189
+ cfWs._fire('message', { data: 'test' })
190
+ expect(handler).toHaveBeenCalledTimes(1)
191
+
192
+ ws.removeEventListener('message', handler)
193
+ cfWs._fire('message', { data: 'test2' })
194
+ expect(handler).toHaveBeenCalledTimes(1)
195
+ })
196
+ })
197
+ })
198
+
199
+ describe('WebSocketServer shim', () => {
200
+ it('creates with noServer option', () => {
201
+ const wss = new WebSocketServer({ noServer: true })
202
+ expect(wss).toBeInstanceOf(WebSocketServer)
203
+ })
204
+
205
+ describe('handleUpgrade()', () => {
206
+ it('wraps CF WebSocket and calls callback', () => {
207
+ const wss = new WebSocketServer({ noServer: true })
208
+ const cfWs = createMockCFWebSocket()
209
+ const callback = vi.fn()
210
+
211
+ wss.handleUpgrade({ url: '/test', headers: {} }, cfWs, Buffer.alloc(0), callback)
212
+
213
+ expect(callback).toHaveBeenCalledTimes(1)
214
+ const ws = callback.mock.calls[0][0]
215
+ expect(ws).toBeInstanceOf(WebSocket)
216
+ })
217
+
218
+ it('wrapped WebSocket delegates send to CF WebSocket', () => {
219
+ const wss = new WebSocketServer({ noServer: true })
220
+ const cfWs = createMockCFWebSocket()
221
+ let wrappedWs: InstanceType<typeof WebSocket>
222
+
223
+ wss.handleUpgrade({}, cfWs, null, (ws) => {
224
+ wrappedWs = ws
225
+ })
226
+
227
+ wrappedWs!.send('hello from server')
228
+ expect(cfWs.send).toHaveBeenCalledWith('hello from server')
229
+ })
230
+ })
231
+ })
232
+
233
+ describe('createWebSocketStream', () => {
234
+ it('creates a duplex stream from WebSocket', () => {
235
+ const cfWs = createMockCFWebSocket()
236
+ const ws = new WebSocket(cfWs as any)
237
+ const stream = createWebSocketStream(ws)
238
+
239
+ expect(stream).toBeDefined()
240
+ expect(stream.readable).toBe(true)
241
+ expect(stream.writable).toBe(true)
242
+ })
243
+
244
+ it('writes to WebSocket via stream', async () => {
245
+ const cfWs = createMockCFWebSocket()
246
+ const ws = new WebSocket(cfWs as any)
247
+ const stream = createWebSocketStream(ws)
248
+
249
+ await new Promise<void>((resolve) => {
250
+ stream.write('hello', () => {
251
+ expect(cfWs.send).toHaveBeenCalledWith('hello')
252
+ stream.destroy()
253
+ resolve()
254
+ })
255
+ })
256
+ })
257
+
258
+ it('reads from WebSocket messages', async () => {
259
+ const cfWs = createMockCFWebSocket()
260
+ const ws = new WebSocket(cfWs as any)
261
+ const stream = createWebSocketStream(ws)
262
+
263
+ const received = new Promise<string>((resolve) => {
264
+ stream.on('data', (chunk) => {
265
+ resolve(chunk.toString())
266
+ })
267
+ })
268
+
269
+ cfWs._fire('message', { data: 'world' })
270
+
271
+ const data = await received
272
+ expect(data).toBe('world')
273
+ stream.destroy()
274
+ })
275
+
276
+ it('ends stream on WebSocket close', async () => {
277
+ const cfWs = createMockCFWebSocket()
278
+ const ws = new WebSocket(cfWs as any)
279
+ const stream = createWebSocketStream(ws)
280
+
281
+ const closed = new Promise<void>((resolve) => {
282
+ stream.on('close', () => resolve())
283
+ })
284
+
285
+ cfWs._fire('close', { code: 1000 })
286
+ await closed
287
+ })
288
+ })