@owlmeans/socket 0.1.0

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/model.ts ADDED
@@ -0,0 +1,362 @@
1
+ import { ResilientError } from '@owlmeans/error'
2
+ import { CALL_TIMEOUT, MessageType } from './consts.js'
3
+ import { SocketMessageMalformed, SocketTimeout } from './errors.js'
4
+ import type {
5
+ AuthMessage, CallHendler, CallMessage, CallResolver, Connection, ConnectionListener,
6
+ EventMessage, Message, RequestHandler
7
+ } from './types.js'
8
+ import { uuid } from '@owlmeans/basic-ids'
9
+ import { AuthenticationStage, AuthError } from '@owlmeans/auth'
10
+
11
+ export const createBasicConnection = (): Connection => {
12
+ const listeners: ConnectionListener[] = []
13
+
14
+ const callPerformers: { [method: string]: CallHendler<any, any[]> } = {}
15
+ const calls: { [id: string]: CallResolver<any> } = {}
16
+ const requestAcknowledgers: RequestHandler<any>[] = []
17
+ const observers: { [id: string]: (payload: any) => Promise<boolean> } = {}
18
+ const responseObservers: ((payload: any) => Promise<boolean>)[] = []
19
+ const eventObservers: { [event: string]: ((event: EventMessage<any>) => Promise<void>)[] } = {}
20
+ const queue: Message<any>[] = []
21
+
22
+ const conn: Connection = {
23
+ stage: AuthenticationStage.Init,
24
+
25
+ send: async () => {
26
+ throw new SyntaxError('Connection send method is not implemented')
27
+ },
28
+ close: async () => {
29
+ throw new SyntaxError('Connection close method is not implemented')
30
+ },
31
+ authenticate: async () => {
32
+ throw new SyntaxError('Connection authenticate method is not implemented')
33
+ },
34
+
35
+ notify: async (event, payload) => {
36
+ const msg: EventMessage<any> = {
37
+ event, payload, type: MessageType.Event
38
+ }
39
+ await conn.send(msg)
40
+ },
41
+
42
+ observe: (event, handler) => {
43
+ eventObservers[event] = eventObservers[event] ?? []
44
+ eventObservers[event].push(handler)
45
+ return () => {
46
+ const index = eventObservers[event].indexOf(handler)
47
+ if (index !== -1) {
48
+ eventObservers[event].splice(index, 1)
49
+ }
50
+ }
51
+ },
52
+
53
+ call: async (method, ...payload) => {
54
+ const msg: CallMessage<any> = {
55
+ method, payload, type: MessageType.Call, id: uuid()
56
+ }
57
+ conn.prepare?.(msg)
58
+ return new Promise(async (resolve, reject) => {
59
+ msg.timeout = msg.timeout ?? conn.defaultCallTimeout ?? CALL_TIMEOUT
60
+ const timeout = setTimeout(() => msg.timeout !== 0 && reject(new SocketTimeout('call')), msg.timeout)
61
+ calls[msg.id!] = {
62
+ resolve: value => {
63
+ clearTimeout(timeout)
64
+ delete calls[msg.id!]
65
+ resolve(value)
66
+ }, reject: error => {
67
+ clearTimeout(timeout)
68
+ delete calls[msg.id!]
69
+ reject(error)
70
+ }
71
+ }
72
+
73
+ try {
74
+ await conn.send(msg)
75
+ } catch (e) {
76
+ clearTimeout(timeout)
77
+ delete calls[msg.id!]
78
+ reject(e)
79
+ }
80
+ })
81
+ },
82
+
83
+ perform: async (method, handler) => {
84
+ callPerformers[method] = handler
85
+ },
86
+
87
+ request: async (payload, observer) => {
88
+ const msg: Message<any> = {
89
+ type: MessageType.Request, payload, id: uuid()
90
+ }
91
+ if (observer != null) {
92
+ observers[msg.id!] = observer
93
+ }
94
+ await conn.send(msg)
95
+
96
+ return () => {
97
+ if (observers[msg.id!] != null) {
98
+ delete observers[msg.id!]
99
+ }
100
+ }
101
+ },
102
+
103
+ observeResponse: async observer => {
104
+ responseObservers.push(observer)
105
+ return () => {
106
+ const index = responseObservers.indexOf(observer)
107
+ if (index !== -1) {
108
+ responseObservers.splice(index, 1)
109
+ }
110
+ }
111
+ },
112
+
113
+ acknowledge: handler => {
114
+ requestAcknowledgers.push(handler)
115
+
116
+ return () => {
117
+ const index = requestAcknowledgers.indexOf(handler)
118
+ if (index !== -1) {
119
+ requestAcknowledgers.splice(index, 1)
120
+ }
121
+ }
122
+ },
123
+
124
+ reply: async (id, payload) => {
125
+ const msg: Message<any> = {
126
+ type: MessageType.Response, payload, id
127
+ }
128
+ await conn.send(msg)
129
+ },
130
+
131
+ listen: listener => {
132
+ listeners.push(listener)
133
+ return () => {
134
+ const index = listeners.indexOf(listener)
135
+ if (index !== -1) {
136
+ listeners.splice(index, 1)
137
+ }
138
+ }
139
+ },
140
+
141
+ auth: async (stage, payload) => {
142
+ conn.stage = stage
143
+ const msg: AuthMessage<any> = { type: MessageType.Auth, stage, payload }
144
+
145
+ return new Promise(async (resolve, reject) => {
146
+ conn._authSequence = { reject, resolve }
147
+ await conn.send(msg)
148
+ })
149
+ },
150
+
151
+ enqueue: async (payload, id) => {
152
+ const msg: Message<any> = {
153
+ type: MessageType.Message, payload, id
154
+ }
155
+ await conn.send(msg)
156
+ },
157
+
158
+ enqueued: () => queue.length,
159
+
160
+ consume: filter => {
161
+ if (queue.length === 0) {
162
+ return null
163
+ }
164
+ if (filter == null) {
165
+ const msg = queue.shift()
166
+ return [msg?.payload ?? null, msg?.id, queue.length]
167
+ }
168
+ const index = queue.findIndex(msg => filter(msg.payload))
169
+ if (index < 0) {
170
+ return [null, null, queue.length]
171
+ }
172
+ const [msg] = queue.splice(index, 1)
173
+
174
+ return [msg.payload, msg.id, queue.length]
175
+ },
176
+
177
+ receive: async message => {
178
+ if (message.startsWith('{') || message.startsWith('[')) {
179
+ let msg: Message<any> | string = message
180
+ try {
181
+ msg = JSON.parse(message)
182
+ } catch {
183
+ }
184
+ if (typeof msg === 'object') {
185
+ if (msg.payload == null) {
186
+ msg.payload = msg
187
+ }
188
+ if (msg.type == null) {
189
+ msg.type = MessageType.Message
190
+ }
191
+ msg.rawData = message
192
+ if (conn.prepare != null) {
193
+ conn.prepare(msg, true)
194
+ }
195
+ try {
196
+ switch (msg.type) {
197
+ case MessageType.Call: {
198
+ await conn._receiveCall(msg as CallMessage<any>)
199
+ break
200
+ }
201
+ case MessageType.Result: {
202
+ await conn._receiveResult(msg)
203
+ break
204
+ }
205
+ case MessageType.Error: {
206
+ await conn._receiveError(msg)
207
+ break
208
+ }
209
+ case MessageType.Request: {
210
+ await conn._receiveRequest(msg)
211
+ break
212
+ }
213
+ case MessageType.Response: {
214
+ await conn._receiveResponse(msg)
215
+ break
216
+ }
217
+ case MessageType.Event: {
218
+ await conn._receiveEvent(msg as EventMessage<any>)
219
+ break
220
+ }
221
+ case MessageType.Message: {
222
+ await conn._receiveMessage(msg)
223
+ break
224
+ }
225
+ case MessageType.Auth: {
226
+ const _msg: AuthMessage<any> = msg as AuthMessage<any>
227
+ if (conn._authSequence != null) {
228
+ if (_msg.stage == null) {
229
+ conn._authSequence.reject(ResilientError.ensure(_msg.payload ?? new AuthError('socket:unknown')))
230
+ } else {
231
+ conn.stage = _msg.stage
232
+ conn._authSequence.resolve(_msg.payload)
233
+ }
234
+ conn._authSequence = undefined
235
+ } else {
236
+ try {
237
+ const [stage, response] = await conn.authenticate(_msg.stage, _msg.payload)
238
+ if (stage != null) {
239
+ conn.auth(stage, response)
240
+ }
241
+ } catch (e) {
242
+ conn.auth(null as any, ResilientError.marshal(ResilientError.ensure(e as Error)))
243
+ } finally {
244
+ conn._authSequence = undefined
245
+ }
246
+ }
247
+ break
248
+ }
249
+ }
250
+ } catch (e) {
251
+ console.error('Error on message processing:', e)
252
+ throw ResilientError.ensure(e as Error)
253
+ }
254
+ }
255
+ await Promise.all(listeners.map(async listener => listener(msg)))
256
+ }
257
+ },
258
+
259
+ _receiveCall: async msg => {
260
+ let cancel = false
261
+ callPerformers[msg.method](...msg.payload)
262
+ .then(async result => {
263
+ if (cancel) {
264
+ return
265
+ }
266
+ clearTimeout(timeout)
267
+ const resultMsg: Message<any> = {
268
+ id: msg.id,
269
+ type: MessageType.Result,
270
+ payload: result
271
+ }
272
+ await conn.send(resultMsg)
273
+ }).catch(async error => {
274
+ console.error('Error during the call:', error)
275
+ if (cancel) {
276
+ return
277
+ }
278
+ clearTimeout(timeout)
279
+ const response: Message<any> = {
280
+ id: msg.id,
281
+ type: MessageType.Error,
282
+ payload: ResilientError.ensure(error).marshal().message
283
+ }
284
+ await conn.send(response)
285
+ })
286
+ msg.timeout = msg.timeout ?? conn.defaultCallTimeout ?? CALL_TIMEOUT
287
+ const timeout = setTimeout(() => { if (msg.timeout !== 0) cancel = true }, msg.timeout)
288
+ },
289
+
290
+ _receiveResult: async msg => {
291
+ if (msg.id == null) {
292
+ throw new SocketMessageMalformed('result:id')
293
+ }
294
+ if (calls[msg.id] == null) {
295
+ throw new SocketTimeout('result')
296
+ }
297
+ calls[msg.id].resolve(msg.payload)
298
+ delete calls[msg.id]
299
+ },
300
+
301
+ _receiveError: async msg => {
302
+ if (msg.id == null) {
303
+ throw new SocketMessageMalformed('result:id')
304
+ }
305
+ if (calls[msg.id] == null) {
306
+ throw new SocketTimeout('result')
307
+ }
308
+ calls[msg.id].reject(ResilientError.ensure(msg.payload))
309
+ delete calls[msg.id]
310
+ },
311
+
312
+ _receiveRequest: async msg => {
313
+ if (msg.id == null) {
314
+ throw new SocketMessageMalformed('request:id')
315
+ }
316
+ await requestAcknowledgers.reduce<Promise<boolean>>(
317
+ async (acknowledge, acknowledger) => {
318
+ if (await acknowledge) {
319
+ return acknowledge
320
+ }
321
+ return acknowledger(msg.id as string, msg.payload)
322
+ }, Promise.resolve(false)
323
+ )
324
+ },
325
+
326
+ _receiveResponse: async msg => {
327
+ if (msg.id == null) {
328
+ throw new SocketMessageMalformed('response:id')
329
+ }
330
+ if (observers[msg.id] == null) {
331
+ await observers[msg.id](msg.payload)
332
+ delete observers[msg.id]
333
+ }
334
+ await responseObservers.reduce<Promise<boolean>>(
335
+ async (captured, observer) => {
336
+ if (await captured) {
337
+ return captured
338
+ }
339
+ return observer(msg.payload)
340
+ }, Promise.resolve(false)
341
+ )
342
+ },
343
+
344
+ _receiveEvent: async msg => {
345
+ if (msg.event == null) {
346
+ throw new SocketMessageMalformed('event')
347
+ }
348
+ if (eventObservers[msg.event] == null) {
349
+ return
350
+ }
351
+ await Promise.all(eventObservers[msg.event].map(async observer => observer(msg)))
352
+ },
353
+
354
+ _receiveMessage: async msg => {
355
+ queue.push(msg)
356
+ },
357
+
358
+ getListeners: () => listeners
359
+ }
360
+
361
+ return conn
362
+ }
package/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type { ResilientError } from '@owlmeans/error'
2
+ import type { MessageType } from './consts.js'
3
+ import type { AuthenticationStage } from '@owlmeans/auth'
4
+
5
+ export interface Connection {
6
+ defaultCallTimeout?: number,
7
+
8
+ _authSequence?: CallResolver<any>
9
+
10
+ stage: AuthenticationStage
11
+
12
+ notify: <T>(event: string, payload: T) => Promise<void>
13
+ observe: <T>(event: string, handler: (event: EventMessage<T>) => Promise<void>) => () => void
14
+
15
+ call: <R, T extends any[]>(method: string, ...payload: T) => Promise<R>
16
+ perform: <R, T extends any[]>(method: string, handler: CallHendler<R, T>) => void
17
+
18
+ request: <T, R>(payload: T, observer?: (payload: R) => Promise<boolean>) => Promise<() => void>
19
+ observeResponse: <T>(observer: (payload: T) => Promise<boolean>) => Promise<() => void>
20
+ acknowledge: <T>(handler: RequestHandler<T>) => () => void
21
+ reply: <T>(id: string, payload: T) => Promise<void>
22
+
23
+ listen: (listner: ConnectionListener) => () => void
24
+
25
+ auth: <T, R>(stage: AuthenticationStage, payload: T) => Promise<R>
26
+
27
+ enqueue: <T>(payload: T, id?: string) => Promise<void>
28
+ enqueued: () => number
29
+ consume: <T>(filter?: (payload: T) => boolean) => [T | null, string | null | undefined, number] | null
30
+
31
+ /**
32
+ * @abstract
33
+ */
34
+ close: () => Promise<void>
35
+
36
+ /**
37
+ * Methods called by the socket connection
38
+ */
39
+ receive: (message: string) => Promise<void>
40
+ /**
41
+ * @abstract
42
+ */
43
+ send: (message: Message<any> | string) => Promise<void>
44
+ /**
45
+ * @abstract
46
+ */
47
+ prepare?: <T>(message: Message<T>, isRequest?: boolean) => Message<T>
48
+ /**
49
+ * @abstract
50
+ */
51
+ authenticate: AuthenticateMethod
52
+
53
+ /**
54
+ * Utility methods
55
+ */
56
+ _receiveCall: (message: CallMessage<any>) => Promise<void>
57
+ _receiveResult: (message: Message<any>) => Promise<void>
58
+ _receiveError: (message: Message<any>) => Promise<void>
59
+ _receiveRequest: (message: Message<any>) => Promise<void>
60
+ _receiveResponse: (message: Message<any>) => Promise<void>
61
+ _receiveEvent: (message: EventMessage<any>) => Promise<void>
62
+ _receiveMessage: (message: Message<any>) => Promise<void>
63
+
64
+ getListeners: () => ConnectionListener[]
65
+ }
66
+
67
+ export interface ConnectionListener {
68
+ (message: Message<any> | string): Promise<void>
69
+ }
70
+
71
+ export interface CallHendler<R, T extends any[]> {
72
+ (...args: T): Promise<R>
73
+ }
74
+
75
+ export interface RequestHandler<T> {
76
+ (id: string, payload: T): Promise<boolean>
77
+ }
78
+
79
+ export interface Message<T> {
80
+ type: MessageType
81
+ id?: string
82
+ rawData?: string
83
+ sender?: string
84
+ recipient?: string
85
+ dt?: number,
86
+ payload: T
87
+ }
88
+
89
+ export interface AuthenticateMethod {
90
+ <T, R>(stage: AuthenticationStage, payload: T): Promise<[AuthenticationStage | null, R]>
91
+ }
92
+
93
+ export interface CallMessage<T extends any[]> extends Message<T> {
94
+ type: MessageType.Call
95
+ method: string
96
+ timeout?: number
97
+ }
98
+
99
+ export interface EventMessage<T> extends Message<T> {
100
+ type: MessageType.Event | MessageType.System
101
+ event: string
102
+ }
103
+
104
+ export interface AuthMessage<T> extends Message<T> {
105
+ type: MessageType.Auth
106
+ stage: AuthenticationStage
107
+ }
108
+
109
+ export interface CallResolver<T> {
110
+ resolve: (result: T) => void
111
+ reject: (error: ResilientError) => void
112
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": [
3
+ "../tsconfig.default.json",
4
+ "../tsconfig.react.json",
5
+ ],
6
+ "compilerOptions": {
7
+ "rootDir": "./src/", /* Specify the root folder within your source files. */
8
+ "outDir": "./build/", /* Specify an output folder for all emitted files. */
9
+ },
10
+ "exclude": [
11
+ "./dist/**/*",
12
+ "./build/**/*",
13
+ "./*.ts"
14
+ ]
15
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/consts.ts","./src/errors.ts","./src/helper.ts","./src/index.ts","./src/model.ts","./src/types.ts"],"version":"5.6.3"}