@platformatic/itc 2.0.0-alpha.7 → 2.0.0-alpha.9
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 +2 -2
- package/index.js +1 -3
- package/lib/errors.js +12 -44
- package/lib/itc.js +190 -105
- package/package.json +2 -2
- package/test/helper.js +0 -31
- package/test/itc.test.js +0 -442
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
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
|
-
|
|
10
|
-
|
|
11
|
-
),
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
),
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
try {
|
|
186
|
+
this.#enableKeepAlive()
|
|
49
187
|
|
|
50
|
-
|
|
188
|
+
const request = generateRequest(name, message)
|
|
51
189
|
|
|
52
|
-
|
|
190
|
+
this._send(request)
|
|
53
191
|
|
|
54
|
-
|
|
192
|
+
const responsePromise = once(this.#requestEmitter, request.reqId).then(([response]) => response)
|
|
55
193
|
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
278
|
+
request = parseRequest(raw)
|
|
133
279
|
handler = this.#handlers.get(request.name)
|
|
134
280
|
|
|
135
|
-
if (handler
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
289
|
+
response = generateResponse(request, null)
|
|
290
|
+
}
|
|
141
291
|
} catch (error) {
|
|
142
292
|
if (!request) {
|
|
143
|
-
response =
|
|
293
|
+
response = generateUnhandledErrorResponse(error)
|
|
144
294
|
} else if (!handler) {
|
|
145
|
-
response =
|
|
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 =
|
|
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 =
|
|
317
|
+
response = parseResponse(response)
|
|
181
318
|
} catch (error) {
|
|
182
|
-
response =
|
|
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
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
#
|
|
205
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "2.0.0-alpha.9",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Matteo Collina <hello@matteocollina.com>",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typescript": "^5.5.4"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@fastify/error": "^
|
|
25
|
+
"@fastify/error": "^4.0.0",
|
|
26
26
|
"@watchable/unpromise": "^1.0.2"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
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
|
-
})
|