@platformatic/itc 2.0.0-alpha.7 → 2.0.0-alpha.8

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/README.md CHANGED
@@ -17,7 +17,7 @@ const { port1, port2 } = new MessageChannel()
17
17
 
18
18
 
19
19
  // thread 1
20
- const itc1 = new ITC({ port: port1 })
20
+ const itc1 = new ITC({ port: port1, name: 'thread-1' })
21
21
 
22
22
  itc1.handle('get-users', async (request) => {
23
23
  return [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
@@ -26,7 +26,7 @@ itc1.handle('get-users', async (request) => {
26
26
  itc1.listen()
27
27
 
28
28
  // thread 2
29
- const itc2 = new ITC({ port: port2 })
29
+ const itc2 = new ITC({ port: port2, name: 'thread-2' })
30
30
  itc2.listen()
31
31
 
32
32
  const users = await itc2.send('get-users')
package/index.js CHANGED
@@ -1,5 +1,3 @@
1
1
  'use strict'
2
2
 
3
- const ITC = require('./lib/itc')
4
-
5
- module.exports = { ITC }
3
+ module.exports = require('./lib/itc')
package/lib/errors.js CHANGED
@@ -5,52 +5,20 @@ const createError = require('@fastify/error')
5
5
  const ERROR_PREFIX = 'PLT_ITC'
6
6
 
7
7
  module.exports = {
8
- HandlerFailed: createError(
9
- `${ERROR_PREFIX}_HANDLER_FAILED`,
10
- 'Handler failed with error: %s'
11
- ),
12
- HandlerNotFound: createError(
13
- `${ERROR_PREFIX}_HANDLER_NOT_FOUND`,
14
- 'Handler not found for request: "%s"'
15
- ),
16
- PortAlreadyListening: createError(
17
- `${ERROR_PREFIX}_ALREADY_LISTENING`,
18
- 'ITC is already listening'
19
- ),
20
- SendBeforeListen: createError(
21
- `${ERROR_PREFIX}_SEND_BEFORE_LISTEN`,
22
- 'ITC cannot send requests before listening'
23
- ),
24
- InvalidRequestVersion: createError(
25
- `${ERROR_PREFIX}_INVALID_REQUEST_VERSION`,
26
- 'Invalid ITC request version: "%s"'
27
- ),
28
- InvalidResponseVersion: createError(
29
- `${ERROR_PREFIX}_INVALID_RESPONSE_VERSION`,
30
- 'Invalid ITC response version: "%s"'
31
- ),
32
- MissingRequestName: createError(
33
- `${ERROR_PREFIX}_MISSING_REQUEST_NAME`,
34
- 'ITC request name is missing'
35
- ),
36
- MissingResponseName: createError(
37
- `${ERROR_PREFIX}_MISSING_RESPONSE_NAME`,
38
- 'ITC response name is missing'
39
- ),
40
- MissingRequestReqId: createError(
41
- `${ERROR_PREFIX}_MISSING_REQUEST_REQ_ID`,
42
- 'ITC request reqId is missing'
43
- ),
44
- MissingResponseReqId: createError(
45
- `${ERROR_PREFIX}_MISSING_RESPONSE_REQ_ID`,
46
- 'ITC response reqId is missing'
47
- ),
8
+ HandlerFailed: createError(`${ERROR_PREFIX}_HANDLER_FAILED`, 'Handler failed with error: %s'),
9
+ HandlerNotFound: createError(`${ERROR_PREFIX}_HANDLER_NOT_FOUND`, 'Handler not found for request: "%s"'),
10
+ PortAlreadyListening: createError(`${ERROR_PREFIX}_ALREADY_LISTENING`, 'ITC is already listening'),
11
+ SendBeforeListen: createError(`${ERROR_PREFIX}_SEND_BEFORE_LISTEN`, 'ITC cannot send requests before listening'),
12
+ InvalidRequestVersion: createError(`${ERROR_PREFIX}_INVALID_REQUEST_VERSION`, 'Invalid ITC request version: "%s"'),
13
+ InvalidResponseVersion: createError(`${ERROR_PREFIX}_INVALID_RESPONSE_VERSION`, 'Invalid ITC response version: "%s"'),
14
+ MissingRequestName: createError(`${ERROR_PREFIX}_MISSING_REQUEST_NAME`, 'ITC request name is missing'),
15
+ MissingResponseName: createError(`${ERROR_PREFIX}_MISSING_RESPONSE_NAME`, 'ITC response name is missing'),
16
+ MissingRequestReqId: createError(`${ERROR_PREFIX}_MISSING_REQUEST_REQ_ID`, 'ITC request reqId is missing'),
17
+ MissingResponseReqId: createError(`${ERROR_PREFIX}_MISSING_RESPONSE_REQ_ID`, 'ITC response reqId is missing'),
48
18
  RequestNameIsNotString: createError(
49
19
  `${ERROR_PREFIX}_REQUEST_NAME_IS_NOT_STRING`,
50
20
  'ITC request name is not a string: "%s"'
51
21
  ),
52
- MessagePortClosed: createError(
53
- `${ERROR_PREFIX}_MESSAGE_PORT_CLOSED`,
54
- 'ITC MessagePort is closed'
55
- ),
22
+ MessagePortClosed: createError(`${ERROR_PREFIX}_MESSAGE_PORT_CLOSED`, 'ITC MessagePort is closed'),
23
+ MissingName: createError(`${ERROR_PREFIX}_MISSING_NAME`, 'ITC name is missing')
56
24
  }
package/lib/itc.js CHANGED
@@ -11,6 +11,116 @@ const PLT_ITC_NOTIFICATION_TYPE = 'PLT_ITC_NOTIFICATION'
11
11
  const PLT_ITC_UNHANDLED_ERROR_TYPE = 'PLT_ITC_UNHANDLED_ERROR'
12
12
  const PLT_ITC_VERSION = '1.0.0'
13
13
 
14
+ function parseRequest (request) {
15
+ if (request.reqId === undefined) {
16
+ throw new errors.MissingRequestReqId()
17
+ }
18
+ if (request.version !== PLT_ITC_VERSION) {
19
+ throw new errors.InvalidRequestVersion(request.version)
20
+ }
21
+ if (request.name === undefined) {
22
+ throw new errors.MissingRequestName()
23
+ }
24
+ return request
25
+ }
26
+
27
+ function parseResponse (response) {
28
+ if (response.reqId === undefined) {
29
+ throw new errors.MissingResponseReqId()
30
+ }
31
+ if (response.version !== PLT_ITC_VERSION) {
32
+ throw new errors.InvalidResponseVersion(response.version)
33
+ }
34
+ if (response.name === undefined) {
35
+ throw new errors.MissingResponseName()
36
+ }
37
+ return response
38
+ }
39
+
40
+ function generateRequest (name, data) {
41
+ if (typeof name !== 'string') {
42
+ throw new errors.RequestNameIsNotString(name.toString())
43
+ }
44
+
45
+ return {
46
+ type: PLT_ITC_REQUEST_TYPE,
47
+ version: PLT_ITC_VERSION,
48
+ reqId: randomUUID(),
49
+ name,
50
+ data: sanitize(data)
51
+ }
52
+ }
53
+
54
+ function generateResponse (request, error, data) {
55
+ return {
56
+ type: PLT_ITC_RESPONSE_TYPE,
57
+ version: PLT_ITC_VERSION,
58
+ reqId: request.reqId,
59
+ name: request.name,
60
+ error,
61
+ data: sanitize(data)
62
+ }
63
+ }
64
+
65
+ function generateNotification (name, data) {
66
+ return {
67
+ type: PLT_ITC_NOTIFICATION_TYPE,
68
+ version: PLT_ITC_VERSION,
69
+ name,
70
+ data: sanitize(data)
71
+ }
72
+ }
73
+
74
+ function generateUnhandledErrorResponse (error) {
75
+ return {
76
+ type: PLT_ITC_UNHANDLED_ERROR_TYPE,
77
+ version: PLT_ITC_VERSION,
78
+ error,
79
+ data: null
80
+ }
81
+ }
82
+
83
+ function sanitize (data) {
84
+ if (!data || typeof data !== 'object') {
85
+ return data
86
+ }
87
+
88
+ let sanitized
89
+
90
+ if (Buffer.isBuffer(data)) {
91
+ return {
92
+ data: Array.from(data.values())
93
+ }
94
+ } else if (Array.isArray(data)) {
95
+ sanitized = []
96
+
97
+ for (const value of data) {
98
+ const valueType = typeof value
99
+
100
+ /* c8 ignore next 3 */
101
+ if (valueType === 'function' || valueType === 'symbol') {
102
+ continue
103
+ }
104
+
105
+ sanitized.push(value && typeof value === 'object' ? sanitize(value) : value)
106
+ }
107
+ } else {
108
+ sanitized = {}
109
+
110
+ for (const [key, value] of Object.entries(data)) {
111
+ const valueType = typeof value
112
+
113
+ if (valueType === 'function' || valueType === 'symbol') {
114
+ continue
115
+ }
116
+
117
+ sanitized[key] = value && typeof value === 'object' ? sanitize(value) : value
118
+ }
119
+ }
120
+
121
+ return sanitized
122
+ }
123
+
14
124
  class ITC extends EventEmitter {
15
125
  #requestEmitter
16
126
  #handlers
@@ -18,20 +128,47 @@ class ITC extends EventEmitter {
18
128
  #handling
19
129
  #closePromise
20
130
  #closeAfterCurrentRequest
131
+ #throwOnMissingHandler
132
+ #keepAlive
133
+ #keepAliveCount
21
134
 
22
- constructor ({ port, handlers }) {
135
+ constructor ({ port, handlers, throwOnMissingHandler, name }) {
23
136
  super()
24
137
 
138
+ if (!name) {
139
+ throw new errors.MissingName()
140
+ }
141
+
142
+ // The name property is useful only for debugging purposes.
143
+ // Without it, it's impossible to know which "side" of the ITC is being used.
144
+ this.name = name
25
145
  this.port = port
26
146
  this.#requestEmitter = new EventEmitter()
27
147
  this.#handlers = new Map()
28
148
  this.#listening = false
29
149
  this.#handling = false
30
150
  this.#closeAfterCurrentRequest = false
151
+ this.#throwOnMissingHandler = throwOnMissingHandler ?? true
31
152
 
32
153
  // Make sure the emitter handle a lot of listeners at once before raising a warning
33
154
  this.#requestEmitter.setMaxListeners(1e3)
34
155
 
156
+ /*
157
+ There some contexts in which a message is sent and the event loop empties up while waiting for a response.
158
+ For instance @platformatic/astro when doing build with custom commands.
159
+
160
+ The interval below is immediately unref() after creation.
161
+ Everytime a message is sent and awaiting for a response we ref() it.
162
+ We unref() it again as soon as the response is received.
163
+ This ensures the event loop stays up as intended.
164
+ */
165
+ /* c8 ignore next 4 */
166
+ this.#keepAlive = setInterval(() => {
167
+ // Debugging line used to know who is not closing the ITC
168
+ // process._rawDebug('Keep alive', this.name, this.#keepAliveCount)
169
+ }, 10000).unref()
170
+ this.#keepAliveCount = 0
171
+
35
172
  // Register handlers provided with the constructor
36
173
  if (typeof handlers === 'object') {
37
174
  for (const [name, fn] of Object.entries(handlers)) {
@@ -45,20 +182,26 @@ class ITC extends EventEmitter {
45
182
  throw new errors.SendBeforeListen()
46
183
  }
47
184
 
48
- const request = this.#generateRequest(name, message)
185
+ try {
186
+ this.#enableKeepAlive()
49
187
 
50
- this._send(request)
188
+ const request = generateRequest(name, message)
51
189
 
52
- const responsePromise = once(this.#requestEmitter, request.reqId).then(([response]) => response)
190
+ this._send(request)
53
191
 
54
- const { error, data } = await Unpromise.race([responsePromise, this.#closePromise])
192
+ const responsePromise = once(this.#requestEmitter, request.reqId).then(([response]) => response)
55
193
 
56
- if (error !== null) throw error
57
- return data
194
+ const { error, data } = await Unpromise.race([responsePromise, this.#closePromise])
195
+
196
+ if (error !== null) throw error
197
+ return data
198
+ } finally {
199
+ this.#manageKeepAlive()
200
+ }
58
201
  }
59
202
 
60
203
  async notify (name, message) {
61
- this._send(this.#generateNotification(name, message))
204
+ this._send(generateNotification(name, message))
62
205
  }
63
206
 
64
207
  handle (message, handler) {
@@ -92,6 +235,8 @@ class ITC extends EventEmitter {
92
235
  this.#closePromise = this._createClosePromise().then(() => {
93
236
  this.#listening = false
94
237
  const error = new errors.MessagePortClosed()
238
+ clearInterval(this.#keepAlive)
239
+ this.#keepAliveCount = -1000
95
240
  return { error, data: null }
96
241
  })
97
242
  }
@@ -118,7 +263,8 @@ class ITC extends EventEmitter {
118
263
  }
119
264
 
120
265
  _close () {
121
- this.port.close()
266
+ clearTimeout(this.#keepAlive)
267
+ this.port?.close?.()
122
268
  }
123
269
 
124
270
  async #handleRequest (raw) {
@@ -129,27 +275,31 @@ class ITC extends EventEmitter {
129
275
  this.#handling = true
130
276
 
131
277
  try {
132
- request = this.#parseRequest(raw)
278
+ request = parseRequest(raw)
133
279
  handler = this.#handlers.get(request.name)
134
280
 
135
- if (handler === undefined) {
136
- throw new errors.HandlerNotFound(request.name)
137
- }
281
+ if (handler) {
282
+ const result = await handler(request.data)
283
+ response = generateResponse(request, null, result)
284
+ } else {
285
+ if (this.#throwOnMissingHandler) {
286
+ throw new errors.HandlerNotFound(request.name)
287
+ }
138
288
 
139
- const result = await handler(request.data)
140
- response = this.#generateResponse(request, null, result)
289
+ response = generateResponse(request, null)
290
+ }
141
291
  } catch (error) {
142
292
  if (!request) {
143
- response = this.#generateUnhandledErrorResponse(error)
293
+ response = generateUnhandledErrorResponse(error)
144
294
  } else if (!handler) {
145
- response = this.#generateResponse(request, error, null)
295
+ response = generateResponse(request, error, null)
146
296
  } else {
147
297
  const failedError = new errors.HandlerFailed(error.message)
148
298
  failedError.handlerError = error
149
299
  // This is needed as the code might be lost when sending the message over the port
150
300
  failedError.handlerErrorCode = error.code
151
301
 
152
- response = this.#generateResponse(request, failedError, null)
302
+ response = generateResponse(request, failedError, null)
153
303
  }
154
304
  } finally {
155
305
  this.#handling = false
@@ -162,24 +312,11 @@ class ITC extends EventEmitter {
162
312
  }
163
313
  }
164
314
 
165
- #parseRequest (request) {
166
- if (request.reqId === undefined) {
167
- throw new errors.MissingRequestReqId()
168
- }
169
- if (request.version !== PLT_ITC_VERSION) {
170
- throw new errors.InvalidRequestVersion(request.version)
171
- }
172
- if (request.name === undefined) {
173
- throw new errors.MissingRequestName()
174
- }
175
- return request
176
- }
177
-
178
315
  #handleResponse (response) {
179
316
  try {
180
- response = this.#parseResponse(response)
317
+ response = parseResponse(response)
181
318
  } catch (error) {
182
- response = this.#generateUnhandledErrorResponse(error)
319
+ response = generateUnhandledErrorResponse(error)
183
320
  this._send(response)
184
321
  return
185
322
  }
@@ -188,83 +325,31 @@ class ITC extends EventEmitter {
188
325
  this.#requestEmitter.emit(reqId, response)
189
326
  }
190
327
 
191
- #parseResponse (response) {
192
- if (response.reqId === undefined) {
193
- throw new errors.MissingResponseReqId()
194
- }
195
- if (response.version !== PLT_ITC_VERSION) {
196
- throw new errors.InvalidResponseVersion(response.version)
197
- }
198
- if (response.name === undefined) {
199
- throw new errors.MissingResponseName()
200
- }
201
- return response
328
+ #enableKeepAlive () {
329
+ this.#keepAlive.ref()
330
+ this.#keepAliveCount++
202
331
  }
203
332
 
204
- #generateRequest (name, data) {
205
- if (typeof name !== 'string') {
206
- throw new errors.RequestNameIsNotString(name.toString())
207
- }
208
-
209
- if (typeof data === 'object') {
210
- data = this.#sanitize(data)
211
- }
333
+ #manageKeepAlive () {
334
+ this.#keepAliveCount--
212
335
 
213
- return {
214
- type: PLT_ITC_REQUEST_TYPE,
215
- version: PLT_ITC_VERSION,
216
- reqId: randomUUID(),
217
- name,
218
- data
219
- }
220
- }
221
-
222
- #generateResponse (request, error, data) {
223
- return {
224
- type: PLT_ITC_RESPONSE_TYPE,
225
- version: PLT_ITC_VERSION,
226
- reqId: request.reqId,
227
- name: request.name,
228
- error,
229
- data
230
- }
231
- }
232
-
233
- #generateNotification (name, data) {
234
- return {
235
- type: PLT_ITC_NOTIFICATION_TYPE,
236
- version: PLT_ITC_VERSION,
237
- name,
238
- data
239
- }
240
- }
241
-
242
- #generateUnhandledErrorResponse (error) {
243
- return {
244
- type: PLT_ITC_UNHANDLED_ERROR_TYPE,
245
- version: PLT_ITC_VERSION,
246
- error,
247
- data: null
336
+ /* c8 ignore next 3 */
337
+ if (this.#keepAliveCount > 0) {
338
+ return
248
339
  }
249
- }
250
-
251
- #sanitize (data) {
252
- const sanitizedObject = {}
253
- for (const key in data) {
254
- const value = data[key]
255
- const type = typeof value
256
340
 
257
- if (type === 'object') {
258
- sanitizedObject[key] = this.#sanitize(value)
259
- continue
260
- }
261
-
262
- if (type !== 'function' && type !== 'symbol') {
263
- sanitizedObject[key] = value
264
- }
265
- }
266
- return sanitizedObject
341
+ this.#keepAlive.unref()
342
+ this.#keepAliveCount = 0
267
343
  }
268
344
  }
269
345
 
270
- module.exports = ITC
346
+ module.exports = {
347
+ ITC,
348
+ parseRequest,
349
+ parseResponse,
350
+ generateRequest,
351
+ generateResponse,
352
+ generateNotification,
353
+ generateUnhandledErrorResponse,
354
+ sanitize
355
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/itc",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": "Matteo Collina <hello@matteocollina.com>",
package/test/helper.js DELETED
@@ -1,31 +0,0 @@
1
- 'use strict'
2
-
3
- const { randomUUID } = require('node:crypto')
4
-
5
- function generateItcRequest (request) {
6
- return {
7
- type: 'PLT_ITC_REQUEST',
8
- reqId: randomUUID(),
9
- version: '1.0.0',
10
- name: 'test-command',
11
- data: { test: 'test-req-message' },
12
- ...request,
13
- }
14
- }
15
-
16
- function generateItcResponse (response) {
17
- return {
18
- type: 'PLT_ITC_RESPONSE',
19
- reqId: randomUUID(),
20
- version: '1.0.0',
21
- name: 'test-command',
22
- error: null,
23
- data: { test: 'test-req-message' },
24
- ...response,
25
- }
26
- }
27
-
28
- module.exports = {
29
- generateItcRequest,
30
- generateItcResponse,
31
- }
package/test/itc.test.js DELETED
@@ -1,442 +0,0 @@
1
- 'use strict'
2
-
3
- const assert = require('node:assert/strict')
4
- const { once } = require('node:events')
5
- const { test } = require('node:test')
6
- const { setTimeout: sleep } = require('node:timers/promises')
7
- const { MessageChannel } = require('node:worker_threads')
8
- const { ITC } = require('../index.js')
9
- const { generateItcRequest, generateItcResponse } = require('./helper.js')
10
-
11
- test('should send a request between threads', async t => {
12
- const { port1, port2 } = new MessageChannel()
13
-
14
- const itc1 = new ITC({ port: port1 })
15
- const itc2 = new ITC({ port: port2 })
16
-
17
- const requestName = 'test-command'
18
- const testRequest = { test: 'test-req-message' }
19
- const testResponse = { test: 'test-res-message' }
20
-
21
- const requests = []
22
- itc2.handle(requestName, async request => {
23
- requests.push(request)
24
- return testResponse
25
- })
26
-
27
- itc1.listen()
28
- itc2.listen()
29
-
30
- t.after(() => itc1.close())
31
- t.after(() => itc2.close())
32
-
33
- const response = await itc1.send(requestName, testRequest)
34
- assert.deepStrictEqual(response, testResponse)
35
- assert.deepStrictEqual(requests, [testRequest])
36
- })
37
-
38
- test('should support close while replying to a message', async t => {
39
- const { port1, port2 } = new MessageChannel()
40
-
41
- const requestName = 'test-command'
42
- const testRequest = { test: 'test-req-message' }
43
- const testResponse = { test: 'test-res-message' }
44
-
45
- const requests = []
46
-
47
- const itc1 = new ITC({ port: port1 })
48
- const itc2 = new ITC({
49
- port: port2,
50
- handlers: {
51
- [requestName] (request) {
52
- requests.push(request)
53
- itc2.close()
54
- return testResponse
55
- }
56
- }
57
- })
58
-
59
- itc2.handle()
60
-
61
- itc1.listen()
62
- itc2.listen()
63
-
64
- t.after(() => itc1.close())
65
- t.after(() => itc2.close())
66
-
67
- const response = await itc1.send(requestName, testRequest)
68
- assert.deepStrictEqual(response, testResponse)
69
- assert.deepStrictEqual(requests, [testRequest])
70
- })
71
-
72
- test('should throw an error if send req before listen', async t => {
73
- const { port1 } = new MessageChannel()
74
-
75
- const itc = new ITC({ port: port1 })
76
- t.after(() => itc.close())
77
-
78
- try {
79
- await itc.send('test', 'test-request')
80
- assert.fail('Expected an error to be thrown')
81
- } catch (error) {
82
- assert.strictEqual(error.code, 'PLT_ITC_SEND_BEFORE_LISTEN')
83
- assert.strictEqual(error.message, 'ITC cannot send requests before listening')
84
- }
85
- })
86
-
87
- test('should throw an error if request name is not a string', async t => {
88
- const { port1 } = new MessageChannel()
89
-
90
- const itc = new ITC({ port: port1 })
91
- t.after(() => itc.close())
92
-
93
- itc.listen()
94
-
95
- try {
96
- await itc.send(true, 'test-request')
97
- assert.fail('Expected an error to be thrown')
98
- } catch (error) {
99
- assert.strictEqual(error.code, 'PLT_ITC_REQUEST_NAME_IS_NOT_STRING')
100
- assert.strictEqual(error.message, 'ITC request name is not a string: "true"')
101
- }
102
- })
103
-
104
- test('should send a notification between threads', async t => {
105
- const { port1, port2 } = new MessageChannel()
106
-
107
- const itc1 = new ITC({ port: port1 })
108
- const itc2 = new ITC({ port: port2 })
109
-
110
- const notificationName = 'notification'
111
- const testNotification = { test: 'test-notification' }
112
-
113
- t.after(() => itc2.close())
114
- await itc2.listen()
115
-
116
- await itc1.notify(notificationName, testNotification)
117
- const [receivedNotification] = await once(itc2, notificationName)
118
- assert.deepStrictEqual(testNotification, receivedNotification)
119
- })
120
-
121
- test('should throw if call listen twice', async t => {
122
- const { port1 } = new MessageChannel()
123
-
124
- const itc = new ITC({ port: port1 })
125
- t.after(() => itc.close())
126
-
127
- itc.listen()
128
-
129
- try {
130
- itc.listen()
131
- assert.fail('Expected an error to be thrown')
132
- } catch (error) {
133
- assert.strictEqual(error.code, 'PLT_ITC_ALREADY_LISTENING')
134
- assert.strictEqual(error.message, 'ITC is already listening')
135
- }
136
- })
137
-
138
- test('should throw an error if handler fails', async t => {
139
- const { port1, port2 } = new MessageChannel()
140
-
141
- const itc1 = new ITC({ port: port1 })
142
- const itc2 = new ITC({ port: port2 })
143
-
144
- const requestName = 'test-command'
145
-
146
- itc2.handle(requestName, async () => {
147
- throw new Error('test-error')
148
- })
149
-
150
- itc1.listen()
151
- itc2.listen()
152
-
153
- t.after(() => itc1.close())
154
- t.after(() => itc2.close())
155
-
156
- try {
157
- await itc1.send(requestName, 'test-request')
158
- assert.fail('Expected an error to be thrown')
159
- } catch (error) {
160
- assert.strictEqual(error.code, 'PLT_ITC_HANDLER_FAILED')
161
- assert.strictEqual(error.message, 'Handler failed with error: test-error')
162
- assert.strictEqual(error.handlerError.message, 'test-error')
163
- }
164
- })
165
-
166
- test('should throw if handler is not found', async t => {
167
- const { port1, port2 } = new MessageChannel()
168
-
169
- const itc1 = new ITC({ port: port1 })
170
- const itc2 = new ITC({ port: port2 })
171
-
172
- const requestName = 'test-command'
173
-
174
- itc1.listen()
175
- itc2.listen()
176
-
177
- t.after(() => itc1.close())
178
- t.after(() => itc2.close())
179
-
180
- try {
181
- await itc1.send(requestName, 'test-request')
182
- assert.fail('Expected an error to be thrown')
183
- } catch (error) {
184
- assert.strictEqual(error.code, 'PLT_ITC_HANDLER_NOT_FOUND')
185
- assert.strictEqual(error.message, 'Handler not found for request: "test-command"')
186
- }
187
- })
188
-
189
- test('should skip non-platformatic message', async t => {
190
- const { port1, port2 } = new MessageChannel()
191
-
192
- const itc1 = new ITC({ port: port1 })
193
- const itc2 = new ITC({ port: port2 })
194
-
195
- const requests = []
196
- itc1.handle('test', async request => {
197
- requests.push(request)
198
- })
199
-
200
- itc1.listen()
201
- itc2.listen()
202
-
203
- t.after(() => itc1.close())
204
- t.after(() => itc2.close())
205
-
206
- port2.postMessage({ type: 'test' })
207
- port2.postMessage({ type: 'platformatic' })
208
- port2.postMessage({ type: 'test' })
209
-
210
- await itc2.send('test', 'test-message')
211
-
212
- assert.deepStrictEqual(requests, ['test-message'])
213
- })
214
-
215
- test('should emit unhandledError if request version is wrong', (t, done) => {
216
- const { port1, port2 } = new MessageChannel()
217
-
218
- const itc1 = new ITC({ port: port1 })
219
- const itc2 = new ITC({ port: port2 })
220
-
221
- t.after(() => itc1.close())
222
- t.after(() => itc2.close())
223
-
224
- itc1.listen()
225
- itc2.listen()
226
-
227
- itc2.on('unhandledError', error => {
228
- assert.strictEqual(error.code, 'PLT_ITC_INVALID_REQUEST_VERSION')
229
- assert.strictEqual(error.message, 'Invalid ITC request version: "0.0.0"')
230
- done()
231
- })
232
-
233
- const itcRequest = generateItcRequest({ version: '0.0.0' })
234
- port2.postMessage(itcRequest)
235
- })
236
-
237
- test('should emit unhandledError if request reqId is missing', (t, done) => {
238
- const { port1, port2 } = new MessageChannel()
239
-
240
- const itc1 = new ITC({ port: port1 })
241
- const itc2 = new ITC({ port: port2 })
242
-
243
- t.after(() => itc1.close())
244
- t.after(() => itc2.close())
245
-
246
- itc1.listen()
247
- itc2.listen()
248
-
249
- itc2.on('unhandledError', error => {
250
- assert.strictEqual(error.code, 'PLT_ITC_MISSING_REQUEST_REQ_ID')
251
- assert.strictEqual(error.message, 'ITC request reqId is missing')
252
- done()
253
- })
254
-
255
- const itcRequest = generateItcRequest()
256
- delete itcRequest.reqId
257
-
258
- port2.postMessage(itcRequest)
259
- })
260
-
261
- test('should emit unhandledError if request name is missing', (t, done) => {
262
- const { port1, port2 } = new MessageChannel()
263
-
264
- const itc1 = new ITC({ port: port1 })
265
- const itc2 = new ITC({ port: port2 })
266
-
267
- t.after(() => itc1.close())
268
- t.after(() => itc2.close())
269
-
270
- itc1.listen()
271
- itc2.listen()
272
-
273
- itc2.on('unhandledError', error => {
274
- assert.strictEqual(error.code, 'PLT_ITC_MISSING_REQUEST_NAME')
275
- assert.strictEqual(error.message, 'ITC request name is missing')
276
- done()
277
- })
278
-
279
- const itcRequest = generateItcRequest()
280
- delete itcRequest.name
281
-
282
- port2.postMessage(itcRequest)
283
- })
284
-
285
- test('should emit unhandledError if response version is wrong', (t, done) => {
286
- const { port1, port2 } = new MessageChannel()
287
-
288
- const itc1 = new ITC({ port: port1 })
289
- const itc2 = new ITC({ port: port2 })
290
-
291
- t.after(() => itc1.close())
292
- t.after(() => itc2.close())
293
-
294
- itc1.listen()
295
- itc2.listen()
296
-
297
- itc2.on('unhandledError', error => {
298
- assert.strictEqual(error.code, 'PLT_ITC_INVALID_RESPONSE_VERSION')
299
- assert.strictEqual(error.message, 'Invalid ITC response version: "0.0.0"')
300
- done()
301
- })
302
-
303
- const itcResponse = generateItcResponse({ version: '0.0.0' })
304
- port2.postMessage(itcResponse)
305
- })
306
-
307
- test('should emit unhandledError if response reqId is missing', (t, done) => {
308
- const { port1, port2 } = new MessageChannel()
309
-
310
- const itc1 = new ITC({ port: port1 })
311
- const itc2 = new ITC({ port: port2 })
312
-
313
- t.after(() => itc1.close())
314
- t.after(() => itc2.close())
315
-
316
- itc1.listen()
317
- itc2.listen()
318
-
319
- itc2.on('unhandledError', error => {
320
- assert.strictEqual(error.code, 'PLT_ITC_MISSING_RESPONSE_REQ_ID')
321
- assert.strictEqual(error.message, 'ITC response reqId is missing')
322
- done()
323
- })
324
-
325
- const itcResponse = generateItcResponse()
326
- delete itcResponse.reqId
327
-
328
- port2.postMessage(itcResponse)
329
- })
330
-
331
- test('should emit unhandledError if response name is missing', (t, done) => {
332
- const { port1, port2 } = new MessageChannel()
333
-
334
- const itc1 = new ITC({ port: port1 })
335
- const itc2 = new ITC({ port: port2 })
336
-
337
- t.after(() => itc1.close())
338
- t.after(() => itc2.close())
339
-
340
- itc1.listen()
341
- itc2.listen()
342
-
343
- itc2.on('unhandledError', error => {
344
- assert.strictEqual(error.code, 'PLT_ITC_MISSING_RESPONSE_NAME')
345
- assert.strictEqual(error.message, 'ITC response name is missing')
346
- done()
347
- })
348
-
349
- const itcResponse = generateItcResponse()
350
- delete itcResponse.name
351
-
352
- port2.postMessage(itcResponse)
353
- })
354
-
355
- test('should sanitize a request before sending', async t => {
356
- const { port1, port2 } = new MessageChannel()
357
-
358
- const itc1 = new ITC({ port: port1 })
359
- const itc2 = new ITC({ port: port2 })
360
-
361
- const requestName = 'test-command'
362
- const testRequest = {
363
- test: 'test-req-message',
364
- foo: () => {},
365
- bar: Symbol('test'),
366
- nested: {
367
- test: 'test-req-message',
368
- foo: () => {},
369
- bar: Symbol('test')
370
- }
371
- }
372
- const testResponse = { test: 'test-res-message' }
373
-
374
- const requests = []
375
- itc2.handle(requestName, async request => {
376
- requests.push(request)
377
- return testResponse
378
- })
379
-
380
- itc1.listen()
381
- itc2.listen()
382
-
383
- t.after(() => itc1.close())
384
- t.after(() => itc2.close())
385
-
386
- const response = await itc1.send(requestName, testRequest)
387
- assert.deepStrictEqual(response, testResponse)
388
- assert.deepStrictEqual(requests, [
389
- {
390
- test: 'test-req-message',
391
- nested: { test: 'test-req-message' }
392
- }
393
- ])
394
- })
395
-
396
- test('should throw if receiver ITC port was closed', async t => {
397
- const { port1, port2 } = new MessageChannel()
398
-
399
- const itc1 = new ITC({ port: port1 })
400
- const itc2 = new ITC({ port: port2 })
401
-
402
- const requestName = 'test-command'
403
- const testRequest = { test: 'test-req-message' }
404
-
405
- itc2.handle(requestName, async () => {
406
- await sleep(10000)
407
- assert.fail('Handler should not be called')
408
- })
409
-
410
- itc1.listen()
411
- itc2.listen()
412
-
413
- t.after(() => itc1.close())
414
- t.after(() => itc2.close())
415
-
416
- port2.close()
417
-
418
- try {
419
- await itc1.send(requestName, testRequest)
420
- assert.fail('Expected an error to be thrown')
421
- } catch (error) {
422
- assert.strictEqual(error.code, 'PLT_ITC_MESSAGE_PORT_CLOSED')
423
- assert.strictEqual(error.message, 'ITC MessagePort is closed')
424
- }
425
- })
426
-
427
- test('should throw if sender ITC port was closed', async t => {
428
- const { port1 } = new MessageChannel()
429
-
430
- const itc = new ITC({ port: port1 })
431
- itc.listen()
432
-
433
- port1.close()
434
-
435
- try {
436
- await itc.send('test-command', 'test-req-message')
437
- assert.fail('Expected an error to be thrown')
438
- } catch (error) {
439
- assert.strictEqual(error.code, 'PLT_ITC_MESSAGE_PORT_CLOSED')
440
- assert.strictEqual(error.message, 'ITC MessagePort is closed')
441
- }
442
- })