mockaton 12.6.1 → 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.
package/src/client/app.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  createElement as r,
3
- createSvgElement as s,
4
3
  t, classNames, restoreFocus, Fragment, defineClassNames
5
4
  } from './dom-utils.js'
5
+
6
6
  import { store } from './app-store.js'
7
- import { PayloadViewer, previewMock } from './payload-viewer.js'
7
+ import { API } from './ApiConstants.js'
8
+ import { Header } from './app-header.js'
9
+ import { TimerIcon, CloudIcon } from './app-icons.js'
10
+ import { PayloadViewer, previewMock } from './app-payload-viewer.js'
8
11
 
9
12
  import CSS from './app.css' with { type: 'css' }
10
13
  document.adoptedStyleSheets.push(CSS)
@@ -15,14 +18,12 @@ store.onError = onError
15
18
  store.render = render
16
19
  store.renderRow = renderRow
17
20
 
18
- initRealTimeUpdates()
21
+ onRealTimeUpdate(store.fetchState)
19
22
  initKeyboardNavigation()
20
23
 
21
24
  let mounted = false
22
25
  function render() {
23
- restoreFocus(() => {
24
- document.body.replaceChildren(...App())
25
- })
26
+ restoreFocus(() => document.body.replaceChildren(App()))
26
27
  if (store.hasChosenLink)
27
28
  previewMock()
28
29
  mounted = true
@@ -32,183 +33,42 @@ function render() {
32
33
  const leftSideRef = {}
33
34
 
34
35
  function App() {
35
- return [
36
- Header(),
36
+ return Fragment(Header(), Main())
37
+ }
38
+
39
+
40
+ function Main() {
41
+ return (
37
42
  r('main', null,
38
43
  r('div', {
39
44
  ref: leftSideRef,
40
45
  style: { width: leftSideRef.width },
41
46
  className: CSS.leftSide
42
47
  },
43
- r('div', classNames(CSS.SubToolbar),
48
+ r('div', { className: CSS.SubToolbar },
44
49
  GroupByMethod(),
45
50
  BulkSelector()),
46
- r('div', classNames(CSS.Table),
51
+ r('div', { className: CSS.Table },
47
52
  MockList(),
48
53
  StaticFilesList())),
49
54
  r('div', { className: CSS.rightSide },
50
55
  Resizer(leftSideRef),
51
- PayloadViewer()))
52
- ]
53
- }
54
-
55
- function Header() {
56
- return (
57
- r('header', null,
58
- r('a', {
59
- className: CSS.Logo,
60
- href: 'https://mockaton.com',
61
- alt: t`Documentation`
62
- },
63
- Logo()),
64
- r('div', null,
65
- r('div', classNames(CSS.GlobalDelayWrap),
66
- GlobalDelayField(),
67
- GlobalDelayJitterField()),
68
- CookieSelector(),
69
- store.showProxyField && ProxyFallbackField(),
70
- ResetButton(),
71
- HelpLink()
72
- )))
73
- }
74
-
75
-
76
- function GlobalDelayField() {
77
- function onChange() {
78
- store.setGlobalDelay(this.valueAsNumber)
79
- }
80
- function onWheel(event) {
81
- if (event.deltaY > 0)
82
- this.stepUp()
83
- else
84
- this.stepDown()
85
- clearTimeout(onWheel.timer)
86
- onWheel.timer = setTimeout(onChange.bind(this), 300)
87
- }
88
- return (
89
- r('label', classNames(CSS.Field, CSS.GlobalDelayField),
90
- r('span', null, t`Delay (ms)`),
91
- r('input', {
92
- type: 'number',
93
- min: 0,
94
- step: 100,
95
- autocomplete: 'none',
96
- value: store.delay,
97
- onChange,
98
- onWheel: [onWheel, { passive: true }]
99
- })))
100
- }
101
-
102
- function GlobalDelayJitterField() {
103
- function onChange() {
104
- this.value = this.valueAsNumber.toFixed(0)
105
- this.value = Math.max(0, this.valueAsNumber)
106
- this.value = Math.min(300, this.valueAsNumber)
107
- store.setGlobalDelayJitter(this.valueAsNumber / 100)
108
- }
109
- function onWheel(event) {
110
- if (event.deltaY > 0)
111
- this.stepUp()
112
- else
113
- this.stepDown()
114
- clearTimeout(onWheel.timer)
115
- onWheel.timer = setTimeout(onChange.bind(this), 300)
116
- }
117
- return (
118
- r('label', classNames(CSS.Field, CSS.GlobalDelayJitterField),
119
- r('span', null, t`Max Jitter %`),
120
- r('input', {
121
- type: 'number',
122
- min: 0,
123
- max: 300,
124
- step: 10,
125
- autocomplete: 'none',
126
- value: (store.delayJitter * 100).toFixed(0),
127
- onChange,
128
- onWheel: [onWheel, { passive: true }]
129
- })))
130
- }
131
-
132
-
133
- function CookieSelector() {
134
- const { cookies } = store
135
- const disabled = cookies.length <= 1
136
- const list = cookies.length ? cookies : [[t`None`, true]]
137
- return (
138
- r('label', classNames(CSS.Field, CSS.CookieSelector),
139
- r('span', null, t`Cookie`),
140
- r('select', {
141
- autocomplete: 'off',
142
- disabled,
143
- title: disabled
144
- ? t`No cookies specified in config.cookies`
145
- : undefined,
146
- onChange() { store.selectCookie(this.value) }
147
- }, list.map(([value, selected]) =>
148
- r('option', { value, selected }, value)))))
56
+ PayloadViewer())))
149
57
  }
150
58
 
151
59
 
152
- function ProxyFallbackField() {
153
- const checkboxRef = {}
154
- function onChange() {
155
- checkboxRef.elem.disabled = !this.validity.valid || !this.value.trim()
156
- if (!this.validity.valid)
157
- this.reportValidity()
158
- else
159
- store.setProxyFallback(this.value.trim())
160
- }
161
- return (
162
- r('div', classNames(CSS.Field, CSS.FallbackBackend),
163
- r('label', null,
164
- r('span', null, t`Fallback`),
165
- r('input', {
166
- type: 'url',
167
- name: 'fallback',
168
- placeholder: t`Type backend address`,
169
- value: store.proxyFallback,
170
- onChange
171
- })),
172
- SaveProxiedCheckbox(checkboxRef)))
173
- }
174
-
175
- function SaveProxiedCheckbox(ref) {
60
+ function GroupByMethod() {
176
61
  return (
177
- r('label', classNames(CSS.SaveProxiedCheckbox),
62
+ r('label', { className: CSS.GroupByMethod },
178
63
  r('input', {
179
- ref,
180
64
  type: 'checkbox',
181
- disabled: !store.canProxy,
182
- checked: store.collectProxied,
183
- onChange() { store.setCollectProxied(this.checked) }
65
+ checked: store.groupByMethod,
66
+ onChange: store.toggleGroupByMethod
184
67
  }),
185
- r('span', classNames(CSS.checkboxBody), t`Save Mocks`)))
186
- }
187
-
188
-
189
- function ResetButton() {
190
- return (
191
- r('button', {
192
- className: CSS.ResetButton,
193
- onClick: store.reset
194
- }, t`Reset`))
195
- }
196
-
197
-
198
- function HelpLink() {
199
- return (
200
- r('a', {
201
- target: '_blank',
202
- href: 'https://mockaton.com',
203
- title: t`Documentation`,
204
- className: CSS.HelpLink
205
- }, HelpIcon()))
68
+ r('span', { className: CSS.checkboxBody }, t`Group by Method`)))
206
69
  }
207
70
 
208
71
 
209
- /** # Left Side */
210
-
211
-
212
72
  function BulkSelector() {
213
73
  const { comments } = store
214
74
  const firstOption = t`Pick Comment…`
@@ -219,7 +79,7 @@ function BulkSelector() {
219
79
  }
220
80
  const disabled = !comments.length
221
81
  return (
222
- r('label', classNames(CSS.BulkSelector),
82
+ r('label', { className: CSS.BulkSelector },
223
83
  r('span', null, t`Bulk Select`),
224
84
  r('select', {
225
85
  autocomplete: 'off',
@@ -232,23 +92,9 @@ function BulkSelector() {
232
92
  r('option', { value: firstOption }, firstOption),
233
93
  r('hr'),
234
94
  comments.map(value => r('option', { value }, value)))))
235
- // TODO For a11y, use `menu` instead of `select`
236
- }
237
-
238
-
239
- function GroupByMethod() {
240
- return (
241
- r('label', classNames(CSS.GroupByMethod),
242
- r('input', {
243
- type: 'checkbox',
244
- checked: store.groupByMethod,
245
- onChange: store.toggleGroupByMethod
246
- }),
247
- r('span', classNames(CSS.checkboxBody), t`Group by Method`)))
248
95
  }
249
96
 
250
97
 
251
- /** # MockList */
252
98
 
253
99
  function MockList() {
254
100
  if (!Object.keys(store.brokersByMethod).length)
@@ -257,7 +103,11 @@ function MockList() {
257
103
  if (store.groupByMethod)
258
104
  return Object.keys(store.brokersByMethod).map(method =>
259
105
  Fragment(
260
- r('div', classNames(CSS.TableHeading, store.canProxy && CSS.canProxy), method),
106
+ r('div', {
107
+ className: classNames(
108
+ CSS.TableHeading,
109
+ store.canProxy && CSS.canProxy)
110
+ }, method),
261
111
  store.brokersAsRowsByMethod(method).map(Row)))
262
112
 
263
113
  return store.brokersAsRowsByMethod('*').map(Row)
@@ -272,7 +122,8 @@ function Row(row, i) {
272
122
  return (
273
123
  r('div', {
274
124
  key: row.key,
275
- ...classNames(CSS.TableRow,
125
+ className: classNames(
126
+ CSS.TableRow,
276
127
  mounted && row.isNew && CSS.animIn)
277
128
  },
278
129
  store.canProxy && ProxyToggler(method, urlMask, row.proxied),
@@ -294,7 +145,7 @@ function Row(row, i) {
294
145
  }
295
146
  }),
296
147
 
297
- !store.groupByMethod && r('span', classNames(CSS.Method), method),
148
+ !store.groupByMethod && r('span', { className: CSS.Method }, method),
298
149
 
299
150
  PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0),
300
151
 
@@ -350,12 +201,14 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
350
201
  const [ditto, tail] = urlMaskDittoed
351
202
  return (
352
203
  r('a', {
353
- ...classNames(CSS.PreviewLink, isChosen && CSS.chosen),
204
+ className: classNames(
205
+ CSS.PreviewLink,
206
+ isChosen && CSS.chosen),
354
207
  href: urlMask,
355
208
  autofocus,
356
209
  onClick
357
210
  }, ditto
358
- ? [r('span', classNames(CSS.dittoDir), ditto), tail]
211
+ ? [r('span', { className: CSS.dittoDir }, ditto), tail]
359
212
  : tail))
360
213
  }
361
214
 
@@ -375,7 +228,7 @@ function MockSelector(row) {
375
228
  },
376
229
  'aria-label': t`Mock Selector`,
377
230
  disabled: row.opts.length < 2,
378
- ...classNames(
231
+ className: classNames(
379
232
  CSS.MockSelector,
380
233
  row.selectedIdx > 0 && CSS.nonDefault,
381
234
  row.selectedFileIs4xx && CSS.status4xx)
@@ -405,10 +258,11 @@ function StaticFilesList() {
405
258
  return !rows.length
406
259
  ? null
407
260
  : Fragment(
408
- r('div',
409
- classNames(CSS.TableHeading,
410
- store.canProxy && CSS.canProxy,
411
- !store.groupByMethod && CSS.nonGroupedByMethod),
261
+ r('div', {
262
+ className: classNames(CSS.TableHeading,
263
+ store.canProxy && CSS.canProxy,
264
+ !store.groupByMethod && CSS.nonGroupedByMethod),
265
+ },
412
266
  store.groupByMethod
413
267
  ? t`Static GET`
414
268
  : t`Static`),
@@ -422,7 +276,8 @@ function StaticRow(row) {
422
276
  return (
423
277
  r('div', {
424
278
  key: row.key,
425
- ...classNames(CSS.TableRow,
279
+ className: classNames(
280
+ CSS.TableRow,
426
281
  mounted && row.isNew && CSS.animIn)
427
282
  },
428
283
 
@@ -445,14 +300,14 @@ function StaticRow(row) {
445
300
  }
446
301
  }),
447
302
 
448
- !groupByMethod && r('span', classNames(CSS.Method), 'GET'),
303
+ !groupByMethod && r('span', { className: CSS.Method }, 'GET'),
449
304
 
450
305
  r('a', {
451
306
  href: row.urlMask,
452
307
  target: '_blank',
453
308
  className: CSS.PreviewLink,
454
309
  }, ditto
455
- ? [r('span', classNames(CSS.dittoDir), ditto), tail]
310
+ ? [r('span', { className: CSS.dittoDir }, ditto), tail]
456
311
  : tail)))
457
312
  }
458
313
 
@@ -472,7 +327,7 @@ function DelayToggler({ checked, commit, optClassName }) {
472
327
  canClickDrag: true,
473
328
  checked,
474
329
  commit,
475
- ...classNames(CSS.DelayToggler, optClassName),
330
+ className: classNames(CSS.DelayToggler, optClassName),
476
331
  title: t`Delay`,
477
332
  body: TimerIcon()
478
333
  })
@@ -481,13 +336,38 @@ function DelayToggler({ checked, commit, optClassName }) {
481
336
  function ClickDragToggler({ checked, commit, className, title, body }) {
482
337
  function onPointerEnter(event) {
483
338
  if (event.buttons === 1)
484
- onPointerDown.call(this)
339
+ onPointerDown.call(this, event)
485
340
  }
486
- function onPointerDown() {
341
+
342
+ function onPointerDown(event) {
343
+ if (event.altKey) {
344
+ onExclusiveClick.call(this)
345
+ return
346
+ }
487
347
  this.checked = !this.checked
488
348
  this.focus()
489
349
  commit(this.checked)
490
350
  }
351
+
352
+ function onExclusiveClick() {
353
+ const selector = selectorForColumnOf(this)
354
+ if (!selector)
355
+ return
356
+
357
+ // Uncheck all other in the column.
358
+ for (const elem of leftSideRef.elem.querySelectorAll(selector))
359
+ if (elem !== this && elem.checked && !elem.disabled) {
360
+ elem.checked = false
361
+ elem.dispatchEvent(new Event('change'))
362
+ }
363
+
364
+ if (!this.checked) {
365
+ this.checked = true
366
+ this.dispatchEvent(new Event('change'))
367
+ }
368
+ this.focus()
369
+ }
370
+
491
371
  function onClick(event) {
492
372
  if (event.pointerType === 'mouse')
493
373
  event.preventDefault()
@@ -496,7 +376,7 @@ function ClickDragToggler({ checked, commit, className, title, body }) {
496
376
  commit(this.checked)
497
377
  }
498
378
  return (
499
- r('label', { ...classNames(CSS.Toggler, className), title },
379
+ r('label', { className: classNames(CSS.Toggler, className), title },
500
380
  r('input', {
501
381
  type: 'checkbox',
502
382
  checked,
@@ -505,7 +385,7 @@ function ClickDragToggler({ checked, commit, className, title, body }) {
505
385
  onClick,
506
386
  onChange
507
387
  }),
508
- r('span', classNames(CSS.checkboxBody), body)))
388
+ r('span', { className: CSS.checkboxBody }, body)))
509
389
  }
510
390
 
511
391
  function Resizer(ref) {
@@ -596,89 +476,72 @@ ErrorToast.close = () => {
596
476
  }
597
477
 
598
478
 
599
- /** # Graphics */
600
-
601
- function Logo() {
602
- return (
603
- s('svg', { viewBox: '0 0 556 100' },
604
- s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
605
- s('path', { opacity: 0.7, d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
606
- s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' })))
607
- }
608
-
609
- function TimerIcon() {
610
- return (
611
- s('svg', { viewBox: '0 0 24 24' },
612
- s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' })))
613
- }
614
-
615
- function CloudIcon() {
616
- return (
617
- s('svg', { viewBox: '0 0 24 24' },
618
- s('path', { d: 'm6.1 8.9c0.98-2.3 3.3-3.9 6-3.9 3.3-2e-7 6 2.5 6.4 5.7 0.018 0.15 0.024 0.18 0.026 0.23 0.0016 0.037 8.2e-4 0.084 0.098 0.14 0.097 0.054 0.29 0.05 0.48 0.05 2.2 0 4 1.8 4 4s-1.8 4-4 4c-4-0.038-9-0.038-13-0.018-2.8 0-5-2.2-5-5-2.2e-7 -2.8 2.2-5 5-5 2.8 2e-7 5 2.2 5 5' }),
619
- s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' })))
620
- }
621
-
622
- function HelpIcon() {
623
- return (
624
- s('svg', { viewBox: '0 0 24 24' },
625
- s('path', { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25' })))
626
- }
627
-
628
479
 
629
- /**
630
- * # Long polls UI sync version
631
- * The version increments when a mock file is added, removed, or renamed.
632
- */
633
- function initRealTimeUpdates() {
480
+ /** The version increments when a mock file is added, removed, or renamed. */
481
+ function onRealTimeUpdate(onUpdate) {
634
482
  let oldVersion = -1
635
- let controller = new AbortController()
483
+ let es = null
484
+ let timer = null
636
485
 
637
- longPoll()
486
+ connect()
638
487
  document.addEventListener('visibilitychange', () => {
639
- if (document.hidden) {
640
- controller.abort('_hidden_tab_')
641
- controller = new AbortController()
642
- }
488
+ if (document.hidden)
489
+ teardown()
643
490
  else
644
- longPoll()
491
+ connect()
645
492
  })
493
+ window.addEventListener('beforeunload', teardown)
646
494
 
647
- async function longPoll() {
648
- try {
649
- const response = await store.getSyncVersion(oldVersion, controller.signal)
650
- if (!response.ok)
651
- throw response.status
495
+ function connect() {
496
+ if (es) return
652
497
 
498
+ clearTimeout(timer)
499
+ es = new EventSource(API.syncVersion)
500
+
501
+ es.onmessage = function (event) {
653
502
  if (ErrorToast.isOffline)
654
503
  ErrorToast.close()
655
504
 
656
- const version = await response.json()
657
- if (oldVersion !== version) { // because it could be < or >
505
+ const version = Number(event.data)
506
+ if (oldVersion !== version) {
658
507
  oldVersion = version
659
- store.fetchState()
508
+ onUpdate()
660
509
  }
661
- longPoll()
662
510
  }
663
- catch (error) {
664
- if (error !== '_hidden_tab_')
665
- setTimeout(longPoll, 3000)
511
+
512
+ es.onerror = function () {
513
+ teardown()
514
+ timer = setTimeout(connect, 3000)
666
515
  }
667
516
  }
517
+
518
+ function teardown() {
519
+ clearTimeout(timer)
520
+ es?.close()
521
+ es = null
522
+ }
668
523
  }
669
524
 
670
525
 
671
- function initKeyboardNavigation() {
672
- const columnSelectors = [
526
+
527
+ function selectorForColumnOf(elem) {
528
+ return columnSelectors().find(s => elem?.matches(s))
529
+ }
530
+
531
+ function columnSelectors() {
532
+ return [
673
533
  `.${CSS.TableRow} .${CSS.ProxyToggler} input`,
674
534
  `.${CSS.TableRow} .${CSS.DelayToggler} input`,
675
535
  `.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
676
536
  `.${CSS.TableRow} .${CSS.PreviewLink}`,
677
537
  // No .MockSelector because down/up arrows have native behavior on them
678
538
  ]
539
+ }
679
540
 
541
+
542
+ function initKeyboardNavigation() {
680
543
  const rowSelectors = [
681
- ...columnSelectors,
544
+ ...columnSelectors(),
682
545
  `.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
683
546
  ]
684
547
 
@@ -687,7 +550,7 @@ function initKeyboardNavigation() {
687
550
  case 'ArrowDown':
688
551
  case 'ArrowUp': {
689
552
  const pivot = document.activeElement
690
- const sel = columnSelectors.find(s => pivot?.matches(s))
553
+ const sel = selectorForColumnOf(pivot)
691
554
  if (sel) {
692
555
  const offset = key === 'ArrowDown' ? +1 : -1
693
556
  const siblings = leftSideRef.elem.querySelectorAll(sel)
@@ -714,4 +577,3 @@ function initKeyboardNavigation() {
714
577
  return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
715
578
  }
716
579
  }
717
-
@@ -3,9 +3,7 @@ export function t(translation) {
3
3
  }
4
4
 
5
5
  export function classNames(...args) {
6
- return {
7
- className: args.filter(Boolean).join(' ')
8
- }
6
+ return args.filter(Boolean).join(' ')
9
7
  }
10
8
 
11
9
  export function createElement(tag, props, ...children) {
@@ -1,6 +1,11 @@
1
1
  import { test } from 'node:test'
2
- import { deepEqual } from 'node:assert/strict'
3
- import { extractClassNames } from './dom-utils.js'
2
+ import { deepEqual, equal } from 'node:assert/strict'
3
+ import { extractClassNames, classNames } from './dom-utils.js'
4
+
5
+
6
+ test('classNames', () => {
7
+ equal(classNames('a', false && 'b'), 'a')
8
+ })
4
9
 
5
10
 
6
11
  const cssRules = [
@@ -1,29 +1,38 @@
1
1
  import { API } from './ApiConstants.js'
2
2
 
3
3
 
4
- longPollDevChanges()
5
- async function longPollDevChanges() {
6
- try {
7
- const response = await fetch(API.watchHotReload)
8
- if (!response.ok)
9
- throw response.statusText
10
-
11
- const file = await response.json() || ''
12
- if (file.endsWith('.css')) {
13
- await hotReloadCSS(file)
14
- longPollDevChanges()
15
- }
4
+ let es = null
5
+ let timer = null
6
+
7
+ window.addEventListener('beforeunload', teardown)
8
+ connect()
9
+ function connect() {
10
+ if (es) return
11
+
12
+ clearTimeout(timer)
13
+ es = new EventSource(API.watchHotReload)
14
+
15
+ es.onmessage = function (event) {
16
+ const file = event.data
17
+ if (file.endsWith('.css'))
18
+ hotReloadCSS(file)
16
19
  else if (file)
17
20
  location.reload()
18
- else // server timeout
19
- longPollDevChanges()
20
21
  }
21
- catch (error) {
22
- console.error('hot reload', error?.message || error)
23
- setTimeout(longPollDevChanges, 3000)
22
+
23
+ es.onerror = function () {
24
+ console.error('hot reload')
25
+ teardown()
26
+ timer = setTimeout(connect, 3000)
24
27
  }
25
28
  }
26
29
 
30
+ function teardown() {
31
+ clearTimeout(timer)
32
+ es?.close()
33
+ es = null
34
+ }
35
+
27
36
  async function hotReloadCSS(file) {
28
37
  const mod = await import(`./${file}?${Date.now()}`, { with: { type: 'css' } })
29
38
  document.adoptedStyleSheets = [mod.default]
package/src/server/Api.js CHANGED
@@ -6,11 +6,11 @@
6
6
  import { join } from 'node:path'
7
7
 
8
8
  import {
9
- longPollDevClientHotReload,
9
+ sseClientHotReload,
10
10
  DASHBOARD_ASSETS,
11
11
  CLIENT_DIR
12
12
  } from './WatcherDevClient.js'
13
- import { longPollClientSyncVersion, startWatchers, stopWatchers } from './Watcher.js'
13
+ import { startWatchers, stopWatchers, sseClientSyncVersion } from './Watcher.js'
14
14
 
15
15
  import pkgJSON from '../../package.json' with { type: 'json' }
16
16
 
@@ -29,16 +29,16 @@ export const apiGetReqs = new Map([
29
29
  ...DASHBOARD_ASSETS.map(f => [API.dashboard + '/' + f, serveStatic(f)]),
30
30
 
31
31
  [API.state, getState],
32
- [API.syncVersion, longPollClientSyncVersion],
32
+ [API.syncVersion, sseClientSyncVersion],
33
33
 
34
- [API.watchHotReload, longPollDevClientHotReload],
34
+ [API.watchHotReload, sseClientHotReload],
35
35
  [API.throws, () => { throw new Error('Test500') }]
36
36
  ])
37
37
 
38
38
 
39
39
  export const apiPatchReqs = new Map([
40
40
  [API.cors, setCorsAllowed],
41
- [API.reset, reinitialize],
41
+ [API.reset, reset],
42
42
  [API.cookies, selectCookie],
43
43
  [API.globalDelay, setGlobalDelay],
44
44
  [API.globalDelayJitter, setGlobalDelayJitter],
@@ -92,7 +92,7 @@ function getState(_, response) {
92
92
 
93
93
  /** # PATCH */
94
94
 
95
- function reinitialize(_, response) {
95
+ function reset(_, response) {
96
96
  mockBrokersCollection.init()
97
97
  staticCollection.init()
98
98
  response.ok()