@lightningjs/blits 1.32.1 → 1.34.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ _10 Jul 2025_
4
+
5
+ ## v1.34.0
6
+
7
+ - Added storage plugin
8
+ - Improved test coverage
9
+ - Fixed announcer plugin to work on RDK devices where early GC was happening
10
+ - Added check to throttle only the same input key in a row
11
+ - Updated docs on transitions
12
+ - Added warning when using `hasFocus` in component state
13
+
14
+ _08 Jul 2025_
15
+
16
+ ## v1.33.0
17
+
18
+ - Added dereferencing of previousFocus pointer to ensure memory cleanup
19
+ - Improved cleanup of components (via explicit dereference of closure scope)
20
+ - Optimized time function in logger
21
+ - Optimized initial registration of routes
22
+ - Added `maxFPS` launch setting
23
+
3
24
  _04 Jul 2025_
4
25
 
5
26
  ## v1.32.1
package/NOTICE CHANGED
@@ -10,3 +10,7 @@ within this component.
10
10
  This repository uses components from the Lightning-SDK. Any components from the Lightning-SDK are:
11
11
  Copyright (c) 2020 Metrological
12
12
  Licensed under the Apache License, Version 2.0
13
+
14
+ This repository uses components from WebPlatformForEmbedded
15
+ Copyright (c) 2019 Metrological
16
+ Licensed under the Apache License, Version 2.0
package/index.d.ts CHANGED
@@ -970,8 +970,10 @@ declare module '@lightningjs/blits' {
970
970
  /**
971
971
  * Input throttle time in milliseconds to prevent rapid successive inputs
972
972
  *
973
- * Within the throttle window, only one input will be processed immediately Subsequent
974
- * inputs are ignored until the scheduled input is processed.
973
+ * Within the throttle window, only one input will be processed immediately.
974
+ * Subsequent inputs _of the same key_ are ignored until the scheduled input is processed.
975
+ *
976
+ * Pressing a different key will be processed immediately.
975
977
  *
976
978
  * Set to `0` to disable input throttling.
977
979
  *
@@ -1015,6 +1017,15 @@ declare module '@lightningjs/blits' {
1015
1017
  * @default false
1016
1018
  */
1017
1019
  announcer?: boolean
1020
+ /**
1021
+ * Maximum FPS at which the App will be rendered
1022
+ *
1023
+ * Lowering the maximum FPS value can improve the overall experience on lower end devices.
1024
+ * Targetting a lower FPS may gives the CPU more time to construct each frame leading to a smoother rendering.
1025
+ *
1026
+ * Defaults to `0` which means no maximum
1027
+ */
1028
+ maxFPS?: number
1018
1029
  }
1019
1030
 
1020
1031
  interface State {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.32.1",
3
+ "version": "1.34.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -13,7 +13,8 @@
13
13
  "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "c8 npm run test:run",
16
+ "test": "c8 node scripts/runTests.js",
17
+ "test:ci": "c8 npm run test:run",
17
18
  "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff",
18
19
  "lint": "eslint '**/*.js'",
19
20
  "lint:fix": "eslint '**/*.js' --fix",
@@ -38,6 +39,7 @@
38
39
  "eslint": "^8.8.0",
39
40
  "eslint-config-prettier": "^8.3.0",
40
41
  "eslint-plugin-prettier": "^4.0.0",
42
+ "fast-glob": "^3.3.3",
41
43
  "global-jsdom": "24.0.0",
42
44
  "husky": "^9.1.7",
43
45
  "jsdom": "24.0.0",
@@ -146,6 +146,7 @@ const stop = () => {
146
146
  }
147
147
 
148
148
  const clear = () => {
149
+ isProcessing = false
149
150
  queue.length = 0
150
151
  }
151
152
 
@@ -21,9 +21,17 @@ const syn = window.speechSynthesis
21
21
 
22
22
  const isAndroid = /android/i.test((window.navigator || {}).userAgent || '')
23
23
 
24
+ const utterances = new Map() // Strong references with unique keys
25
+
24
26
  let initialized = false
25
27
  let infinityTimer = null
26
- const clear = () => infinityTimer && clearTimeout(infinityTimer)
28
+
29
+ const clear = () => {
30
+ if (infinityTimer) {
31
+ clearTimeout(infinityTimer)
32
+ infinityTimer = null
33
+ }
34
+ }
27
35
 
28
36
  const resumeInfinity = (target) => {
29
37
  if (!target || infinityTimer) {
@@ -31,7 +39,9 @@ const resumeInfinity = (target) => {
31
39
  }
32
40
 
33
41
  syn.pause()
34
- syn.resume()
42
+ setTimeout(() => {
43
+ syn.resume()
44
+ }, 0)
35
45
 
36
46
  infinityTimer = setTimeout(() => {
37
47
  resumeInfinity(target)
@@ -47,18 +57,20 @@ const defaultUtteranceProps = {
47
57
  }
48
58
 
49
59
  const initialize = () => {
50
- defaultUtteranceProps.voice = syn.getVoices()[0]
60
+ const voices = syn.getVoices()
61
+ defaultUtteranceProps.voice = voices[0] || null
51
62
  initialized = true
52
63
  }
53
64
 
54
65
  const speak = (options) => {
55
66
  const utterance = new SpeechSynthesisUtterance(options.message)
56
-
67
+ const id = Date.now() + Math.random() // Unique ID for tracking
57
68
  utterance.lang = options.lang || defaultUtteranceProps.lang
58
69
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
59
70
  utterance.rate = options.rate || defaultUtteranceProps.rate
60
71
  utterance.voice = options.voice || defaultUtteranceProps.voice
61
72
  utterance.volume = options.volume || defaultUtteranceProps.volume
73
+ utterances.set(id, utterance) // Strong reference
62
74
 
63
75
  if (isAndroid === false) {
64
76
  utterance.onstart = () => {
@@ -72,9 +84,14 @@ const speak = (options) => {
72
84
 
73
85
  return new Promise((resolve, reject) => {
74
86
  utterance.onend = () => {
87
+ clear()
88
+ utterances.delete(id) // Cleanup
75
89
  resolve()
76
90
  }
91
+
77
92
  utterance.onerror = (e) => {
93
+ clear()
94
+ utterances.delete(id) // Cleanup
78
95
  reject(e)
79
96
  }
80
97
 
@@ -48,6 +48,7 @@ const Application = (config) => {
48
48
  let keyUpHandler
49
49
  let holdTimeout
50
50
  let lastInputTime = 0
51
+ let lastInputKey = null
51
52
 
52
53
  config.hooks[symbols.destroy] = function () {
53
54
  document.removeEventListener('keydown', keyDownHandler)
@@ -66,8 +67,11 @@ const Application = (config) => {
66
67
  const currentTime = performance.now()
67
68
 
68
69
  const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode
70
+ const sameKey = lastInputKey === key
71
+ lastInputKey = key
69
72
  // execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
70
- if (throttleMs === 0 || e[symbols.internalEvent] === true) {
73
+ // or key is different from the last used key
74
+ if (throttleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
71
75
  return await processInput.call(this, e, key)
72
76
  }
73
77
 
@@ -40,6 +40,7 @@ export default {
40
40
  * @this {import('../../component').BlitsComponent}
41
41
  */
42
42
  value: function (e) {
43
+ if (this.eol === true) return
43
44
  this[symbols.state].hasFocus = true
44
45
  Focus.set(this, e)
45
46
  },
@@ -103,11 +104,15 @@ export default {
103
104
  delete this.componentId
104
105
  delete this[symbols.id]
105
106
  delete this.ref
107
+ delete this[symbols.state].hasFocus
106
108
 
107
109
  this[symbols.holder].destroy()
108
110
  this[symbols.holder] = null
109
111
  delete this[symbols.holder]
110
112
 
113
+ this[symbols.cleanup]()
114
+ delete this[symbols.cleanup]
115
+
111
116
  Log.debug(`Destroyed component ${this.componentId}`)
112
117
  },
113
118
  writable: false,
@@ -24,19 +24,16 @@ export default (component, data) => {
24
24
  routes = data.routes
25
25
  }
26
26
  component[symbols.routes] = []
27
- Object.keys(routes).forEach((key) => {
28
- // todo: validate routes[key] for expected format etc.
29
- component[symbols.routes][key] = {
30
- ...routes[key],
27
+
28
+ const l = routes.length
29
+ for (let i = 0; i < l; i++) {
30
+ component[symbols.routes][i] = routes[i]
31
+ // merge default route options with route specific options
32
+ component[symbols.routes][i].options = {
31
33
  ...{
32
- // merge default route options with route specific options
33
- options: {
34
- ...{
35
- inHistory: true,
36
- },
37
- ...routes[key].options,
38
- },
34
+ inHistory: true,
39
35
  },
36
+ ...routes[i].options,
40
37
  }
41
- })
38
+ }
42
39
  }
@@ -28,8 +28,14 @@ export default (component, state = () => {}) => {
28
28
  })
29
29
 
30
30
  const stateKeys = Object.keys(state.apply(component) || {})
31
- // add built-in hasFocus key
32
- stateKeys.push(['hasFocus'])
31
+ if (stateKeys.indexOf('hasFocus') > -1) {
32
+ Log.warn(
33
+ 'State `hasFocus` already exists as a built-in Component variable (to indicate whether the component currently has focus). Avoid using your own `hasFocus` key in the Component state'
34
+ )
35
+ } else {
36
+ // add built-in hasFocus key
37
+ stateKeys.push(['hasFocus'])
38
+ }
33
39
  const stateKeysLength = stateKeys.length
34
40
 
35
41
  for (let i = 0; i < stateKeysLength; i++) {
package/src/component.js CHANGED
@@ -243,16 +243,18 @@ const Component = (name = required('name'), config = required('config')) => {
243
243
 
244
244
  // execute the render code that constructs the initial state of the component
245
245
  // and store the children result (a flat map of elements and components)
246
- this[symbols.children] =
247
- config.code.render.apply(stage, [
248
- parentEl,
249
- this,
250
- config,
251
- globalComponents,
252
- effect,
253
- getRaw,
254
- Log,
255
- ]) || []
246
+ const { elms, cleanup } = config.code.render.apply(stage, [
247
+ parentEl,
248
+ this,
249
+ config,
250
+ globalComponents,
251
+ effect,
252
+ getRaw,
253
+ Log,
254
+ ]) || { elms: [], cleanup: () => {} }
255
+
256
+ this[symbols.children] = elms
257
+ this[symbols.cleanup] = cleanup
256
258
 
257
259
  // create a reference to the wrapper element of the component (i.e. the root Element of the component)
258
260
  this[symbols.wrapper] = this[symbols.children][0]
@@ -23,30 +23,52 @@ export default () =>
23
23
  code: {
24
24
  render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
25
25
  const elms = []
26
+ const elementConfigs = []
27
+ const forloops = []
28
+ const props = []
29
+ const created = []
30
+ const skips = []
26
31
  let componentType
27
- const rootComponent = component
32
+ let rootComponent = component
28
33
  let propData
29
34
  let slotComponent
30
35
  let inSlot = false
31
36
  let slotChildCounter = 0
32
- const elementConfig0 = {}
37
+ let cmps = []
38
+ elementConfigs[0] = {}
33
39
 
34
40
  elms[0] = this.element(
35
41
  { parent: parent || 'root' },
36
42
  inSlot === true ? slotComponent : component
37
43
  )
38
44
 
39
- elementConfig0['color'] = component.color
40
- elementConfig0['w'] = component.size
41
- elementConfig0['h'] = component.size
42
- elementConfig0['effects'] = [component.shader('radius', { radius: component.radius })]
43
- elms[0].populate(elementConfig0)
45
+ elementConfigs[0]['color'] = component.color
46
+ elementConfigs[0]['w'] = component.size
47
+ elementConfigs[0]['h'] = component.size
48
+ elementConfigs[0]['effects'] = [component.shader('radius', { radius: component.radius })]
49
+ elms[0].populate(elementConfigs[0])
44
50
 
45
51
  if (inSlot === true) {
46
52
  slotChildCounter -= 1
47
53
  }
48
54
 
49
- return elms
55
+ return {
56
+ elms,
57
+ cleanup: () => {
58
+ rootComponent = null
59
+ propData = null
60
+ slotComponent = null
61
+ component = null
62
+ parent = null
63
+ cmps.length = 0
64
+ elms.length = 0
65
+ components.length = 0
66
+ elementConfigs.length = 0
67
+ forloops.length = 0
68
+ props.length = 0
69
+ skips.length = 0
70
+ },
71
+ }
50
72
  },
51
73
  effects: [
52
74
  function anonymous(component, elms, context, components, rootComponent, effect) {