@lightningjs/blits 2.14.0 → 2.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -70,7 +70,7 @@
70
70
  "bench:compare": "npm --prefix benchmarks run bench:compare --",
71
71
  "bench:profile": "npm --prefix benchmarks run bench:profile --",
72
72
  "bench:serve": "npm --prefix benchmarks run serve",
73
- "release": "npm publish --access public"
73
+ "release": "node scripts/release.js"
74
74
  },
75
75
  "types": "./index.d.ts",
76
76
  "lint-staged": {
@@ -95,7 +95,7 @@
95
95
  "globals": "^17.12.0",
96
96
  "husky": "^9.1.7",
97
97
  "jsdom": "^30.1.0",
98
- "lint-staged": "^15.5.2",
98
+ "lint-staged": "^17.6.0",
99
99
  "prettier": "^3.6.2",
100
100
  "sinon": "^22.1.0",
101
101
  "tap-diff": "^0.1.1",
@@ -25,6 +25,55 @@ import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25
25
  import { renderer } from './launch.js'
26
26
  import { platform } from './platform.js'
27
27
 
28
+ // Reacts to Blits-only settings changed at runtime via `this.$settings.set()`.
29
+ Settings.onRuntimeSettingChange = (key, value) => {
30
+ if (key === 'keymap') {
31
+ initKeyMap()
32
+ return
33
+ }
34
+
35
+ if (key === 'inputThrottle') {
36
+ inputThrottleMs = value
37
+ return
38
+ }
39
+
40
+ if (key === 'mouseMoveThrottle') {
41
+ mouseMoveThrottleMs = value
42
+ return
43
+ }
44
+
45
+ if (
46
+ key === 'announcerOptions' &&
47
+ liveAppInstance &&
48
+ liveAppInstance.$announcer &&
49
+ value &&
50
+ typeof value === 'object'
51
+ ) {
52
+ liveAppInstance.$announcer.configure(value)
53
+ return
54
+ }
55
+
56
+ if (key === 'enableMouse') {
57
+ enableMouseFlag = value
58
+ if (liveInput !== null && liveViewport !== null) {
59
+ if (value === true && mouseListenersAttached === false) {
60
+ liveInput.addEventListener('mousemove', mouseMoveHandlerRef)
61
+ liveInput.addEventListener('click', mouseClickHandlerRef)
62
+ liveViewport.addEventListener('resize', updateCanvasRectRef)
63
+ liveViewport.addEventListener('scroll', updateCanvasRectRef)
64
+ mouseListenersAttached = true
65
+ } else if (value === false && mouseListenersAttached === true) {
66
+ liveInput.removeEventListener('mousemove', mouseMoveHandlerRef)
67
+ liveInput.removeEventListener('click', mouseClickHandlerRef)
68
+ liveViewport.removeEventListener('resize', updateCanvasRectRef)
69
+ liveViewport.removeEventListener('scroll', updateCanvasRectRef)
70
+ mouseListenersAttached = false
71
+ Hover.clear()
72
+ }
73
+ }
74
+ }
75
+ }
76
+
28
77
  /**
29
78
  * Merged keyMap (default + custom settings).
30
79
  * Initialized once during application init.
@@ -32,6 +81,24 @@ import { platform } from './platform.js'
32
81
  */
33
82
  export let keyMap = {}
34
83
 
84
+ /** @type {any} Active App component instance, set during App init */
85
+ let liveAppInstance = null
86
+ /** @type {any} Platform input target, set during App init */
87
+ let liveInput = null
88
+ /** @type {any} Platform viewport target, set during App init */
89
+ let liveViewport = null
90
+ let mouseListenersAttached = false
91
+ /** @type {((e: any) => void)|null} */
92
+ let mouseMoveHandlerRef = null
93
+ /** @type {(() => void)|null} */
94
+ let mouseClickHandlerRef = null
95
+ /** @type {(() => void)|null} */
96
+ let updateCanvasRectRef = null
97
+
98
+ let inputThrottleMs = 0
99
+ let mouseMoveThrottleMs = 100
100
+ let enableMouseFlag = false
101
+
35
102
  export const initKeyMap = () => {
36
103
  keyMap = { ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) }
37
104
  }
@@ -66,6 +133,7 @@ const Application = (config) => {
66
133
  }
67
134
 
68
135
  config.hooks[symbols.init] = function () {
136
+ liveAppInstance = this
69
137
  // set the initial activation state of the announcer based on launch setting
70
138
  this.$announcer.toggle(Settings.get('announcer', false))
71
139
  // configure global default utterance options if provided in settings
@@ -75,15 +143,15 @@ const Application = (config) => {
75
143
  }
76
144
  // Initialize merged keyMap once during application init
77
145
  initKeyMap()
78
-
79
- /** @type {number} Input throttle time in milliseconds (0 = disabled) */
80
- const throttleMs = Settings.get('inputThrottle', 0)
81
-
82
- const mouseEnabled = Settings.get('enableMouse', false)
83
- const mouseMoveThrottle = Settings.get('mouseMoveThrottle', 100)
146
+ // Initialize fast-path cached settings once during application init
147
+ inputThrottleMs = Settings.get('inputThrottle', 0)
148
+ mouseMoveThrottleMs = Settings.get('mouseMoveThrottle', 100)
149
+ enableMouseFlag = Settings.get('enableMouse', false)
84
150
  const { createKeyboardEvent, input, isKeyboardEvent, now = Date.now, viewport } = platform
151
+ liveInput = input
152
+ liveViewport = viewport
85
153
 
86
- keyDownHandler = async (e) => {
154
+ keyDownHandler = async (/** @type {any} */ e) => {
87
155
  const currentTime = now()
88
156
 
89
157
  const key = keyMap[e.keyCode] || e.keyCode
@@ -91,11 +159,11 @@ const Application = (config) => {
91
159
  lastInputKey = key
92
160
  // execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
93
161
  // or key is different from the last used key
94
- if (throttleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
162
+ if (inputThrottleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
95
163
  return await processInput.call(this, e, key)
96
164
  }
97
165
 
98
- if (currentTime - lastInputTime < throttleMs) {
166
+ if (currentTime - lastInputTime < inputThrottleMs) {
99
167
  return
100
168
  }
101
169
 
@@ -115,7 +183,7 @@ const Application = (config) => {
115
183
  }
116
184
 
117
185
  Focus.input(key, e)
118
- if (mouseEnabled === true) {
186
+ if (enableMouseFlag === true) {
119
187
  Hover.clear()
120
188
  // forget the node under the pointer too, so moving on it hovers it again
121
189
  currentNode = undefined
@@ -130,6 +198,9 @@ const Application = (config) => {
130
198
  }
131
199
 
132
200
  keyUpHandler = (e) => {
201
+ clearTimeout(holdTimeout)
202
+ Focus.hold = false
203
+
133
204
  const entry = keyUpCallbacks.get(e.keyCode)
134
205
  if (entry !== undefined) {
135
206
  keyUpCallbacks.delete(e.keyCode)
@@ -137,8 +208,6 @@ const Application = (config) => {
137
208
  entry.callback()
138
209
  }
139
210
  }
140
- clearTimeout(holdTimeout)
141
- Focus.hold = false
142
211
  }
143
212
 
144
213
  let lastMoved = 0
@@ -154,7 +223,7 @@ const Application = (config) => {
154
223
 
155
224
  // limit the amount of move events per time frame
156
225
  mouseMoveHandler = (e) => {
157
- if (e.timeStamp - lastMoved < mouseMoveThrottle) return
226
+ if (e.timeStamp - lastMoved < mouseMoveThrottleMs) return
158
227
  lastMoved = e.timeStamp
159
228
 
160
229
  this.$emit('mouse::move', e)
@@ -166,7 +235,7 @@ const Application = (config) => {
166
235
  return
167
236
  }
168
237
 
169
- const stage = renderer.stage
238
+ const stage = /** @type {any} */ (renderer).stage
170
239
  if (stage == null) {
171
240
  return
172
241
  }
@@ -215,16 +284,21 @@ const Application = (config) => {
215
284
  input.addEventListener('keydown', keyDownHandler)
216
285
  input.addEventListener('keyup', keyUpHandler)
217
286
  inputCleanup = () => {
218
- input.removeEventListener('keydown', keyDownHandler)
219
- input.removeEventListener('keyup', keyUpHandler)
287
+ input.removeEventListener('keydown', /** @type {any} */ (keyDownHandler))
288
+ input.removeEventListener('keyup', /** @type {any} */ (keyUpHandler))
220
289
  }
221
290
  }
222
- if (mouseEnabled === true && input !== undefined && viewport !== undefined) {
291
+ mouseMoveHandlerRef = mouseMoveHandler
292
+ mouseClickHandlerRef = mouseClickHandler
293
+ updateCanvasRectRef = updateCanvasRect
294
+
295
+ if (enableMouseFlag === true && input !== undefined && viewport !== undefined) {
223
296
  updateCanvasRect()
224
297
  input.addEventListener('mousemove', mouseMoveHandler)
225
298
  input.addEventListener('click', mouseClickHandler)
226
299
  viewport.addEventListener('resize', updateCanvasRect)
227
300
  viewport.addEventListener('scroll', updateCanvasRect)
301
+ mouseListenersAttached = true
228
302
  mouseCleanup = () => {
229
303
  input.removeEventListener('mousemove', mouseMoveHandler)
230
304
  input.removeEventListener('click', mouseClickHandler)
@@ -233,8 +307,9 @@ const Application = (config) => {
233
307
  }
234
308
  }
235
309
 
236
- // next tick
237
- setTimeout(() => Focus.set(this))
310
+ // focus once the component tree is built (microtask: runs right after the
311
+ // current instantiation finishes, without yielding to render-loop frames)
312
+ Promise.resolve().then(() => Focus.set(this))
238
313
  }
239
314
 
240
315
  return Component('App', config)
@@ -21,6 +21,7 @@ import events from './events.js'
21
21
  import router from './router.js'
22
22
  import announcer from './announcer.js'
23
23
  import utils from './utils.js'
24
+ import settings from './settings.js'
24
25
  import symbols from '../../lib/symbols.js'
25
26
  import { reactive } from '../../lib/reactivity/reactive.js'
26
27
  import Settings from '../../settings.js'
@@ -29,6 +30,7 @@ export const shared = {
29
30
  ...events,
30
31
  ...router,
31
32
  ...announcer,
33
+ ...settings,
32
34
  $reactive: {
33
35
  value: function (target) {
34
36
  return reactive(target, Settings.get('reactivityMode'), true)
@@ -149,9 +149,9 @@ const createRouter = (component, routerViewName) => {
149
149
  toRouterView(routerView, path, data, options)
150
150
  return true
151
151
  },
152
- back() {
152
+ back(steps = 1) {
153
153
  const routerView = resolveRouterView(component, routerViewName)
154
- return routerView === null ? false : back.call(routerView)
154
+ return routerView === null ? false : back.call(routerView, steps)
155
155
  },
156
156
  get(name) {
157
157
  return getRouterFromCache(component, name)
@@ -172,6 +172,10 @@ const createRouter = (component, routerViewName) => {
172
172
  get navigating() {
173
173
  return state.navigating
174
174
  },
175
+ get history() {
176
+ const routerView = resolveRouterView(component, routerViewName)
177
+ return routerView === null ? [] : routerView.history.slice()
178
+ },
175
179
  state,
176
180
  }
177
181
  }
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright 2023 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ import Settings from '../../settings.js'
19
+ import engine from '../../engine.js'
20
+ import { Log } from '../../lib/log.js'
21
+
22
+ // Settings that can be changed at runtime via `this.$settings.set()`.
23
+ const blitsKeys = [
24
+ 'keymap',
25
+ 'debugLevel',
26
+ 'inputThrottle',
27
+ 'announcerOptions',
28
+ 'enableMouse',
29
+ 'mouseMoveThrottle',
30
+ ]
31
+
32
+ const rendererKeys = [
33
+ 'w',
34
+ 'h',
35
+ 'screenResolution',
36
+ 'renderQuality',
37
+ 'pixelRatio',
38
+ 'canvasColor',
39
+ 'viewportMargin',
40
+ 'maxFPS',
41
+ 'fpsInterval',
42
+ ]
43
+
44
+ const changeableKeys = [...blitsKeys, ...rendererKeys]
45
+
46
+ export default {
47
+ $settings: {
48
+ value: {
49
+ get(key, defaultValue) {
50
+ return Settings.get(key, defaultValue)
51
+ },
52
+ set(key, value) {
53
+ if (changeableKeys.indexOf(key) === -1) {
54
+ Log.warn(`Setting "${key}" can not be changed at runtime`)
55
+ return
56
+ }
57
+ Settings.set(key, value)
58
+
59
+ if (rendererKeys.indexOf(key) !== -1) {
60
+ engine.updateSetting(key, value)
61
+ }
62
+ },
63
+ },
64
+ writable: false,
65
+ enumerable: true,
66
+ configurable: false,
67
+ },
68
+ }
package/src/component.js CHANGED
@@ -426,8 +426,9 @@ const Component = (name = required('name'), config = required('config')) => {
426
426
  }
427
427
  }
428
428
 
429
- // finaly set the lifecycle state to ready (in the next tick)
430
- setTimeout(() => (this[symbols.lifecycle].state = 'ready'))
429
+ // finally set the lifecycle state to ready (synchronously: instantiation,
430
+ // reactivity setup and template spawn are all complete at this point)
431
+ this[symbols.lifecycle].state = 'ready'
431
432
 
432
433
  // and return this
433
434
  return this
@@ -16,11 +16,15 @@
16
16
  */
17
17
 
18
18
  import Blits from '../../index.js'
19
- import { renderer } from '../launch.js'
20
19
  import fps_sprite from '../../public/assets/fps_sprite.base64.js'
21
20
 
22
21
  // exporting as named export to allow selective importing
23
22
  export const FPScounter = Blits.Component('FPScounter', {
23
+ props: {
24
+ active: true,
25
+ show: true,
26
+ alpha: 1,
27
+ },
24
28
  /* eslint-disable no-unused-vars */
25
29
  code: {
26
30
  render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
@@ -662,28 +666,34 @@ export const FPScounter = Blits.Component('FPScounter', {
662
666
  avgFps: '---',
663
667
  minFps: '---',
664
668
  maxFps: '---',
669
+
670
+ data: {
671
+ minFps: 10000,
672
+ maxFps: 0,
673
+ totalFps: 0,
674
+ fpsUpdateCounter: 0,
675
+ avgFps: 0,
676
+ },
665
677
  }
666
678
  },
667
679
  hooks: {
668
- ready() {
669
- let minFps = 10000
670
- let maxFps = 0
671
- let avgFps = 0
672
- let totalFps = 0
673
- let fpsUpdateCounter = 0
674
-
675
- renderer.on('fpsUpdate', (rM, { fps }) => {
676
- minFps = Math.min(fps, minFps)
677
- maxFps = Math.max(fps, maxFps)
678
- totalFps += fps
679
- fpsUpdateCounter++
680
- avgFps = Math.round(totalFps / fpsUpdateCounter)
681
-
682
- this.fps = fps.toString().padStart(3, '0')
683
- this.avgFps = avgFps.toString().padStart(3, '0')
684
- this.minFps = minFps.toString().padStart(3, '0')
685
- this.maxFps = maxFps.toString().padStart(3, '0')
686
- })
680
+ /**
681
+ * @param {number} fps
682
+ */
683
+ fpsUpdate(fps) {
684
+ // early return when not active or hidden
685
+ if (!this.active || !this.show || this.alpha === 0) return
686
+
687
+ this.data.minFps = Math.min(fps, this.data.minFps)
688
+ this.data.maxFps = Math.max(fps, this.data.maxFps)
689
+ this.data.totalFps += fps
690
+ this.data.fpsUpdateCounter++
691
+ this.data.avgFps = Math.round(this.data.totalFps / this.data.fpsUpdateCounter)
692
+
693
+ this.fps = fps.toString().padStart(3, '0')
694
+ this.avgFps = this.data.avgFps.toString().padStart(3, '0')
695
+ this.minFps = this.data.minFps.toString().padStart(3, '0')
696
+ this.maxFps = this.data.maxFps.toString().padStart(3, '0')
687
697
  },
688
698
  },
689
699
  })
@@ -16,9 +16,47 @@
16
16
  */
17
17
 
18
18
  import Element from './element.js'
19
- import Launch from './launch.js'
19
+ import Launch, { renderer } from './launch.js'
20
+ import colors from '../../lib/colors/colors.js'
21
+ import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
22
+
23
+ /**
24
+ * Maps Blits setting keys to renderer runtime options
25
+ *
26
+ * @type {Record<string, (value: any) => Partial<import('@lightningjs/renderer').RendererRuntimeSettings>>}
27
+ */
28
+ const rendererOptions = {
29
+ // defaults to transparent (same as on launch) for empty or invalid colors
30
+ w: (v) => ({ appWidth: v }),
31
+ h: (v) => ({ appHeight: v }),
32
+ screenResolution: (v) => ({
33
+ deviceLogicalPixelRatio: typeof v === 'string' ? (SCREEN_RESOLUTIONS[v] ?? 1) : v,
34
+ }),
35
+ renderQuality: (v) => ({
36
+ devicePhysicalPixelRatio: RENDER_QUALITIES[v] ?? v ?? 1,
37
+ }),
38
+ canvasColor: (v) => ({ clearColor: Number(colors.normalize(v ?? '', '0x00000000')) }),
39
+ fpsInterval: (v) => ({ fpsUpdateInterval: v }),
40
+ pixelRatio: (v) => ({ deviceLogicalPixelRatio: v }),
41
+ viewportMargin: (v) => ({ boundsMargin: v }),
42
+ maxFPS: (v) => ({ targetFPS: v }),
43
+ }
44
+
45
+ /**
46
+ * Applies a runtime setting to the renderer via RendererMain.setOptions()
47
+ *
48
+ * @param {string} key - Blits setting key (validated by $settings)
49
+ * @param {any} value - New value
50
+ */
51
+ const updateSetting = (key, value) => {
52
+ const rendererInstance = renderer
53
+ if (rendererInstance && typeof rendererInstance.setOptions === 'function') {
54
+ rendererInstance.setOptions(rendererOptions[key](value))
55
+ }
56
+ }
20
57
 
21
58
  export default {
22
59
  Element,
23
60
  Launch,
61
+ updateSetting,
24
62
  }
@@ -20,7 +20,6 @@ import { WebGlCoreRenderer, SdfTextRenderer } from '@lightningjs/renderer/webgl'
20
20
  import { CanvasCoreRenderer, CanvasTextRenderer } from '@lightningjs/renderer/canvas'
21
21
  import { Inspector } from '@lightningjs/renderer/inspector'
22
22
 
23
- import { Log } from '../../lib/log.js'
24
23
  import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
25
24
  import colors from '../../lib/colors/colors.js'
26
25
  import fontLoader from './fontLoader.js'
@@ -85,7 +84,7 @@ const textureMemorySettings = (settings) => {
85
84
  * @param {Partial<import('../../launch.js').BlitsSettings>} [settings] - The settings for the renderer
86
85
  *
87
86
  */
88
- export default (App, target, settings = {}) => {
87
+ export default (target, settings = {}) => {
89
88
  const screenHeight = settings.platform ? settings.platform.screenHeight : platform.screenHeight
90
89
  const hardwareConcurrency = settings.platform
91
90
  ? settings.platform.hardwareConcurrency
@@ -125,21 +124,8 @@ export default (App, target, settings = {}) => {
125
124
  target
126
125
  )
127
126
 
128
- const initApp = () => {
129
- let app = App()
130
- app.quit = () => {
131
- Log.info('Closing App')
132
- app.destroy()
133
- app = null
134
- renderer = null
135
- }
136
- }
137
-
138
127
  shaderLoader()
139
128
  fontLoader()
140
- // Defer app initialization until after the current call stack is cleared,
141
- // allowing the renderer to finish setting up before the app starts creating components
142
- Promise.resolve().then(initApp)
143
129
 
144
130
  return renderer
145
131
  }
@@ -68,6 +68,10 @@ export default {
68
68
  while (i--) {
69
69
  // don't unfocus when part of the new focus chain
70
70
  if (newFocusChain.indexOf(focusChain[i]) > -1) break
71
+ // only unfocus components that actually have focus
72
+ // ($hasFocus, not the lifecycle state which is shared with hover/enter/exit)
73
+ const componentState = focusChain[i][symbols.state]
74
+ if (componentState !== undefined && componentState.$hasFocus === false) continue
71
75
  focusChain[i][symbols.lifecycle].state = 'unfocus'
72
76
  }
73
77
  focusChain = newFocusChain
package/src/launch.js CHANGED
@@ -155,8 +155,17 @@ export default (App, target, settings) => {
155
155
 
156
156
  stage.element = engine.Element
157
157
 
158
- renderer = engine.Launch(App, target, settings)
158
+ renderer = engine.Launch(target, settings)
159
159
  if (isDev === true) {
160
160
  profiler.setRenderer(renderer)
161
161
  }
162
+
163
+ // Instantiate the app and wire up the quit function
164
+ let app = App()
165
+ app.quit = () => {
166
+ Log.info('Closing App')
167
+ app.destroy()
168
+ app = null
169
+ renderer = null
170
+ }
162
171
  }
@@ -590,9 +590,22 @@ export const getSingleRegisteredRouterView = () => {
590
590
  return singleRouterView
591
591
  }
592
592
 
593
- export const back = function () {
593
+ export const back = function (steps = 1) {
594
594
  if (this.history === undefined) return
595
+ if (steps < 1) steps = 1
596
+ if (steps > this.history.length) steps = this.history.length
595
597
  const viewState = getViewState(this.name)
598
+
599
+ // discard the intermediate history entries when navigating back more than 1 step
600
+ for (let i = 1; i < steps; i++) {
601
+ const poppedRoute = this.history.pop()
602
+ // if skipped history item is configured as keepAlive true then invoke destroy on view
603
+ if (poppedRoute.view !== undefined && poppedRoute.view !== null) {
604
+ poppedRoute.view.destroy()
605
+ poppedRoute.focus = null
606
+ }
607
+ }
608
+
596
609
  const route = this.history.pop()
597
610
  if (route && this.currentRoute !== route) {
598
611
  // set indicator that we are navigating back (to prevent adding page to history stack)
package/src/settings.js CHANGED
@@ -19,6 +19,8 @@ import symbols from './lib/symbols.js'
19
19
 
20
20
  const settings = {
21
21
  [symbols.settings]: {},
22
+ // Reacts to Blits-only settings changed at runtime via `this.$settings.set()`.
23
+ onRuntimeSettingChange: null,
22
24
  get(key, defaultValue = null) {
23
25
  if (key in this[symbols.settings]) {
24
26
  return this[symbols.settings][key]
@@ -33,6 +35,9 @@ const settings = {
33
35
  })
34
36
  } else {
35
37
  this[symbols.settings][key] = value
38
+ if (typeof this.onRuntimeSettingChange === 'function') {
39
+ this.onRuntimeSettingChange(key, value)
40
+ }
36
41
  }
37
42
  },
38
43
  }