mikro 0.18.1 → 0.18.2

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 CHANGED
@@ -60,7 +60,7 @@ The core runtime is a standalone C++ library with no ESP-IDF dependencies. This
60
60
 
61
61
  - **Engine**: [QuickJS-NG](https://github.com/quickjs-ng/quickjs) ([near-complete ES2024 coverage](https://test262.fyi/#|qjs_ng))
62
62
  - **Runtime**: Standalone C++ library with cooperative, single-threaded event loop
63
- - **Platform**: ESP-IDF >= 6.0.1 (ESP32, ESP32-C3, ESP32-S3, ESP32-C5, ESP32-C6), desktop (macOS, Linux)
63
+ - **Platform**: ESP-IDF >= 6.1 (ESP32, ESP32-C3, ESP32-S3, ESP32-C5, ESP32-C6), desktop (macOS, Linux)
64
64
  - **Modules**: Four-source resolution: virtual modules, native C modules, bytecode builtins, filesystem (LittleFS)
65
65
  - **Bytecode**: TypeScript bundled with esbuild, compiled to QuickJS bytecode at build time
66
66
  - **Tooling**: Node.js CLI (Ink/React), Node-API addon for host-side development and simulation
@@ -1,3 +1,3 @@
1
- export type { ApplyOutcome, Diagnostic, DownloadFn, InstallOptions, InstallOutcome, Offer, Ota, OtaBeginError, OtaDownloadError, OtaError, OtaInstallError, OtaWriteError, RunningBuild, Update, } from '@mikrojs/native/runtime/ota/types';
1
+ export type { ApplyOutcome, ConfigErrorReport, ConfigState, ConfigWrite, Diagnostic, DownloadFn, InstallOptions, InstallOutcome, Offer, Ota, OtaBeginError, OtaDownloadError, OtaError, OtaInstallError, OtaWriteError, RunningBuild, StoredConfig, Update, } from '@mikrojs/native/runtime/ota/types';
2
2
  export { ota } from '@mikrojs/native/runtime/ota/types';
3
3
  //# sourceMappingURL=ota.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ota.d.ts","sourceRoot":"","sources":["../../src/_exports/ota.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,UAAU,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,KAAK,EACL,GAAG,EACH,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,aAAa,EACb,YAAY,EACZ,MAAM,GACP,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAA"}
1
+ {"version":3,"file":"ota.d.ts","sourceRoot":"","sources":["../../src/_exports/ota.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,UAAU,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,KAAK,EACL,GAAG,EACH,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,MAAM,GACP,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ota.js","sourceRoot":"","sources":["../../src/_exports/ota.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAA"}
1
+ {"version":3,"file":"ota.js","sourceRoot":"","sources":["../../src/_exports/ota.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ota-client.d.ts","sourceRoot":"","sources":["../../../src/simulator/builtins/ota-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAA;AAajD,eAAO,MAAM,gBAAgB,EAAE,iBAiK9B,CAAA"}
1
+ {"version":3,"file":"ota-client.d.ts","sourceRoot":"","sources":["../../../src/simulator/builtins/ota-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAA;AAcjD,eAAO,MAAM,gBAAgB,EAAE,iBAuS9B,CAAA"}
@@ -8,10 +8,11 @@
8
8
  // pretending to run a round. Everything else is simulated: the host has no app
9
9
  // partition, but the staging -> finish -> reconcile -> confirm/revert sequence
10
10
  // is the part an app's own update code drives, and it runs here in memory.
11
- // `config()` serves whatever the session's mik.sys `ota.cfg` slot holds.
11
+ // `config()` serves whatever the session's mik.sys `ota.cfg` slot holds, and
12
+ // `applyConfig()` is what puts a document there.
12
13
  export const otaClientBuiltin = {
13
14
  source: `
14
- import {sysGet} from 'native:mikro/nvs_kv'
15
+ import {sysGet, sysRemove, sysSet} from 'native:mikro/nvs_kv'
15
16
  import {readFile} from 'mikro/fs'
16
17
  import {ok, err} from 'mikro/result'
17
18
 
@@ -38,6 +39,7 @@ export function config() {
38
39
  const stored = sysGet('ota.cfg')
39
40
  const doc = typeof stored === 'object' && stored !== null ? stored.doc : undefined
40
41
  const hasDoc = typeof doc === 'object' && doc !== null
42
+ if (hasDoc) markConfigRead()
41
43
  const defaults = manifestDefaults()
42
44
  if (defaults === undefined) {
43
45
  if (hasDoc) return doc
@@ -54,16 +56,31 @@ export function config() {
54
56
  // materialized defaults is an app that declares no config schema, whose config
55
57
  // is {}, not a failure. Mirrors the device reader (src/mik_ota_config.cpp).
56
58
  function manifestDefaults() {
57
- const manifest = readFile('/app/mikro.app.json', 'utf-8')
58
- if (!manifest.ok) return undefined
59
+ const parsed = manifest()
60
+ if (parsed === undefined) return undefined
61
+ const held = parsed.configDefaults
62
+ return typeof held === 'object' && held !== null ? held : {}
63
+ }
64
+
65
+ // The running app's version, which is what decides where a delivered document
66
+ // belongs. The manifest carries the same value the device reads out of
67
+ // /app/package.json.
68
+ function manifestVersion() {
69
+ const parsed = manifest()
70
+ const version = parsed === undefined ? undefined : parsed.version
71
+ return typeof version === 'string' && version !== '' ? version : undefined
72
+ }
73
+
74
+ function manifest() {
75
+ const text = readFile('/app/mikro.app.json', 'utf-8')
76
+ if (!text.ok) return undefined
59
77
  let parsed
60
78
  try {
61
- parsed = JSON.parse(manifest.value)
79
+ parsed = JSON.parse(text.value)
62
80
  } catch {
63
81
  return undefined
64
82
  }
65
- const held = parsed === null ? undefined : parsed.configDefaults
66
- return typeof held === 'object' && held !== null ? held : {}
83
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined
67
84
  }
68
85
 
69
86
  export function bearer() {
@@ -76,6 +93,113 @@ export function registry() {
76
93
  return typeof url === 'string' && url !== '' ? url : undefined
77
94
  }
78
95
 
96
+ // ── config delivery ──────────────────────────────────────────────────────────
97
+ // Mirrors src/mik_ota_config.cpp. A sim run is one boot, so a trial is armed
98
+ // and can be adopted here, but never burned down to a rollback: that needs the
99
+ // power cycles only a device has.
100
+
101
+ export function parseConfig(raw) {
102
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return undefined
103
+ const version = raw.version
104
+ // Bounded by the buffers the device stores them in. A truncated rev would
105
+ // never match the one the registry issued, so it is rejected, not cut.
106
+ if (typeof version !== 'string' || version === '' || version.length > 31) return undefined
107
+ const rev = raw.rev
108
+ const hasRev = rev !== undefined && rev !== null
109
+ if (hasRev && (typeof rev !== 'string' || rev.length > 64)) return undefined
110
+ const doc = raw.doc
111
+ const hasDoc = doc !== undefined && doc !== null
112
+ // The document is spread over the defaults top level: anything that is not a
113
+ // plain object has no keys to spread. An absent doc is the clear.
114
+ if (hasDoc && (typeof doc !== 'object' || Array.isArray(doc))) return undefined
115
+ const out = {version}
116
+ if (hasRev && rev !== '') out.rev = rev
117
+ if (hasDoc) out.doc = doc
118
+ return out
119
+ }
120
+
121
+ export function applyConfig(config, options) {
122
+ const parsed = parseConfig(config)
123
+ if (parsed === undefined) return 'invalid'
124
+ const version = manifestVersion()
125
+ // Which slot the document belongs in is unknown without the running version,
126
+ // and storing it in either would be a guess the reader drops in silence.
127
+ if (version === undefined) return 'failed'
128
+
129
+ if (parsed.version !== version) {
130
+ // Stamped for another release: it applies with the build it names.
131
+ if (parsed.doc === undefined) sysRemove('ota.cfgNext')
132
+ else sysSet('ota.cfgNext', parsed)
133
+ return 'staged'
134
+ }
135
+
136
+ const held = sysGet('ota.cfg')
137
+ const heldCfg = typeof held === 'object' && held !== null ? held : undefined
138
+
139
+ if (parsed.doc === undefined) {
140
+ // A rev riding along does not turn a clear into a document.
141
+ sysRemove('ota.cfgTrial')
142
+ sysRemove('ota.cfgErr')
143
+ sysRemove('ota.cfgPrev')
144
+ if (heldCfg === undefined) return 'unchanged'
145
+ sysRemove('ota.cfg')
146
+ return 'cleared'
147
+ }
148
+
149
+ // An identical document must cost nothing: re-storing it would put a document
150
+ // that already passed its trial back on trial. The rev counts as part of it.
151
+ if (heldCfg !== undefined && sameConfig(heldCfg, parsed)) return 'unchanged'
152
+
153
+ // The document it replaces is the rollback baseline: a schema-valid value can
154
+ // still be fatal to the app.
155
+ if (heldCfg === undefined) sysRemove('ota.cfgPrev')
156
+ else sysSet('ota.cfgPrev', heldCfg)
157
+ sysSet('ota.cfg', parsed)
158
+ let trialBoots = 1
159
+ if (options !== undefined && typeof options.trialBoots === 'number' && options.trialBoots >= 1) {
160
+ trialBoots = options.trialBoots
161
+ }
162
+ sysSet('ota.cfgTrial', {left: trialBoots, read: false})
163
+ sysRemove('ota.cfgErr')
164
+ return 'applied'
165
+ }
166
+
167
+ export function configState() {
168
+ const out = {}
169
+ const report = sysGet('ota.cfgErr')
170
+ const hasError =
171
+ typeof report === 'object' && report !== null && typeof report.rev === 'string'
172
+ if (hasError) {
173
+ out.error = {
174
+ rev: report.rev,
175
+ message: typeof report.message === 'string' ? report.message : '',
176
+ }
177
+ }
178
+ // After a rollback the FAILED document's rev is the one to echo, not the
179
+ // restored one's: that is what stops the registry serving it again.
180
+ const held = sysGet('ota.cfg')
181
+ const heldRev =
182
+ typeof held === 'object' && held !== null && typeof held.rev === 'string' ? held.rev : ''
183
+ const rev = hasError ? report.rev : heldRev
184
+ if (rev !== '') out.rev = rev
185
+ return out
186
+ }
187
+
188
+ function sameConfig(held, next) {
189
+ const heldRev = typeof held.rev === 'string' ? held.rev : ''
190
+ const nextRev = typeof next.rev === 'string' ? next.rev : ''
191
+ if (heldRev !== nextRev || held.version !== next.version) return false
192
+ return JSON.stringify(held.doc) === JSON.stringify(next.doc)
193
+ }
194
+
195
+ // A document the app has read is a document the app ran with, which is what a
196
+ // later confirm() adopts on.
197
+ function markConfigRead() {
198
+ const trial = sysGet('ota.cfgTrial')
199
+ if (typeof trial !== 'object' || trial === null || trial.read === true) return
200
+ sysSet('ota.cfgTrial', {left: trial.left, read: true})
201
+ }
202
+
79
203
  // ── the in-memory build state ────────────────────────────────────────────────
80
204
 
81
205
  let current = {checksum: undefined, version: undefined, trial: false}
@@ -89,11 +213,22 @@ function install(checksum) {
89
213
  }
90
214
 
91
215
  export function running() {
92
- return {checksum: current.checksum, version: current.version, trial: current.trial}
216
+ // The version falls back to the manifest, the way mik__ota_policy_running
217
+ // falls back to read_app_version: an app needs it to stamp what it delivers.
218
+ const version = current.version === undefined ? manifestVersion() : current.version
219
+ return {checksum: current.checksum, version, trial: current.trial}
93
220
  }
94
221
 
95
222
  export function confirm() {
96
223
  current = {checksum: current.checksum, version: current.version, trial: false}
224
+ // One confirm settles both trials, as on device (mik__ota_policy_confirm).
225
+ // The config trial waits additionally for the app to have read the document.
226
+ const trial = sysGet('ota.cfgTrial')
227
+ const armed = typeof trial === 'object' && trial !== null
228
+ if (!armed || trial.read === true) {
229
+ sysRemove('ota.cfgPrev')
230
+ sysRemove('ota.cfgTrial')
231
+ }
97
232
  }
98
233
 
99
234
  export function revert() {
@@ -1 +1 @@
1
- {"version":3,"file":"ota-client.js","sourceRoot":"","sources":["../../../src/simulator/builtins/ota-client.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,6DAA6D;AAC7D,4EAA4E;AAC5E,gFAAgF;AAChF,yDAAyD;AACzD,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+JT;CACA,CAAA"}
1
+ {"version":3,"file":"ota-client.js","sourceRoot":"","sources":["../../../src/simulator/builtins/ota-client.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,6DAA6D;AAC7D,4EAA4E;AAC5E,gFAAgF;AAChF,yDAAyD;AACzD,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,6EAA6E;AAC7E,iDAAiD;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqST;CACA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikro",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Mikro.js: TypeScript for microcontrollers",
5
5
  "keywords": [
6
6
  "embedded",
@@ -97,11 +97,11 @@
97
97
  "tar": "^7.5.13",
98
98
  "uint8array-extras": "^1.5.0",
99
99
  "update-notifier": "^7.3.1",
100
- "@mikrojs/analyze-imports": "0.18.1",
101
- "@mikrojs/esptool": "0.18.1",
102
- "@mikrojs/firmware": "0.18.1",
103
- "@mikrojs/native": "0.18.1",
104
- "@mikrojs/quickjs": "0.18.1"
100
+ "@mikrojs/analyze-imports": "0.18.2",
101
+ "@mikrojs/firmware": "0.18.2",
102
+ "@mikrojs/native": "0.18.2",
103
+ "@mikrojs/quickjs": "0.18.2",
104
+ "@mikrojs/esptool": "0.18.2"
105
105
  },
106
106
  "devDependencies": {
107
107
  "@swc/core": "^1.15.30",