wspromisify 2.10.1 → 2.10.2

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.
package/src/WSC.ts DELETED
@@ -1,319 +0,0 @@
1
- import { AnyFunc, AnyObject, both, callWith, F, isNil, notf, once, qfilter, T, tap, typeIs } from 'pepka'
2
- import { Zipnum } from 'zipnum'
3
- import { processConfig } from './config'
4
- import './types'
5
- import { add_event, rm_event, sett } from './utils'
6
-
7
- const MAX_32 = 2**31 - 1
8
- const { random } = Math
9
- const zipnum = new Zipnum()
10
- const callit = callWith([])
11
- const isNumber = both(typeIs('Number'), notf(isNaN))
12
- const ping_send_opts: wsc.SendOptions = {_is_ping: true}
13
- const clearTO = (to: NodeJS.Timeout|null) => to && clearTimeout(to)
14
-
15
- type EventHandler<T extends keyof WebSocketEventMap> = AnyFunc<any, [WebSocketEventMap[T]]>
16
- type EventHandlers = {
17
- open: EventHandler<'open'>[]
18
- close: EventHandler<'close'>[]
19
- error: EventHandler<'error'>[]
20
- message: AnyFunc<any, [WebSocketEventMap['message'] & {data: any}]>[]
21
- timeout: AnyFunc<any, [data: any]>[]
22
- }
23
- const genid = (q: AnyObject) => {
24
- const id = zipnum.zip((random()*(MAX_32-10))|0)
25
- return id in q ? genid(q) : id
26
- }
27
-
28
- export class WebSocketClient {
29
- private ws: wsc.Socket|null = null
30
- private intentionally_closed = false
31
- private reconnect_timeout: NodeJS.Timeout|null = null
32
- private queue: Record<string, wsc.Message> = {}
33
- private onReadyQueue: AnyFunc[] = []
34
- private onCloseQueue: AnyFunc[] = []
35
- private handlers: EventHandlers = { open: [], close: [], message: [], error: [], timeout: [] }
36
- private config = <wsc.Config>{}
37
- private ping_timer: NodeJS.Timeout|null = null
38
- private idle_timer: NodeJS.Timeout|null = null
39
- private get opened() { return this.ws?.readyState===1 } // The only opened state.
40
-
41
- private init_flush(): void {
42
- // TODO: reject them or save somehow ?..
43
- qfilter(F, this.queue)
44
- }
45
- private call(event_name: wsc.WSEvent, ...args: any[]) {
46
- for(const h of this.handlers[event_name]) h(...args)
47
- }
48
- private log(event: wsc.WSEvent, message: any = null, time: number|null = null): void {
49
- const {config} = this
50
- setTimeout(() => {
51
- if(time === null)
52
- if(config.timer) config.log(event, null, message)
53
- else config.log(event, message)
54
- else
55
- config.log(event, time, message)
56
- })
57
- }
58
- private resetPing() {
59
- const {config: {ping}, ping_timer} = this
60
- if(ping) {
61
- if(!isNil(ping_timer))
62
- clearTimeout(ping_timer as NodeJS.Timeout)
63
- this.ping_timer = sett(ping.interval*1e3, async () => {
64
- const {ping_timer, opened} = this
65
- if(opened) {
66
- await this.send(ping.content, ping_send_opts)
67
- this.resetPing()
68
- } else clearTimeout(ping_timer!)
69
- })
70
- }
71
- }
72
-
73
- // FIXME: Make some version where it could work faster (for streaming).
74
- private resetIdle() {
75
- const {config: {max_idle_time: time}, idle_timer} = this
76
- if(time!==Infinity) {
77
- if(!isNil(idle_timer)) clearTimeout(idle_timer!)
78
- this.idle_timer = sett(time*1e3, () => this.opened && this.close())
79
- }
80
- }
81
-
82
- private initSocket(ws: wsc.Socket) {
83
- const {queue, config} = this
84
- this.ws = ws
85
- this.onReadyQueue.forEach((fn: Function) => fn())
86
- this.onReadyQueue.splice(0)
87
- const {id_key, data_key} = config.server
88
- // Works also on previously opened sockets that do not fire 'open' event.
89
- this.call('open', ws)
90
- for(const msg_id in queue) ws.send(queue[msg_id].msg)
91
- if(this.reconnect_timeout !== null) {
92
- clearInterval(this.reconnect_timeout)
93
- this.reconnect_timeout = null
94
- }
95
- this.resetPing(); this.resetIdle()
96
- add_event(ws, 'close', async (...e) => {
97
- this.ws = null
98
- this.onCloseQueue.forEach(callit)
99
- this.onCloseQueue.splice(0)
100
- this.call('close', ...e)
101
- // Auto reconnect.
102
- let {reconnect, reconnection_attempts} = config
103
- if(isNumber(reconnect)) {
104
- const reconnectFunc = async () => {
105
- if(this.intentionally_closed || !reconnection_attempts) return;
106
- reconnection_attempts--
107
- this.log('reconnect')
108
- if(!isNil(this.ws)) {
109
- this.ws!.close()
110
- this.ws = null
111
- }
112
- // If some error occured, try again.
113
- const status = await this.connect()
114
- if(!isNil(status))
115
- this.reconnect_timeout = setTimeout(reconnectFunc, reconnect*1e3)
116
- }
117
- // TODO: test normal close by server. Would it be infinite ?
118
- reconnectFunc()
119
- }
120
- })
121
- add_event(ws, 'message', (e) => {
122
- try {
123
- const data = config.decode(e.data)
124
- this.call('message', {...e, data}) // TODO: Breaking: make message-ext another handler.
125
- let internal = false
126
- if(typeIs('Object', data) && id_key in data) {
127
- const q = this.queue[data[id_key]]
128
- if(q) {
129
- // Debug, Log.
130
- const time = q.sent_time ? (Date.now() - q.sent_time) : null
131
- this.log('message', data[data_key], time)
132
- // Play.
133
- q.ff(data[data_key])
134
- internal = true
135
- }
136
- }
137
- if(!internal) this.log('message-ext', data)
138
- } catch (err) {
139
- console.error(err, `WSP: Decode error. Got: ${e.data}`)
140
- }
141
- this.resetPing()
142
- })
143
- }
144
- private opening = false
145
- private connect() { // returns status if won't open or null if ok.
146
- return new Promise<null|number>((ff) => {
147
- if(this.opened||this.opening) return ff(null)
148
- this.opening = true
149
- const config = this.config
150
- const ws = config.socket || config.adapter(config.url, config.protocols)
151
- if(!ws || ws.readyState > 1) {
152
- this.opening = false
153
- this.ws = null
154
- this.log('error', 'ready() on closing or closed state! status 2.')
155
- return ff(2)
156
- }
157
- const ffo = once((s: null|number) => {this.opening=false; ff(s)})
158
- add_event(ws, 'error', once((e) => {
159
- this.ws = null
160
- this.log('error', 'status 3. Err: '+e.message)
161
- this.call('error', e)
162
- // Some network error: Connection refused or so.
163
- ffo(3)
164
- }))
165
- // Because 'open' won't be envoked on opened socket.
166
- if(ws.readyState) {
167
- this.initSocket(ws)
168
- ffo(null)
169
- } else {
170
- add_event(ws, 'open', once(() => {
171
- this.log('open')
172
- this.initSocket(ws)
173
- ffo(null)
174
- }))
175
- }
176
- })
177
- }
178
- public get socket() { return this.ws }
179
- public async ready() {
180
- return new Promise<void>((ff) => {
181
- if(this.config.lazy || this.opened) ff() // FIXME: (possibly) breaking change ?? At least minor ver bump with a notice!!!
182
- else this.onReadyQueue.push(ff)
183
- })
184
- }
185
- public on(
186
- event_name: wsc.WSEvent,
187
- handler: (data: any) => any,
188
- predicate: (data: any) => boolean = T,
189
- raw = false
190
- ) {
191
- const _handler: wsc.EventHandler = (event) =>
192
- predicate(event) && handler(event)
193
- if(raw) add_event(this.ws as wsc.Socket, event_name, _handler)
194
- else this.handlers[event_name].push(_handler)
195
- return _handler
196
- }
197
- public off(
198
- event_name: wsc.WSEvent,
199
- handler: (data: any) => any,
200
- raw = false
201
- ) {
202
- if(raw) return rm_event(this.ws as wsc.Socket, event_name, handler)
203
- const handlers = this.handlers[event_name]
204
- const i = handlers.indexOf(handler)
205
- if(~i) handlers.splice(i, 1)
206
- }
207
- public async close(): wsc.AsyncErrCode {
208
- return new Promise((ff, rj) => {
209
- if(this.ws === null) {
210
- rj('WSP: closing a non-inited socket!')
211
- } else {
212
- this.onCloseQueue.push(() => {
213
- this.init_flush()
214
- ff(null)
215
- })
216
- this.ws.close()
217
- this.ws = null
218
- this.intentionally_closed = true
219
- }
220
- })
221
- }
222
- public open() {
223
- if(!this.opened) {
224
- this.intentionally_closed = false
225
- return this.connect()
226
- }
227
- }
228
- // TODO: Сделать сэттер элементов конфигурации чтобы двигать таймауты.
229
- // И эвент, когда схема наша, а соответствующего элемента очереди не ма.
230
- // Или добавить флажок к эвенту 'message'.F
231
- // И событие 'line' со значением on: boolean. Критерии?
232
- private async prepareMessage<RequestDataType = any>(
233
- message_data: RequestDataType,
234
- opts = <wsc.SendOptions>{}
235
- ) {
236
- this.log(opts._is_ping ? 'ping' : 'send', message_data)
237
- const {config, queue} = this
238
- const {pipes, server: {data_key}} = config
239
- const {top, _is_ping} = opts
240
- const id = genid(queue)
241
- if(typeof top === 'object') {
242
- if(top[data_key]) {
243
- throw new Error(`Attempting to set data key/token via ${opts._is_ping ? 'ping' : 'send'}() options!`)
244
- }
245
- }
246
- for(const pipe of pipes) message_data = pipe(message_data)
247
- const [msg, err] = await Promise.all([
248
- config.encode(id, message_data, config),
249
- this.connect()
250
- ])
251
- if(err) throw new Error('ERR while opening connection #'+err)
252
- const cleanup = tap(() => delete this.queue[id])
253
- const timeout = (rj: AnyFunc) => sett(config.timeout, () => {
254
- if(id in queue) {
255
- this.call('timeout', message_data)
256
- rj({'Websocket timeout expired': config.timeout, 'for the message': message_data})
257
- cleanup()
258
- }
259
- })
260
- if(this.opened) {
261
- sett(0, () => this.ws!.send(msg))
262
- this.resetPing()
263
- if(!_is_ping) this.resetIdle()
264
- }
265
- return { id, msg, timeout, cleanup }
266
- }
267
- /** .send(your_data) wraps request to server with {id: `hash`, data: `actually your data`},
268
- returns a Promise that will be rejected after a timeout or
269
- resolved if server returns the same signature: {id: `same_hash`, data: `response data`}.
270
- */
271
- public async send<RequestDataType = any, ResponseDataType = any>(
272
- message_data: RequestDataType,
273
- opts = <wsc.SendOptions>{}
274
- ): Promise<ResponseDataType> {
275
- const {id, msg, timeout, cleanup} = await this.prepareMessage(message_data, opts)
276
- const {queue, config} = this
277
- return new Promise<ResponseDataType>((ff, rj) => {
278
- const to = timeout(rj)
279
- queue[id] = {
280
- msg,
281
- data_type: config.data_type,
282
- sent_time: config.timer ? Date.now() : null,
283
- ff(x: any) {
284
- clearTO(to)
285
- ff(x)
286
- }
287
- }
288
- }).finally(cleanup)
289
- }
290
- // FIXME: rejects into ff somehow.
291
- public async *stream<RequestDataType = any, ResponseDataType = any>(
292
- message_data: RequestDataType,
293
- opts = <wsc.SendOptions>{}
294
- ): AsyncGenerator<ResponseDataType, void, unknown> {
295
- const {id, msg, timeout, cleanup} = await this.prepareMessage(message_data, opts)
296
- const {queue, config} = this
297
- let done = false, fulfill: AnyFunc, to: NodeJS.Timeout|null = null
298
- queue[id] = {
299
- msg,
300
- ff: (msg: ResponseDataType&{done?: boolean}) => {
301
- if(msg?.done) { delete msg.done; done=true; setTimeout(cleanup) }
302
- fulfill(msg)
303
- },
304
- data_type: config.data_type,
305
- sent_time: config.timer ? Date.now() : null
306
- }
307
- while(!done) yield await new Promise<ResponseDataType>((ff, rj) => {
308
- to=timeout(rj); fulfill=ff
309
- }).catch((e) => cleanup(e)).finally(() => {clearTO(to); to=null})
310
- }
311
- // TODO: Add .on handlers to config!
312
- constructor(user_config: wsc.UserConfig = {}) {
313
- this.config = processConfig(user_config)
314
- if(!this.config.lazy) this.connect()
315
- }
316
- }
317
-
318
- /* TODO: v3: @.deprecated. Use named import { WebSocketClient } instead. */
319
- export default WebSocketClient
package/src/config.ts DELETED
@@ -1,57 +0,0 @@
1
- import './types'
2
- import { native_ws } from './utils'
3
-
4
- const default_config = <wsc.Config>{
5
- data_type: 'json', // ToDo some other stuff maybe.
6
- // Debug features.
7
- log: (() => null),
8
- timer: false,
9
- // Set up.
10
- url: 'localhost',
11
- timeout: 1400,
12
- reconnect: 2, // Reconnect timeout in seconds or null.
13
- reconnection_attempts: Infinity,
14
- max_idle_time: Infinity,
15
- lazy: false,
16
- socket: null,
17
- adapter: ((host, protocols) => new WebSocket(host, protocols)),
18
- encode: (key, data, { server }) => JSON.stringify({
19
- [server.id_key]: key,
20
- [server.data_key]: data
21
- }),
22
- decode: (rawMessage) => JSON.parse(rawMessage),
23
- protocols: [],
24
- pipes: [],
25
- server: {
26
- id_key: 'id',
27
- data_key: 'data'
28
- },
29
- ping: {
30
- interval: 55,
31
- content: {}
32
- }
33
- }
34
-
35
- export const processConfig = (config: wsc.UserConfig) => {
36
- if(native_ws===null && !('adapter' in config)) throw new Error(`
37
- This platform has no native WebSocket implementation.
38
- Please use 'ws' package as an adapter.
39
- See https://github.com/houd1ni/WebsocketPromisify/issues/23
40
- `)
41
- const full_config: wsc.Config = Object.assign(
42
- {},
43
- default_config,
44
- config
45
- )
46
- const url = full_config.url
47
- if(url[0] == '/') {
48
- try {
49
- const protocol = location.protocol.includes('s:') ? 'wss' : 'ws'
50
- full_config.url = `${protocol}://${location.hostname}:${location.port}${url}`
51
- } catch (e) {
52
- throw new Error('WSP: URL starting with / in non-browser environment!')
53
- }
54
- }
55
-
56
- return full_config
57
- }
package/src/types.ts DELETED
@@ -1,68 +0,0 @@
1
- declare namespace wsc {
2
-
3
- interface DataObject {
4
- [key: string]: any
5
- }
6
-
7
- export type WSEvent = 'open' | 'message' | 'message-ext' | 'close' | 'error' | 'timeout' | 'reconnect' | 'send' | 'ping'
8
-
9
- /** Minimal socket-like interface. */
10
- interface Socket {
11
- readyState: number
12
- send(...any: any[]): void
13
- close(): void
14
- addEventListener: WebSocket["addEventListener"]
15
- removeEventListener: WebSocket["removeEventListener"]
16
- }
17
-
18
- export type AsyncErrCode = Promise<number | null | {}>
19
-
20
- export type EventHandler = (e: any) => void
21
-
22
- export type DataPipe = (message: any) => any
23
-
24
- export type DataType = 'json' | 'string'
25
-
26
- export interface Config {
27
- data_type: DataType
28
- log (event: string, time?: number|null, message?: any): void
29
- log (event: string, message?: any): void
30
- timer: boolean
31
- url: string
32
- timeout: number
33
- reconnect: number
34
- reconnection_attempts: number
35
- max_idle_time: number
36
- lazy: boolean
37
- socket: Socket | null
38
- adapter: (host: string, protocols?: string[]) => Socket
39
- encode: (key: string, message: any, config: Config) => any
40
- decode: (rawMessage: any) => {
41
- [id_or_data_key: string]: string
42
- }
43
- protocols: string[]
44
- pipes: DataPipe[]
45
- server: {
46
- id_key: string
47
- data_key: string
48
- },
49
- ping: {
50
- interval: number
51
- content: any
52
- }
53
- }
54
-
55
- export type UserConfig = Partial<Config>
56
-
57
- export type SendOptions = Partial<{
58
- top: any
59
- data_type: DataType
60
- _is_ping: boolean
61
- }>
62
-
63
- export interface Message {
64
- msg: any, ff(x: any): any,
65
- data_type: DataType,
66
- sent_time: number | null
67
- }
68
- }
package/src/utils.ts DELETED
@@ -1,21 +0,0 @@
1
- import './types'
2
-
3
- export const native_ws = (() => {try {return WebSocket||null}catch { return null }})()
4
-
5
- export const add_event = (o: wsc.Socket, e: string, handler: wsc.EventHandler) => {
6
- return o.addEventListener(e, handler)
7
- }
8
- export const rm_event = (o: wsc.Socket, e: string, handler: wsc.EventHandler) => {
9
- return o.removeEventListener(e, handler)
10
- }
11
-
12
- export const sett = (
13
- a: number,
14
- b: { (): void; (...args: any[]): void; }
15
- ) => setTimeout(b, a)
16
-
17
- export const try_splice = <T = any>(arr: T[], el: T) => {
18
- const i = arr.indexOf(el)
19
- if(~i) return arr.splice(arr.indexOf(el), 1)
20
- else return []
21
- }
package/test/index.ts DELETED
@@ -1,24 +0,0 @@
1
- import './specs/close'
2
- import './specs/drops'
3
- import './specs/echo'
4
- import './specs/existing-socket'
5
- import './specs/lazy'
6
- import './specs/ready'
7
- import './specs/reconnect'
8
- import './specs/lazy-send-before-open'
9
- import './specs/socket'
10
- import './specs/no-native-throws'
11
- import './specs/stream-simple'
12
- import './specs/stream-comprehensive'
13
- import './specs/stream-real'
14
- import mockServer from './mock/server'
15
- import {test} from './suite'
16
-
17
- const {shutDown, isRunning} = await mockServer()
18
- test.after(() => {
19
- setTimeout(async () => {
20
- if(isRunning()) await shutDown()
21
- process.exit()
22
- }, 100)
23
- })
24
- test.run()
package/test/mock/WS.ts DELETED
@@ -1,75 +0,0 @@
1
-
2
- import WebSocket, { WebSocketServer } from 'ws'
3
- import {add, compose, genBy, identity, noop, wait} from 'pepka'
4
-
5
- let server: WebSocketServer|null = null
6
-
7
- const createServer = (port = 40510) => new Promise<WebSocketServer>((ff, rj) => {
8
- if(server) return rj('The server is already running!')
9
- server = new WebSocketServer({ port }, () => {
10
- server!.on('connection', (socket: WebSocket&{isAlive: boolean}) => {
11
- socket.on('message', async (rawMessage: string) => {
12
- // console.log({rawMessage: rawMessage.toString()})
13
- const {id, data} = JSON.parse(rawMessage)
14
- let response = ''
15
- if(data.shut) {
16
- socket.terminate()
17
- socket.isAlive = false
18
- socket.ping('', false, noop)
19
- return null
20
- } else if(data.echo) {
21
- response = data
22
- } else if(data.stream) {
23
- // Handle streaming responses
24
- const chunks = genBy(compose(add(1), identity), 20) // Generate 20 chunks
25
- // console.log(chunks)
26
- const delay = 2 // 20ms delay between chunks for reliable delivery
27
-
28
- if(data.multi) {
29
- // Multi-chunk streaming
30
- for(const i in chunks) {
31
- const chunk = chunks[i]
32
- socket.send(JSON.stringify({
33
- id,
34
- data: {
35
- ...data, chunk,
36
- done: +i === chunks.length - 1 // Last chunk gets done: true
37
- }
38
- }))
39
- await wait(delay)
40
- }
41
- } else {
42
- // Single response
43
- socket.send(JSON.stringify({
44
- id,
45
- data: {
46
- ...data,
47
- chunk: chunks[0],
48
- done: true
49
- }
50
- }))
51
- }
52
- return null
53
- }
54
- socket.send(JSON.stringify({ id, data: response }))
55
- return null
56
- })
57
- return true
58
- })
59
- return ff(server!)
60
- })
61
- server.on('', console.log)
62
- })
63
-
64
- const killServer = async () => new Promise<void>((ff, rj) => {
65
- if(server) {
66
- for(const socket of server.clients) socket.terminate()
67
- server.close(() => {
68
- server = null
69
- ff()
70
- })
71
- } else
72
- rj('The server is already down!')
73
- })
74
-
75
- export { createServer, killServer }
@@ -1,18 +0,0 @@
1
-
2
- import { WebSocketServer } from 'ws'
3
- import { createServer, killServer } from './WS'
4
-
5
- let port: number,
6
- server: WebSocketServer|null
7
-
8
- export default async (new_port?: number) => {
9
- if(!server) {
10
- port = new_port || 8000 + Math.ceil(Math.random()*500)
11
- server = await createServer(port)
12
- }
13
- return {
14
- server, port,
15
- isRunning() {return Boolean(server)},
16
- shutDown: async () => { await killServer(); server=null }
17
- }
18
- }
@@ -1,18 +0,0 @@
1
- import { test } from '../suite'
2
- import { createNew } from '../utils.js'
3
- import mockServer from '../mock/server'
4
-
5
- /** Closes the connenction. */
6
- test('close', () => new Promise<void>(async (ff, rj) => {
7
- const {port} = await mockServer()
8
- const ws = await createNew({}, port)
9
-
10
- setTimeout(async () => {
11
- try {
12
- await ws.close()
13
- if(ws.socket === null) ff(); else rj()
14
- } catch(e) {
15
- rj()
16
- }
17
- }, 500)
18
- }))
@@ -1,29 +0,0 @@
1
- import { createNew } from '../utils'
2
- import mockServer from '../mock/server'
3
- import { test } from '../suite'
4
- import { wait } from 'pepka'
5
-
6
- /** Rejects messages by timout */
7
- test('drops', () => new Promise(async (ff, rj) => {
8
- const {port, shutDown} = await mockServer()
9
- const ws = await createNew({timeout: 500}, port)
10
-
11
- await shutDown()
12
- await wait(200)
13
-
14
- const msg = {echo: true, msg: 'hello!'}
15
- let to: NodeJS.Timeout
16
- try {
17
- to = setTimeout(() => {
18
- return rj()
19
- }, 600)
20
- await ws.send(msg)
21
- if(to) clearTimeout(to)
22
- await mockServer(port)
23
- return rj()
24
- } catch(e) {
25
- if(to!) clearTimeout(to)
26
- await mockServer(port)
27
- return ff()
28
- }
29
- }))
@@ -1,24 +0,0 @@
1
- import { equals } from 'pepka'
2
- import { createNew, timeout } from '../utils'
3
- import mockServer from '../mock/server'
4
- import { test } from '../suite'
5
-
6
- /** Proof of work. */
7
- test('echo', timeout(5e3, () => new Promise<void>(async (ff, rj) => {
8
- const {port} = await mockServer()
9
- let to = setTimeout(() => rj('cannot create'), 2e2)
10
- const ws = await createNew({}, port)
11
- clearTimeout(to)
12
-
13
- to = setTimeout(() => rj('cannot ready'), 2e2)
14
- await ws.ready()
15
- clearTimeout(to)
16
-
17
- const msg = {echo: true, msg: 'hello!'}
18
- to = setTimeout(() => rj('cannot send'), 2e2)
19
- const response = await ws.send(msg)
20
- clearTimeout(to)
21
-
22
- if(equals(response, msg)) ff(); else rj('echo msg is not equal.')
23
- })
24
- ))
@@ -1 +0,0 @@
1
- // TODO with mock server with specific port.