mockaton 12.7.0 → 12.7.1

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.
@@ -53,6 +53,7 @@ const renameInStaticMocksDir = (src, target) => rename(join(staticDir, src), joi
53
53
 
54
54
  const api = new Commander(serverAddr)
55
55
 
56
+
56
57
  /** @returns {Promise<State>} */
57
58
  async function fetchState() {
58
59
  return (await api.getState()).json()
@@ -78,14 +79,14 @@ class BaseFixture {
78
79
  }
79
80
 
80
81
  async register() {
81
- const nextVerPromise = api.getSyncVersion()
82
+ const nextVerPromise = resolveOnNextSyncVersion()
82
83
  await this.#nextMacroTask()
83
84
  await this.write()
84
85
  await nextVerPromise
85
86
  }
86
87
 
87
88
  async unregister() {
88
- const nextVerPromise = api.getSyncVersion()
89
+ const nextVerPromise = resolveOnNextSyncVersion()
89
90
  await this.#nextMacroTask()
90
91
  await this.unlink()
91
92
  await nextVerPromise
@@ -1109,38 +1110,30 @@ describe('Registering Mocks', () => {
1109
1110
 
1110
1111
  describe('getSyncVersion', () => {
1111
1112
  const fx0 = new Fixture('reg0/runtime0.GET.200.txt')
1113
+ let version
1112
1114
  before(async () => {
1113
1115
  await makeDirInMocks('reg0')
1114
1116
  await fx0.sync()
1115
- })
1116
-
1117
- let version
1118
-
1119
- test('getSyncVersion responds immediately when version mismatches', async () => {
1120
- const r = await api.getSyncVersion(-1)
1121
- version = await r.json()
1117
+ version = await resolveOnNextSyncVersion(-1)
1122
1118
  })
1123
1119
 
1124
1120
  const fx = new Fixture('runtime1.GET.200.txt')
1125
1121
  test('responds when a file is added', async () => {
1126
- const prom = api.getSyncVersion(version)
1122
+ const prom = resolveOnNextSyncVersion(version)
1127
1123
  await fx.write()
1128
- const r = await prom
1129
- equal(await r.json(), version + 1)
1124
+ equal(await prom, version + 1)
1130
1125
  })
1131
1126
 
1132
1127
  test('responds when a file is deleted', async () => {
1133
- const prom = api.getSyncVersion(version + 1)
1128
+ const prom = resolveOnNextSyncVersion(version + 1)
1134
1129
  await fx.unlink()
1135
- const r = await prom
1136
- equal(await r.json(), version + 2)
1130
+ equal(await prom, version + 2)
1137
1131
  })
1138
1132
 
1139
1133
  test('responds when dir is renamed', async () => {
1140
- const p0 = api.getSyncVersion(version + 2)
1134
+ const p0 = resolveOnNextSyncVersion(version + 2)
1141
1135
  await renameInMocksDir('reg0', 'reg1')
1142
- const r0 = await p0
1143
- equal(await r0.json(), version + 3)
1136
+ equal(await p0, version + 3)
1144
1137
 
1145
1138
  const s = await fetchState()
1146
1139
  equal(s.brokersByMethod.GET['/reg1/runtime0'].file, 'reg1/runtime0.GET.200.txt')
@@ -1185,38 +1178,30 @@ describe('Registering Static Mocks', () => {
1185
1178
 
1186
1179
  describe('getSyncVersion', () => {
1187
1180
  const fx0 = new FixtureStatic('reg0/static0.txt')
1181
+ let version
1188
1182
  before(async () => {
1189
1183
  await makeDirInStaticMocks('reg0')
1190
1184
  await fx0.sync()
1191
- })
1192
-
1193
- let version
1194
-
1195
- test('getSyncVersion responds immediately when version mismatches', async () => {
1196
- const r = await api.getSyncVersion(-1)
1197
- version = await r.json()
1185
+ version = await resolveOnNextSyncVersion(-1)
1198
1186
  })
1199
1187
 
1200
1188
  const fx = new FixtureStatic('static1.txt')
1201
1189
  test('responds when a file is added', async () => {
1202
- const prom = api.getSyncVersion(version)
1190
+ const prom = resolveOnNextSyncVersion(version)
1203
1191
  await fx.write()
1204
- const r = await prom
1205
- equal(await r.json(), version + 1)
1192
+ equal(await prom, version + 1)
1206
1193
  })
1207
1194
 
1208
1195
  test('responds when a file is deleted', async () => {
1209
- const prom = api.getSyncVersion(version + 1)
1196
+ const prom = resolveOnNextSyncVersion(version + 1)
1210
1197
  await fx.unlink()
1211
- const r = await prom
1212
- equal(await r.json(), version + 2)
1198
+ equal(await prom, version + 2)
1213
1199
  })
1214
1200
 
1215
1201
  test('responds when dir is renamed', async () => {
1216
- const p0 = api.getSyncVersion(version + 2)
1202
+ const p0 = resolveOnNextSyncVersion(version + 2)
1217
1203
  await renameInStaticMocksDir('reg0', 'reg1')
1218
- const r0 = await p0
1219
- equal(await r0.json(), version + 3)
1204
+ equal(await p0, version + 3)
1220
1205
 
1221
1206
  const s = await fetchState()
1222
1207
  equal(s.staticBrokers['/reg1/static0.txt'].route, '/reg1/static0.txt')
@@ -1228,3 +1213,29 @@ describe('Registering Static Mocks', () => {
1228
1213
  async function sleep(ms = 100) {
1229
1214
  await new Promise(resolve => setTimeout(resolve, ms))
1230
1215
  }
1216
+
1217
+
1218
+ /** In Node, there's no EventSource, so we work around it like this.
1219
+ * This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
1220
+ async function resolveOnNextSyncVersion(currSyncVer = undefined) {
1221
+ let skipFirst = currSyncVer === undefined
1222
+ const response = await api.getSyncVersion()
1223
+ const stream = response.body.pipeThrough(new TextDecoderStream())
1224
+ let buffer = ''
1225
+
1226
+ for await (const chunk of stream) {
1227
+ buffer += chunk
1228
+ const parts = buffer.split('\n\n')
1229
+ buffer = parts.pop() || ''
1230
+
1231
+ for (const event of parts)
1232
+ for (const line of event.split(/\r?\n/))
1233
+ if (line.startsWith('data:')) {
1234
+ const v = Number(line.slice(5).trim())
1235
+ if (skipFirst || v === currSyncVer)
1236
+ skipFirst = false
1237
+ else
1238
+ return v
1239
+ }
1240
+ }
1241
+ }
@@ -2,11 +2,6 @@ import { join } from 'node:path'
2
2
  import { watch } from 'node:fs'
3
3
  import { EventEmitter } from 'node:events'
4
4
 
5
- import {
6
- HEADER_SYNC_VERSION,
7
- LONG_POLL_SERVER_TIMEOUT
8
- } from '../client/ApiConstants.js'
9
-
10
5
  import { config } from './config.js'
11
6
  import { isFile, isDirectory } from './utils/fs.js'
12
7
 
@@ -33,7 +28,7 @@ const uiSyncVersion = new class extends EventEmitter {
33
28
  })
34
29
 
35
30
  subscribe(listener) {
36
- this.once('ARR', listener)
31
+ this.on('ARR', listener)
37
32
  }
38
33
  unsubscribe(listener) {
39
34
  this.removeListener('ARR', listener)
@@ -98,24 +93,33 @@ export function watchStaticDir() {
98
93
  }
99
94
 
100
95
 
96
+
101
97
  /** Realtime notify ARR Events */
102
- export function longPollClientSyncVersion(req, response) {
103
- const clientVersion = req.headers[HEADER_SYNC_VERSION]
104
- if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
105
- // e.g., tab was hidden while new mocks were added or removed
106
- response.json(uiSyncVersion.version)
107
- return
98
+ export function sseClientSyncVersion(req, response) {
99
+ response.writeHead(200, {
100
+ 'Content-Type': 'text/event-stream',
101
+ 'Cache-Control': 'no-cache',
102
+ 'Connection': 'keep-alive',
103
+ })
104
+ response.flushHeaders()
105
+
106
+ function sendVersion() {
107
+ response.write(`data: ${uiSyncVersion.version}\n\n`)
108
108
  }
109
- function onARR() {
110
- uiSyncVersion.unsubscribe(onARR)
111
- response.json(uiSyncVersion.version)
109
+
110
+ sendVersion()
111
+ uiSyncVersion.subscribe(sendVersion)
112
+
113
+ const keepAlive = setInterval(() => {
114
+ response.write(': ping\n\n')
115
+ }, 10_000)
116
+
117
+ req.on('close', cleanup)
118
+ req.on('error', cleanup)
119
+ function cleanup() {
120
+ clearInterval(keepAlive)
121
+ uiSyncVersion.unsubscribe(sendVersion)
112
122
  }
113
- response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onARR)
114
- req.on('error', () => {
115
- uiSyncVersion.unsubscribe(onARR)
116
- response.destroy()
117
- })
118
- uiSyncVersion.subscribe(onARR)
119
123
  }
120
124
 
121
125
 
@@ -3,7 +3,6 @@ import { EventEmitter } from 'node:events'
3
3
  import { watch, readdirSync } from 'node:fs'
4
4
 
5
5
  import { config } from './config.js'
6
- import { LONG_POLL_SERVER_TIMEOUT } from '../client/ApiConstants.js'
7
6
 
8
7
 
9
8
  export const CLIENT_DIR = join(import.meta.dirname, '../client')
@@ -12,7 +11,7 @@ export const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR)
12
11
 
13
12
  const devClientWatcher = new class extends EventEmitter {
14
13
  emit(file) { super.emit('RELOAD', file) }
15
- subscribe(listener) { this.once('RELOAD', listener) }
14
+ subscribe(listener) { this.on('RELOAD', listener) }
16
15
  unsubscribe(listener) { this.removeListener('RELOAD', listener) }
17
16
  }
18
17
 
@@ -27,23 +26,33 @@ export function watchDevSPA() {
27
26
 
28
27
 
29
28
  /** Realtime notify Dev UI changes */
30
- export function longPollDevClientHotReload(req, response) {
29
+ export function sseClientHotReload(req, response) {
31
30
  if (!config.hotReload) {
32
31
  response.notFound()
33
32
  return
34
33
  }
35
34
 
36
- function onDevChange(file) {
37
- devClientWatcher.unsubscribe(onDevChange)
38
- response.json(file)
39
- }
40
- response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
41
- devClientWatcher.unsubscribe(onDevChange)
42
- response.json('')
43
- })
44
- req.on('error', () => {
45
- devClientWatcher.unsubscribe(onDevChange)
46
- response.destroy()
35
+ response.writeHead(200, {
36
+ 'Content-Type': 'text/event-stream',
37
+ 'Cache-Control': 'no-cache',
38
+ 'Connection': 'keep-alive',
47
39
  })
40
+ response.flushHeaders()
41
+
42
+ function onDevChange(file = '') {
43
+ response.write(`data: ${file}\n\n`)
44
+ }
45
+
48
46
  devClientWatcher.subscribe(onDevChange)
47
+
48
+ const keepAlive = setInterval(() => {
49
+ response.write(': ping\n\n')
50
+ }, 10_000)
51
+
52
+ req.on('close', cleanup)
53
+ req.on('error', cleanup)
54
+ function cleanup() {
55
+ clearInterval(keepAlive)
56
+ devClientWatcher.unsubscribe(onDevChange)
57
+ }
49
58
  }
package/src/server/cli.js CHANGED
@@ -78,7 +78,7 @@ else {
78
78
  : {}
79
79
 
80
80
  if (args.host) opts.host = args.host
81
- if (args.port) opts.port = Number(args.port)
81
+ if (args.port) opts.port = Number.isNaN(Number(args.port)) ? args.port : Number(args.port)
82
82
 
83
83
  if (args['mocks-dir']) opts.mocksDir = args['mocks-dir']
84
84
  if (args['static-dir']) opts.staticDir = args['static-dir']
@@ -5,12 +5,12 @@ import { describe, test } from 'node:test'
5
5
 
6
6
  import pkgJSON from '../../package.json' with { type: 'json' }
7
7
 
8
- const cli = (...args) => spawnSync(join(import.meta.dirname, 'cli.js'), args, {
9
- encoding: 'utf8'
10
- })
8
+
9
+ const rel = f => join(import.meta.dirname, f)
10
+ const cli = (...args) => spawnSync(rel('cli.js'), args, { encoding: 'utf8' })
11
11
 
12
12
  describe('CLI', () => {
13
- test('--invalid-flag', () => {
13
+ test('invalid flag', () => {
14
14
  const { stderr, status } = cli('--invalid-flag')
15
15
  equal(stderr.trim(), `Unknown option '--invalid-flag'`)
16
16
  equal(status, 1)
@@ -22,6 +22,15 @@ describe('CLI', () => {
22
22
  equal(status, 1)
23
23
  })
24
24
 
25
+ test('invalid port', () => {
26
+ const { stderr, status } = cli(
27
+ '--mocks-dir', rel('../../mockaton-mocks'),
28
+ '--port', 'not-a-number',
29
+ )
30
+ equal(stderr.trim(), `port="not-a-number" is invalid`)
31
+ equal(status, 1)
32
+ })
33
+
25
34
  test('-v outputs version from package.json', () => {
26
35
  const { stdout, status } = cli('-v')
27
36
  equal(stdout.trim(), pkgJSON.version)
@@ -7,41 +7,46 @@ describe('validate', () => {
7
7
  describe('optional', () => {
8
8
  test('accepts undefined', () =>
9
9
  doesNotThrow(() =>
10
- validate({}, { field: optional(Number.isInteger) })))
10
+ validate({}, { foo: optional(Number.isInteger) })))
11
11
 
12
12
  test('accepts falsy value regardless of type', () =>
13
13
  doesNotThrow(() =>
14
- validate({ field: 0 }, { field: optional(Array.isArray) })))
14
+ validate({ foo: 0 }, { foo: optional(Array.isArray) })))
15
15
 
16
16
  test('accepts when tester func returns truthy', () =>
17
17
  doesNotThrow(() =>
18
- validate({ field: [] }, { field: optional(Array.isArray) })))
18
+ validate({ foo: [] }, { foo: optional(Array.isArray) })))
19
19
 
20
20
  test('rejects when tester func returns falsy', () =>
21
21
  throws(() =>
22
- validate({ field: 1 }, { field: optional(Array.isArray) }),
23
- /field=1 is invalid/))
22
+ validate({ foo: 1 }, { foo: optional(Array.isArray) }),
23
+ /foo=1 is invalid/))
24
24
  })
25
25
 
26
26
  describe('is', () => {
27
27
  test('rejects mismatched type', () =>
28
28
  throws(() =>
29
- validate({ field: 1 }, { field: is(String) }),
30
- /field=1 is invalid/))
29
+ validate({ foo: 1 }, { foo: is(String) }),
30
+ /foo=1 is invalid/))
31
31
 
32
32
  test('accepts matched type', () =>
33
33
  doesNotThrow(() =>
34
- validate({ field: '' }, { field: is(String) })))
34
+ validate({ foo: '' }, { foo: is(String) })))
35
35
  })
36
36
 
37
37
  describe('custom tester func', () => {
38
38
  test('rejects mismatched type', () =>
39
39
  throws(() =>
40
- validate({ field: 0.1 }, { field: Number.isInteger }),
41
- /field=0.1 is invalid/))
40
+ validate({ foo: 'not-a-number' }, { foo: n => n > 1 }),
41
+ /foo="not-a-number" is invalid/))
42
+
43
+ test('rejects mismatched type', () =>
44
+ throws(() =>
45
+ validate({ foo: 0 }, { foo: n => n > 1 }),
46
+ /foo=0 is invalid/))
42
47
 
43
48
  test('accepts matched type', () =>
44
49
  doesNotThrow(() =>
45
- validate({ field: 1 }, { field: Number.isInteger })))
50
+ validate({ foo: 1 }, { foo: Number.isInteger })))
46
51
  })
47
52
  })