sounding 0.0.3 → 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/README.md +336 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +174 -25
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +26 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
const { normalizeResponse, resolveBaseUrl } = require('./create-request-client')
|
|
2
|
+
const { createSoundingError } = require('./create-error')
|
|
3
|
+
const { loadDependencyFromApp } = require('./resolve-dependency')
|
|
4
|
+
const { resolveAuthConfig } = require('./resolve-auth-config')
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
7
|
+
/** @typedef {import('./types').SoundingActor} SoundingActor */
|
|
8
|
+
/** @typedef {import('./types').SoundingConfig} SoundingConfig */
|
|
9
|
+
/** @typedef {import('./types').SoundingResponse} SoundingResponse */
|
|
10
|
+
/** @typedef {import('./types').SoundingSocketClient} SoundingSocketClient */
|
|
11
|
+
/** @typedef {import('./types').SoundingSocketConnectOptions} SoundingSocketConnectOptions */
|
|
12
|
+
/** @typedef {import('./types').SoundingSocketEvent} SoundingSocketEvent */
|
|
13
|
+
/** @typedef {import('./types').SoundingSocketManager} SoundingSocketManager */
|
|
14
|
+
/** @typedef {import('./types').SoundingSocketRequestOptions} SoundingSocketRequestOptions */
|
|
15
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
16
|
+
/** @typedef {import('./types').SoundingWorldEngine} SoundingWorldEngine */
|
|
17
|
+
|
|
18
|
+
const SAILS_IO_SDK_QUERY = {
|
|
19
|
+
__sails_io_sdk_version: '1.2.1',
|
|
20
|
+
__sails_io_sdk_platform: 'node',
|
|
21
|
+
__sails_io_sdk_language: 'javascript',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {any} value
|
|
26
|
+
* @returns {value is AnyRecord}
|
|
27
|
+
*/
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} value
|
|
34
|
+
* @returns {AnyRecord}
|
|
35
|
+
*/
|
|
36
|
+
function toHeaderObject(value) {
|
|
37
|
+
if (!value) {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof Headers !== 'undefined' && value instanceof Headers) {
|
|
42
|
+
return Object.fromEntries(value.entries())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return Object.fromEntries(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { ...value }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {{ sails?: SoundingSailsApp, getConfig?: () => SoundingConfig }} input
|
|
54
|
+
* @returns {SoundingConfig['sockets']}
|
|
55
|
+
*/
|
|
56
|
+
function resolveSocketConfig({ sails, getConfig }) {
|
|
57
|
+
const soundingConfig =
|
|
58
|
+
(typeof getConfig === 'function' ? getConfig() : null) || sails?.config?.sounding || {}
|
|
59
|
+
|
|
60
|
+
return soundingConfig.sockets || {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} appPath
|
|
65
|
+
* @param {{ resolveImplementation?: (moduleId: string, options?: { paths?: string[] }) => string }} [options]
|
|
66
|
+
* @returns {any}
|
|
67
|
+
*/
|
|
68
|
+
function defaultLoadSocketIoClient(appPath, options = {}) {
|
|
69
|
+
return loadDependencyFromApp({
|
|
70
|
+
appPath,
|
|
71
|
+
moduleId: 'socket.io-client',
|
|
72
|
+
purpose: 'run websocket trials',
|
|
73
|
+
install: 'npm install -D socket.io-client',
|
|
74
|
+
resolveImplementation: options.resolveImplementation,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {SoundingSailsApp | undefined} sails
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function hasSailsSocketSupport(sails) {
|
|
83
|
+
return Boolean(sails?.hooks?.sockets && sails?.io && sails?.sockets)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {{ appPath?: string }} input
|
|
88
|
+
* @returns {Error}
|
|
89
|
+
*/
|
|
90
|
+
function createSocketHookUnavailableError({ appPath }) {
|
|
91
|
+
return createSoundingError({
|
|
92
|
+
code: 'E_SOUNDING_SOCKET_HOOK_UNAVAILABLE',
|
|
93
|
+
name: 'SoundingSocketError',
|
|
94
|
+
message:
|
|
95
|
+
'Sounding websocket helpers need Sails socket support. Install and enable `sails-hook-sockets`, then lift the app with the sockets hook enabled.',
|
|
96
|
+
details: {
|
|
97
|
+
dependency: 'sails-hook-sockets',
|
|
98
|
+
install: 'npm install sails-hook-sockets',
|
|
99
|
+
appPath,
|
|
100
|
+
suggestion:
|
|
101
|
+
'If your test config disables `hooks.sockets`, set it to `true` for websocket trials.',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {{ code: string, event?: string, timeout?: number, cause?: unknown, message: string }} input
|
|
108
|
+
* @returns {Error}
|
|
109
|
+
*/
|
|
110
|
+
function createSocketError({ code, event, timeout, cause, message }) {
|
|
111
|
+
return createSoundingError({
|
|
112
|
+
code,
|
|
113
|
+
name: 'SoundingSocketError',
|
|
114
|
+
message,
|
|
115
|
+
details: {
|
|
116
|
+
event,
|
|
117
|
+
timeout,
|
|
118
|
+
},
|
|
119
|
+
cause,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {any} value
|
|
125
|
+
* @returns {any}
|
|
126
|
+
*/
|
|
127
|
+
function cloneJsonish(value) {
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
return value.map(cloneJsonish)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isPlainObject(value)) {
|
|
133
|
+
return Object.fromEntries(
|
|
134
|
+
Object.entries(value).map(([key, nested]) => [key, cloneJsonish(nested)])
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return value
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {any[]} args
|
|
143
|
+
* @returns {any}
|
|
144
|
+
*/
|
|
145
|
+
function normalizeEventPayload(args) {
|
|
146
|
+
if (args.length === 0) {
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (args.length === 1) {
|
|
151
|
+
return args[0]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return args
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {any} jwr
|
|
159
|
+
* @param {string} method
|
|
160
|
+
* @param {string} target
|
|
161
|
+
* @param {any} body
|
|
162
|
+
* @returns {SoundingResponse}
|
|
163
|
+
*/
|
|
164
|
+
function normalizeJwrResponse(jwr, method, target, body) {
|
|
165
|
+
const status = jwr?.statusCode === undefined ? 200 : jwr.statusCode
|
|
166
|
+
const headers = jwr?.headers || {}
|
|
167
|
+
const responseBody = jwr?.body === undefined ? body : jwr.body
|
|
168
|
+
|
|
169
|
+
return normalizeResponse({
|
|
170
|
+
raw: jwr,
|
|
171
|
+
status,
|
|
172
|
+
statusText: '',
|
|
173
|
+
headers,
|
|
174
|
+
url: target,
|
|
175
|
+
redirected: status >= 300 && status < 400,
|
|
176
|
+
responseBody,
|
|
177
|
+
request: {
|
|
178
|
+
method: method.toUpperCase(),
|
|
179
|
+
target,
|
|
180
|
+
transport: 'socket',
|
|
181
|
+
url: target,
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {SoundingSailsApp} sails
|
|
188
|
+
* @param {AnyRecord} session
|
|
189
|
+
* @returns {Promise<string | null>}
|
|
190
|
+
*/
|
|
191
|
+
async function createSessionCookie(sails, session) {
|
|
192
|
+
if (
|
|
193
|
+
!sails?.session ||
|
|
194
|
+
typeof sails.session.generateNewSidCookie !== 'function' ||
|
|
195
|
+
typeof sails.session.parseSessionIdFromCookie !== 'function' ||
|
|
196
|
+
typeof sails.session.set !== 'function'
|
|
197
|
+
) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const cookie = sails.session.generateNewSidCookie()
|
|
202
|
+
const sid = sails.session.parseSessionIdFromCookie(cookie)
|
|
203
|
+
|
|
204
|
+
await new Promise((resolve, reject) => {
|
|
205
|
+
sails.session.set(
|
|
206
|
+
sid,
|
|
207
|
+
{
|
|
208
|
+
cookie: {
|
|
209
|
+
httpOnly: true,
|
|
210
|
+
path: '/',
|
|
211
|
+
},
|
|
212
|
+
...session,
|
|
213
|
+
},
|
|
214
|
+
(error) => {
|
|
215
|
+
if (error) {
|
|
216
|
+
reject(error)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
resolve()
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
return cookie
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {any} socket
|
|
230
|
+
* @param {number} timeout
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
function waitForSocketConnect(socket, timeout) {
|
|
234
|
+
if (socket.connected) {
|
|
235
|
+
return Promise.resolve()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
let timer = null
|
|
240
|
+
|
|
241
|
+
function cleanup() {
|
|
242
|
+
clearTimeout(timer)
|
|
243
|
+
socket.off?.('connect', onConnect)
|
|
244
|
+
socket.off?.('connect_error', onError)
|
|
245
|
+
socket.off?.('error', onError)
|
|
246
|
+
socket.off?.('connect_timeout', onTimeout)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onConnect() {
|
|
250
|
+
cleanup()
|
|
251
|
+
resolve()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onTimeout() {
|
|
255
|
+
cleanup()
|
|
256
|
+
reject(
|
|
257
|
+
createSocketError({
|
|
258
|
+
code: 'E_SOUNDING_SOCKET_CONNECT_TIMEOUT',
|
|
259
|
+
timeout,
|
|
260
|
+
message: `Sounding socket did not connect within ${timeout}ms.`,
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onError(error) {
|
|
266
|
+
cleanup()
|
|
267
|
+
reject(
|
|
268
|
+
createSocketError({
|
|
269
|
+
code: 'E_SOUNDING_SOCKET_CONNECT_FAILED',
|
|
270
|
+
cause: error,
|
|
271
|
+
message: 'Sounding socket failed to connect.',
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
timer = setTimeout(onTimeout, timeout)
|
|
277
|
+
socket.on?.('connect', onConnect)
|
|
278
|
+
socket.on?.('connect_error', onError)
|
|
279
|
+
socket.on?.('error', onError)
|
|
280
|
+
socket.on?.('connect_timeout', onTimeout)
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {any} socket
|
|
286
|
+
* @param {{
|
|
287
|
+
* timeout: number,
|
|
288
|
+
* defaultHeaders?: AnyRecord,
|
|
289
|
+
* }} options
|
|
290
|
+
* @returns {SoundingSocketClient}
|
|
291
|
+
*/
|
|
292
|
+
function wrapSocket(socket, { timeout, defaultHeaders = {} }) {
|
|
293
|
+
/** @type {SoundingSocketEvent[]} */
|
|
294
|
+
const history = []
|
|
295
|
+
/** @type {SoundingSocketEvent[]} */
|
|
296
|
+
const buffer = []
|
|
297
|
+
/** @type {Array<{ event: string, resolve: (payload: any) => void, reject: (error: Error) => void, timer: NodeJS.Timeout }>} */
|
|
298
|
+
const waiters = []
|
|
299
|
+
let closed = false
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {string} event
|
|
303
|
+
* @param {any[]} args
|
|
304
|
+
*/
|
|
305
|
+
function recordEvent(event, args) {
|
|
306
|
+
const entry = {
|
|
307
|
+
event,
|
|
308
|
+
data: normalizeEventPayload(args),
|
|
309
|
+
args: args.map(cloneJsonish),
|
|
310
|
+
receivedAt: new Date().toISOString(),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
history.push(entry)
|
|
314
|
+
|
|
315
|
+
const waiterIndex = waiters.findIndex((waiter) => waiter.event === event)
|
|
316
|
+
if (waiterIndex === -1) {
|
|
317
|
+
buffer.push(entry)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const [waiter] = waiters.splice(waiterIndex, 1)
|
|
322
|
+
clearTimeout(waiter.timer)
|
|
323
|
+
waiter.resolve(entry.data)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function attachEventBuffer() {
|
|
327
|
+
if (typeof socket.onAny === 'function') {
|
|
328
|
+
socket.onAny((event, ...args) => {
|
|
329
|
+
recordEvent(event, args)
|
|
330
|
+
})
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const originalOnevent = socket.onevent
|
|
335
|
+
if (typeof originalOnevent === 'function') {
|
|
336
|
+
socket.onevent = function soundingOnevent(packet) {
|
|
337
|
+
const args = packet?.data || []
|
|
338
|
+
if (typeof args[0] === 'string') {
|
|
339
|
+
recordEvent(args[0], args.slice(1))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return originalOnevent.call(this, packet)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
attachEventBuffer()
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {string} method
|
|
351
|
+
* @param {string} target
|
|
352
|
+
* @param {any} [payload]
|
|
353
|
+
* @param {SoundingSocketRequestOptions} [options]
|
|
354
|
+
* @returns {Promise<SoundingResponse>}
|
|
355
|
+
*/
|
|
356
|
+
function send(method, target, payload, options = {}) {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const requestTimeout = options.timeout || timeout
|
|
359
|
+
const timer = setTimeout(() => {
|
|
360
|
+
reject(
|
|
361
|
+
createSocketError({
|
|
362
|
+
code: 'E_SOUNDING_SOCKET_REQUEST_TIMEOUT',
|
|
363
|
+
timeout: requestTimeout,
|
|
364
|
+
message: `Sounding socket request to ${target} did not complete within ${requestTimeout}ms.`,
|
|
365
|
+
})
|
|
366
|
+
)
|
|
367
|
+
}, requestTimeout)
|
|
368
|
+
|
|
369
|
+
const requestOptions = {
|
|
370
|
+
method,
|
|
371
|
+
url: target,
|
|
372
|
+
headers: {
|
|
373
|
+
...defaultHeaders,
|
|
374
|
+
...toHeaderObject(options.headers),
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (payload !== undefined) {
|
|
379
|
+
requestOptions.params = payload
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
socket.emit(method, requestOptions, (jwr) => {
|
|
384
|
+
clearTimeout(timer)
|
|
385
|
+
const body = jwr?.body
|
|
386
|
+
resolve(normalizeJwrResponse(jwr, method, target, body))
|
|
387
|
+
})
|
|
388
|
+
} catch (error) {
|
|
389
|
+
clearTimeout(timer)
|
|
390
|
+
reject(error)
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @type {SoundingSocketClient} */
|
|
396
|
+
const client = {
|
|
397
|
+
get id() {
|
|
398
|
+
return socket.id
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
get connected() {
|
|
402
|
+
return Boolean(socket.connected)
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
on(event, listener) {
|
|
406
|
+
socket.on(event, listener)
|
|
407
|
+
return client
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
off(event, listener) {
|
|
411
|
+
socket.off(event, listener)
|
|
412
|
+
return client
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
events(event) {
|
|
416
|
+
return history
|
|
417
|
+
.filter((entry) => (event ? entry.event === event : true))
|
|
418
|
+
.map((entry) => entry.data)
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
async receive(event, options = {}) {
|
|
422
|
+
const bufferedIndex = buffer.findIndex((entry) => entry.event === event)
|
|
423
|
+
if (bufferedIndex !== -1) {
|
|
424
|
+
const [entry] = buffer.splice(bufferedIndex, 1)
|
|
425
|
+
return entry.data
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const eventTimeout = options.timeout || timeout
|
|
429
|
+
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const timer = setTimeout(() => {
|
|
432
|
+
const waiterIndex = waiters.findIndex(
|
|
433
|
+
(waiter) => waiter.event === event && waiter.resolve === resolve
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if (waiterIndex !== -1) {
|
|
437
|
+
waiters.splice(waiterIndex, 1)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
reject(
|
|
441
|
+
createSocketError({
|
|
442
|
+
code: 'E_SOUNDING_SOCKET_EVENT_TIMEOUT',
|
|
443
|
+
event,
|
|
444
|
+
timeout: eventTimeout,
|
|
445
|
+
message: `Sounding socket did not receive \`${event}\` within ${eventTimeout}ms.`,
|
|
446
|
+
})
|
|
447
|
+
)
|
|
448
|
+
}, eventTimeout)
|
|
449
|
+
|
|
450
|
+
waiters.push({
|
|
451
|
+
event,
|
|
452
|
+
resolve,
|
|
453
|
+
reject,
|
|
454
|
+
timer,
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
async request(method, target, payloadOrOptions, maybeOptions) {
|
|
460
|
+
const normalizedMethod = method.toLowerCase()
|
|
461
|
+
const hasPayload = !['get', 'head'].includes(normalizedMethod)
|
|
462
|
+
const payload = hasPayload ? payloadOrOptions : undefined
|
|
463
|
+
const options = (hasPayload ? maybeOptions : payloadOrOptions) || {}
|
|
464
|
+
|
|
465
|
+
return send(normalizedMethod, target, payload, options)
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
async get(target, options) {
|
|
469
|
+
return send('get', target, undefined, options)
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
async head(target, options) {
|
|
473
|
+
return send('head', target, undefined, options)
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
async post(target, payload, options) {
|
|
477
|
+
return send('post', target, payload, options)
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
async put(target, payload, options) {
|
|
481
|
+
return send('put', target, payload, options)
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
async patch(target, payload, options) {
|
|
485
|
+
return send('patch', target, payload, options)
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
async delete(target, payload, options) {
|
|
489
|
+
return send('delete', target, payload, options)
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
async close() {
|
|
493
|
+
if (closed) {
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
closed = true
|
|
498
|
+
for (const waiter of waiters.splice(0)) {
|
|
499
|
+
clearTimeout(waiter.timer)
|
|
500
|
+
waiter.reject(
|
|
501
|
+
createSocketError({
|
|
502
|
+
code: 'E_SOUNDING_SOCKET_CLOSED',
|
|
503
|
+
event: waiter.event,
|
|
504
|
+
message: 'Sounding socket closed before the event was received.',
|
|
505
|
+
})
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
socket.disconnect()
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return client
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {{
|
|
518
|
+
* sails?: SoundingSailsApp,
|
|
519
|
+
* getConfig?: () => SoundingConfig,
|
|
520
|
+
* actor?: SoundingActor | null,
|
|
521
|
+
* options?: SoundingSocketConnectOptions,
|
|
522
|
+
* }} input
|
|
523
|
+
* @returns {Promise<{ headers: AnyRecord, initialConnectionHeaders: AnyRecord }>}
|
|
524
|
+
*/
|
|
525
|
+
async function resolveActorSocketOptions({ sails, getConfig, actor, options = {} }) {
|
|
526
|
+
const actorHeaders = toHeaderObject(actor?.sounding?.headers || actor?.headers)
|
|
527
|
+
const optionHeaders = toHeaderObject(options.headers)
|
|
528
|
+
const optionInitialHeaders = toHeaderObject(options.initialConnectionHeaders)
|
|
529
|
+
const actorSession =
|
|
530
|
+
actor?.sounding?.session ||
|
|
531
|
+
actor?.session ||
|
|
532
|
+
(actor?.id
|
|
533
|
+
? {
|
|
534
|
+
[resolveAuthConfig({ sails, getConfig }).sessionKey]: actor.id,
|
|
535
|
+
...(actor.team ? { teamId: actor.team } : {}),
|
|
536
|
+
...(actor.teamId ? { teamId: actor.teamId } : {}),
|
|
537
|
+
}
|
|
538
|
+
: {})
|
|
539
|
+
|
|
540
|
+
if (!optionInitialHeaders.cookie && Object.keys(actorSession).length > 0) {
|
|
541
|
+
const cookie = await createSessionCookie(sails, actorSession)
|
|
542
|
+
if (cookie) {
|
|
543
|
+
optionInitialHeaders.cookie = cookie
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
headers: {
|
|
549
|
+
...actorHeaders,
|
|
550
|
+
...optionHeaders,
|
|
551
|
+
},
|
|
552
|
+
initialConnectionHeaders: {
|
|
553
|
+
...optionInitialHeaders,
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @param {{
|
|
560
|
+
* actor?: SoundingActor | string | null,
|
|
561
|
+
* world?: SoundingWorldEngine,
|
|
562
|
+
* sails?: SoundingSailsApp,
|
|
563
|
+
* getConfig?: () => SoundingConfig,
|
|
564
|
+
* }} input
|
|
565
|
+
* @returns {SoundingActor | null}
|
|
566
|
+
*/
|
|
567
|
+
function resolveActor({ actor, world, sails, getConfig }) {
|
|
568
|
+
if (!actor) {
|
|
569
|
+
return null
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (typeof actor === 'object') {
|
|
573
|
+
return actor
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const auth = resolveAuthConfig({ sails, getConfig })
|
|
577
|
+
return (
|
|
578
|
+
world?.current?.[auth.worldCollection]?.[actor] ||
|
|
579
|
+
world?.current?.[actor] ||
|
|
580
|
+
null
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @param {{
|
|
586
|
+
* sails?: SoundingSailsApp,
|
|
587
|
+
* getConfig?: () => SoundingConfig,
|
|
588
|
+
* world?: SoundingWorldEngine,
|
|
589
|
+
* appPathResolver?: () => string,
|
|
590
|
+
* loadSocketIoClient?: (appPath: string) => any,
|
|
591
|
+
* }} input
|
|
592
|
+
* @returns {SoundingSocketManager}
|
|
593
|
+
*/
|
|
594
|
+
function createSocketManager({
|
|
595
|
+
sails,
|
|
596
|
+
getConfig,
|
|
597
|
+
world,
|
|
598
|
+
appPathResolver = () => sails?.config?.appPath || process.cwd(),
|
|
599
|
+
loadSocketIoClient = defaultLoadSocketIoClient,
|
|
600
|
+
} = {}) {
|
|
601
|
+
/** @type {Set<SoundingSocketClient>} */
|
|
602
|
+
const activeSockets = new Set()
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* @param {SoundingActor | string | null} actor
|
|
606
|
+
* @param {SoundingSocketConnectOptions} [options]
|
|
607
|
+
* @returns {Promise<SoundingSocketClient>}
|
|
608
|
+
*/
|
|
609
|
+
async function connectAs(actor, options = {}) {
|
|
610
|
+
const config = resolveSocketConfig({ sails, getConfig })
|
|
611
|
+
|
|
612
|
+
if (config.enabled === false) {
|
|
613
|
+
throw createSocketError({
|
|
614
|
+
code: 'E_SOUNDING_SOCKET_DISABLED',
|
|
615
|
+
message: 'Sounding socket helpers are disabled by `sounding.sockets.enabled`.',
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const timeout = options.timeout || config.timeout || 1000
|
|
620
|
+
const appPath = appPathResolver()
|
|
621
|
+
const socketClient = await loadSocketIoClient(appPath)
|
|
622
|
+
|
|
623
|
+
if (!hasSailsSocketSupport(sails)) {
|
|
624
|
+
throw createSocketHookUnavailableError({ appPath })
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const baseUrl = resolveBaseUrl({
|
|
628
|
+
sails,
|
|
629
|
+
getConfig,
|
|
630
|
+
override: options.baseUrl || config.baseUrl,
|
|
631
|
+
})
|
|
632
|
+
const resolvedActor = resolveActor({ actor, world, sails, getConfig })
|
|
633
|
+
const actorOptions = await resolveActorSocketOptions({
|
|
634
|
+
sails,
|
|
635
|
+
getConfig,
|
|
636
|
+
actor: resolvedActor,
|
|
637
|
+
options,
|
|
638
|
+
})
|
|
639
|
+
const connectionOptions = {
|
|
640
|
+
transports: options.transports || config.transports || ['websocket'],
|
|
641
|
+
path: options.path || config.path || '/socket.io',
|
|
642
|
+
timeout,
|
|
643
|
+
reconnection: false,
|
|
644
|
+
headers: {
|
|
645
|
+
...toHeaderObject(config.headers),
|
|
646
|
+
...actorOptions.headers,
|
|
647
|
+
},
|
|
648
|
+
initialConnectionHeaders: {
|
|
649
|
+
...toHeaderObject(config.initialConnectionHeaders),
|
|
650
|
+
...actorOptions.initialConnectionHeaders,
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const rawSocket = socketClient.io(baseUrl, {
|
|
655
|
+
transports: connectionOptions.transports,
|
|
656
|
+
path: connectionOptions.path,
|
|
657
|
+
timeout,
|
|
658
|
+
reconnection: false,
|
|
659
|
+
query: SAILS_IO_SDK_QUERY,
|
|
660
|
+
extraHeaders: connectionOptions.initialConnectionHeaders,
|
|
661
|
+
transportOptions: Object.fromEntries(
|
|
662
|
+
connectionOptions.transports.map((transport) => [
|
|
663
|
+
transport,
|
|
664
|
+
{
|
|
665
|
+
extraHeaders: connectionOptions.initialConnectionHeaders,
|
|
666
|
+
},
|
|
667
|
+
])
|
|
668
|
+
),
|
|
669
|
+
})
|
|
670
|
+
await waitForSocketConnect(rawSocket, timeout)
|
|
671
|
+
|
|
672
|
+
const socket = wrapSocket(rawSocket, {
|
|
673
|
+
timeout,
|
|
674
|
+
defaultHeaders: connectionOptions.headers,
|
|
675
|
+
})
|
|
676
|
+
activeSockets.add(socket)
|
|
677
|
+
return socket
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
connect(options) {
|
|
682
|
+
return connectAs(null, options)
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
as(actor) {
|
|
686
|
+
return {
|
|
687
|
+
connect(options) {
|
|
688
|
+
return connectAs(actor, options)
|
|
689
|
+
},
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
async closeAll() {
|
|
694
|
+
const sockets = Array.from(activeSockets)
|
|
695
|
+
activeSockets.clear()
|
|
696
|
+
await Promise.all(sockets.map((socket) => socket.close()))
|
|
697
|
+
},
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = {
|
|
702
|
+
createSocketHookUnavailableError,
|
|
703
|
+
createSocketManager,
|
|
704
|
+
defaultLoadSocketIoClient,
|
|
705
|
+
hasSailsSocketSupport,
|
|
706
|
+
}
|