@mikrojs/native 0.18.0 → 0.18.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.
Files changed (68) hide show
  1. package/CMakeLists.txt +62 -1
  2. package/dist/index.d.ts +17 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/result/native-result.node-shim.d.ts +3 -0
  7. package/dist/runtime/result/native-result.node-shim.d.ts.map +1 -0
  8. package/dist/runtime/result/native-result.node-shim.js +41 -0
  9. package/dist/runtime/result/native-result.node-shim.js.map +1 -0
  10. package/dist/runtime/result/types.d.ts +55 -0
  11. package/dist/runtime/result/types.d.ts.map +1 -0
  12. package/dist/runtime/result/types.js +2 -0
  13. package/dist/runtime/result/types.js.map +1 -0
  14. package/dist/runtime/schema/core.d.ts +115 -0
  15. package/dist/runtime/schema/core.d.ts.map +1 -0
  16. package/dist/runtime/schema/core.js +259 -0
  17. package/dist/runtime/schema/core.js.map +1 -0
  18. package/dist/runtime/schema/shared.d.ts +54 -0
  19. package/dist/runtime/schema/shared.d.ts.map +1 -0
  20. package/dist/runtime/schema/shared.js +489 -0
  21. package/dist/runtime/schema/shared.js.map +1 -0
  22. package/dist/types.d.ts +7 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/include/mikrojs/cbor_helpers.h +20 -0
  25. package/include/mikrojs/mem.h +11 -0
  26. package/include/mikrojs/mikrojs.h +2 -1
  27. package/include/mikrojs/ota_client.h +342 -0
  28. package/include/mikrojs/ota_config.h +100 -0
  29. package/include/mikrojs/ota_env.h +192 -0
  30. package/include/mikrojs/ota_js_hooks.h +71 -0
  31. package/include/mikrojs/ota_policy.h +131 -0
  32. package/include/mikrojs/ota_slots.h +47 -0
  33. package/include/mikrojs/sys_codec.h +61 -0
  34. package/package.json +7 -5
  35. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  36. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  37. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  38. package/runtime/internal.d.ts +22 -16
  39. package/runtime/kv/shared.ts +11 -5
  40. package/runtime/kv/types.ts +4 -4
  41. package/runtime/ota/client.ts +12 -51
  42. package/runtime/ota/config.ts +18 -0
  43. package/runtime/ota/ota.ts +28 -70
  44. package/runtime/ota/types.ts +220 -2
  45. package/runtime/schema/core.ts +539 -0
  46. package/runtime/schema/schema.ts +36 -314
  47. package/runtime/schema/shared.ts +494 -0
  48. package/runtime/schema/types.ts +84 -12
  49. package/scripts/bundle-runtime.js +33 -0
  50. package/scripts/gen-checkin-fixtures.js +323 -0
  51. package/src/builtins.cpp +7 -8
  52. package/src/fs.cpp +3 -0
  53. package/src/mem.cpp +38 -0
  54. package/src/mik_abort.cpp +8 -1
  55. package/src/mik_cbor.cpp +43 -5
  56. package/src/mik_inspect.cpp +128 -22
  57. package/src/mik_ota_client.cpp +1230 -0
  58. package/src/mik_ota_config.cpp +296 -0
  59. package/src/mik_ota_js_hooks.cpp +190 -0
  60. package/src/mik_ota_policy.cpp +419 -0
  61. package/src/mik_ota_slots.cpp +249 -0
  62. package/src/mik_repl.cpp +9 -3
  63. package/src/mik_result.cpp +3 -1
  64. package/src/mik_sys_codec.cpp +167 -0
  65. package/src/mikrojs.cpp +15 -0
  66. package/src/modules.cpp +32 -13
  67. package/runtime/ota/client-impl.ts +0 -590
  68. package/runtime/ota/policy.ts +0 -299
@@ -0,0 +1,71 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * The `beforeCheck` hook, bridged from JS into the round machine.
5
+ *
6
+ * This is the one place the client re-enters JavaScript, and everything it
7
+ * touches comes back from app code — so it lives here, in the portable library,
8
+ * rather than in the firmware module. Nothing in it needs ESP-IDF, and every
9
+ * bug it has had so far was a marshalling bug that only a device could show.
10
+ */
11
+
12
+ #include <quickjs.h>
13
+
14
+ #include <cstdint>
15
+ #include <string>
16
+
17
+ #include "mikrojs/ota_client.h"
18
+
19
+ namespace mikrojs {
20
+
21
+ /**
22
+ * What a hook may hand back, and what each shape means:
23
+ *
24
+ * a function the round runs, and this runs after it
25
+ * nothing the round runs, with no teardown
26
+ * an `err` Result the round is skipped and retried sooner
27
+ * an `ok` Result as its value: a teardown function, or nothing
28
+ * a thrown exception the round is skipped and retried sooner
29
+ *
30
+ * Anything else is taken as success with no teardown. A promise of any of the
31
+ * above is awaited first.
32
+ *
33
+ * The bare-function form exists because wrapping every teardown in `ok()` is
34
+ * ceremony on the path that always succeeds, while the Result form is what lets
35
+ * a failing setup hand its own error straight back.
36
+ */
37
+ class MIKOtaJsHooks : public MIKOtaRoundHooks {
38
+ public:
39
+ /* Takes ownership of `before_check`, which may be undefined (no hook). */
40
+ MIKOtaJsHooks(JSContext* ctx, JSValue before_check);
41
+ ~MIKOtaJsHooks() override;
42
+ MIKOtaJsHooks(const MIKOtaJsHooks&) = delete;
43
+ MIKOtaJsHooks& operator=(const MIKOtaJsHooks&) = delete;
44
+
45
+ bool BeginBeforeCheck() override;
46
+ MIKOtaHookState PollBeforeCheck() override;
47
+ bool BeginTeardown() override;
48
+ MIKOtaHookState PollTeardown() override;
49
+
50
+ /* True once beforeCheck has handed back a teardown to run. */
51
+ bool has_teardown() const;
52
+
53
+ private:
54
+ void Call(JSValue fn);
55
+ /* Interpret a settled value from app code. */
56
+ void Settle(JSValue value);
57
+ static JSValue OnFulfilled(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
58
+ int magic, JSValue* func_data);
59
+
60
+ JSContext* ctx_;
61
+ JSValue before_check_;
62
+ JSValue teardown_ = JS_UNDEFINED;
63
+ MIKOtaHookState state_ = MIKOtaHookState::kOk;
64
+ bool awaiting_before_check_ = false;
65
+ /* Identifies this instance to a promise callback that outlives it. Never
66
+ * reused, so a continuation from a destroyed hooks object cannot land on
67
+ * whatever was allocated at the same address afterwards. */
68
+ uint64_t id_;
69
+ };
70
+
71
+ } // namespace mikrojs
@@ -0,0 +1,131 @@
1
+ #pragma once
2
+
3
+ #include <cstddef>
4
+ #include <cstdint>
5
+ #include <functional>
6
+ #include <memory>
7
+ #include <string>
8
+
9
+ #include "mikrojs/ota_env.h"
10
+
11
+ namespace mikrojs {
12
+
13
+ struct MIKOtaOffer {
14
+ std::string url;
15
+ std::string checksum;
16
+ size_t size = 0;
17
+ };
18
+
19
+ struct MIKOtaInstallOptions {
20
+ int trial_boots = 1;
21
+ bool require_confirm = false;
22
+ bool install_now = false;
23
+ };
24
+
25
+ enum class MIKOtaApplyOutcome {
26
+ kStaged,
27
+ kTrialPending,
28
+ kCurrent,
29
+ kAbandoned,
30
+ kExhausted,
31
+ };
32
+
33
+ const char* mik__ota_outcome_to_str(MIKOtaApplyOutcome outcome);
34
+
35
+ struct MIKOtaError {
36
+ // One of "StagingFailed", "TooLarge", "StagingFull", "DownloadFailed",
37
+ // "InstallFailed".
38
+ std::string name;
39
+ std::string kind; // "corrupt", "transient", "oom" (for InstallFailed)
40
+ std::string message;
41
+ };
42
+
43
+ class MIKOtaUpdate {
44
+ public:
45
+ virtual ~MIKOtaUpdate() = default;
46
+ virtual size_t resume_offset() const = 0;
47
+ virtual bool write(const uint8_t* bytes, size_t len, MIKOtaError* out_err) = 0;
48
+ virtual bool finish(const MIKOtaInstallOptions& options, MIKOtaError* out_err) = 0;
49
+ virtual void abort() = 0;
50
+ };
51
+
52
+ using MIKOtaDownloadFn = std::function<bool(MIKOtaUpdate& update, MIKOtaError* out_err)>;
53
+
54
+ class MIKOtaStore {
55
+ public:
56
+ explicit MIKOtaStore(const MIKOtaEnv* env) : env_(env) {}
57
+
58
+ /* The offer url is stored as a digest, not verbatim: a presigned url can run
59
+ * to several hundred bytes, past both the read buffer here and what the
60
+ * device's kv can hold, and a url that fails to round-trip never compares
61
+ * equal — which reset the retry budget on every round and let a build that
62
+ * cannot install retry forever. */
63
+ bool UrlMatches(const std::string& url) const;
64
+ void SetUrl(const std::string& url);
65
+
66
+ std::string GetAttempt() const;
67
+ void SetAttempt(const std::string& checksum);
68
+
69
+ int32_t GetTries() const;
70
+ void SetTries(int32_t n);
71
+
72
+ std::string GetBad() const;
73
+ void SetBad(const std::string& checksum);
74
+
75
+ bool GetInFlight() const;
76
+ void SetInFlight(bool in_flight);
77
+
78
+ private:
79
+ const MIKOtaEnv* env_;
80
+ };
81
+
82
+ /* Validate untrusted offer fields into a well-formed MIKOtaOffer */
83
+ bool mik__ota_parse_offer(const char* url, const char* checksum, int64_t size, bool allow_insecure,
84
+ MIKOtaOffer* out_offer, std::string* out_warn_reason);
85
+
86
+ /* Reconcile on-boot outcome and manage retry budget */
87
+ MIKOtaReconcileOutcome mik__ota_policy_reconcile(const MIKOtaEnv* env);
88
+
89
+ /* Query currently running build information */
90
+ MIKOtaRunningBuild mik__ota_policy_running(const MIKOtaEnv* env);
91
+
92
+ /* Revert current trial to last good build */
93
+ bool mik__ota_policy_revert(const MIKOtaEnv* env, MIKOtaError* out_err);
94
+
95
+ /* Confirm currently running build */
96
+ void mik__ota_policy_confirm(const MIKOtaEnv* env);
97
+
98
+ /* Apply an update offer according to the safety and retry policies. The
99
+ * download runs to completion inside the call; callers whose transport is
100
+ * asynchronous use the split form below instead. */
101
+ bool mik__ota_policy_apply_offer(const MIKOtaEnv* env, const MIKOtaOffer& offer,
102
+ MIKOtaDownloadFn download, const MIKOtaInstallOptions& options,
103
+ MIKOtaApplyOutcome* out_outcome, MIKOtaError* out_err);
104
+
105
+ /* One apply attempt, held open across an asynchronous download. `update` is
106
+ * null when the policy declined the offer before any staging began. */
107
+ struct MIKOtaApplySession {
108
+ const MIKOtaEnv* env = nullptr;
109
+ std::string checksum;
110
+ std::unique_ptr<MIKOtaUpdate> update;
111
+ };
112
+
113
+ /* Run the pre-download gates and, if the offer survives them, open the staging
114
+ * session. Returns false with `out_err` set only for StagingFailed; a declined
115
+ * offer returns true with `out_outcome` set and `session->update` left null.
116
+ *
117
+ * A session with an open `update` MUST be closed with mik__ota_policy_apply_end,
118
+ * or the in-flight flag stays set and the attempt is counted as one that took
119
+ * the device down — which is exactly what should happen if the caller dies. */
120
+ bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
121
+ MIKOtaApplySession* session, MIKOtaApplyOutcome* out_outcome,
122
+ MIKOtaError* out_err);
123
+
124
+ /* Close an attempt opened by mik__ota_policy_apply_begin. `download_message` is
125
+ * used only when `download_ok` is false, as the DownloadFailed message. */
126
+ bool mik__ota_policy_apply_end(MIKOtaApplySession* session, bool download_ok,
127
+ const std::string& download_message,
128
+ const MIKOtaInstallOptions& options,
129
+ MIKOtaApplyOutcome* out_outcome, MIKOtaError* out_err);
130
+
131
+ } // namespace mikrojs
@@ -0,0 +1,47 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * The config sync slots, as the device persists them.
5
+ *
6
+ * Three slots mirror the build's install slots: `current` is what the running
7
+ * build reads, `next` is staged alongside an offered build, `prev` is the
8
+ * rollback baseline while a delivery is unresolved. Shared by the check-in
9
+ * client and the `ota.config()` reader so the two agree on keys and shapes.
10
+ */
11
+
12
+ #include <cstdint>
13
+ #include <vector>
14
+
15
+ #include "mikrojs/ota_env.h"
16
+
17
+ namespace mikrojs {
18
+
19
+ /* A stored config, plus the buffer its `doc` span points into. */
20
+ struct MIKOtaLoadedConfig {
21
+ MIKOtaStoredConfig cfg = {};
22
+ std::vector<uint8_t> bytes;
23
+ /* A well-formed document was read. */
24
+ bool present = false;
25
+ /* The store could not answer. `present` is false but says nothing: callers
26
+ * that must not mistake a starved read for a clear check this first. */
27
+ bool failed = false;
28
+ };
29
+
30
+ MIKOtaLoadedConfig mik__ota_load_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot);
31
+ void mik__ota_store_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot,
32
+ const MIKOtaStoredConfig& cfg);
33
+ void mik__ota_clear_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot);
34
+
35
+ /* The running-release delivery trial; absent = no trial in progress. A
36
+ * malformed value reads as absent, but an unreadable store reads as ERROR: the
37
+ * config reader must not burn a trial boot on a read it never got. */
38
+ MIKOtaKvStatus mik__ota_load_trial(const MIKOtaEnv* env, MIKOtaConfigTrial* out);
39
+ void mik__ota_store_trial(const MIKOtaEnv* env, const MIKOtaConfigTrial& trial);
40
+ void mik__ota_clear_trial(const MIKOtaEnv* env);
41
+
42
+ /* The rolled-back document's rev and why, reported until replaced. */
43
+ bool mik__ota_load_config_error(const MIKOtaEnv* env, MIKOtaConfigErrorReport* out);
44
+ void mik__ota_store_config_error(const MIKOtaEnv* env, const MIKOtaConfigErrorReport& report);
45
+ void mik__ota_clear_config_error(const MIKOtaEnv* env);
46
+
47
+ } // namespace mikrojs
@@ -0,0 +1,61 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * Encodings for the runtime's persisted state, shared by its native and JS
5
+ * readers.
6
+ *
7
+ * native:mikro/nvs_kv stores each value as the CBOR encoding of the value
8
+ * itself, so `sysSet('ota.tries', 2)` writes a CBOR integer and
9
+ * `sysSet('ota.url', s)` writes a CBOR text string. Native readers of those same
10
+ * keys — the OTA policy store — have to agree byte for byte, or the C and JS
11
+ * implementations disagree about live device state.
12
+ *
13
+ * These helpers are that agreement, in the portable library so host tests can
14
+ * hold them to it.
15
+ */
16
+
17
+ #include <stdbool.h>
18
+ #include <stddef.h>
19
+ #include <stdint.h>
20
+
21
+ #ifdef __cplusplus
22
+ extern "C" {
23
+ #endif
24
+
25
+ /* Encode `value` as a bare CBOR text string. Returns the number of bytes the
26
+ * encoding needs, which may exceed out_len — nothing is written in that case, so
27
+ * a caller can measure with (NULL, 0) first. */
28
+ size_t mik__kv_encode_str(const char* value, uint8_t* out, size_t out_len);
29
+
30
+ /* Decode a bare CBOR text string into out (always NUL-terminated on success).
31
+ * False when the value is not a text string or does not fit. */
32
+ bool mik__kv_decode_str(const uint8_t* in, size_t in_len, char* out, size_t out_len);
33
+
34
+ /* Encode `value` as a bare CBOR integer. */
35
+ size_t mik__kv_encode_i32(int32_t value, uint8_t* out, size_t out_len);
36
+
37
+ /* Decode a bare CBOR integer. False when the value is not an integer that fits
38
+ * in an int32. */
39
+ bool mik__kv_decode_i32(const uint8_t* in, size_t in_len, int32_t* out);
40
+
41
+ /**
42
+ * The device name pair.
43
+ *
44
+ * The platform stores it as the JSON text `[rev]` or `[rev, name]` — one value,
45
+ * so the pair can never be read or written half-updated (see runtime/sys/sys.ts,
46
+ * which is the other end of this encoding). Revision 0 with no name means never
47
+ * named.
48
+ */
49
+
50
+ /* Parse the stored pair. False when the text is not a well-formed pair, which
51
+ * callers read as "never named" rather than as an error. `out_name` is set to the
52
+ * empty string when the name was cleared. */
53
+ bool mik__device_name_parse(const char* json, int* out_rev, char* out_name, size_t name_len);
54
+
55
+ /* Format a pair for storage. Pass NULL or "" as `name` to write `[rev]`.
56
+ * Returns the length the text needs, which may exceed out_len. */
57
+ size_t mik__device_name_format(int rev, const char* name, char* out, size_t out_len);
58
+
59
+ #ifdef __cplusplus
60
+ }
61
+ #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -64,12 +64,14 @@
64
64
  "./runtime/neopixel/types": "./runtime/neopixel/types.ts",
65
65
  "./runtime/observable/operators": "./runtime/observable/operators.ts",
66
66
  "./runtime/observable/types": "./runtime/observable/types.ts",
67
- "./runtime/ota/client-impl": "./runtime/ota/client-impl.ts",
68
67
  "./runtime/ota/types": "./runtime/ota/types.ts",
69
68
  "./runtime/pin/types": "./runtime/pin/types.ts",
70
69
  "./runtime/pwm/types": "./runtime/pwm/types.ts",
71
70
  "./runtime/reader/types": "./runtime/reader/types.ts",
71
+ "./runtime/result/native-result.node-shim": "./dist/runtime/result/native-result.node-shim.js",
72
72
  "./runtime/result/types": "./runtime/result/types.ts",
73
+ "./runtime/schema/core": "./dist/runtime/schema/core.js",
74
+ "./runtime/schema/shared": "./dist/runtime/schema/shared.js",
73
75
  "./runtime/schema/types": "./runtime/schema/types.ts",
74
76
  "./runtime/sleep/types": "./runtime/sleep/types.ts",
75
77
  "./runtime/sntp/types": "./runtime/sntp/types.ts",
@@ -86,14 +88,14 @@
86
88
  "cmake-js": "^8.0.0",
87
89
  "node-addon-api": "^8.7.0",
88
90
  "node-gyp-build": "^4.8.4",
89
- "@mikrojs/quickjs": "0.18.0"
91
+ "@mikrojs/quickjs": "0.18.1"
90
92
  },
91
93
  "devDependencies": {
92
94
  "@swc/core": "^1.15.30",
93
95
  "@types/node": "^24.12.2",
94
96
  "esbuild": "^0.28.0",
95
97
  "terser": "^5.46.2",
96
- "@mikrojs/registry": "0.18.0"
98
+ "@mikrojs/registry": "0.18.1"
97
99
  },
98
100
  "engines": {
99
101
  "node": ">=24.0.0"
@@ -102,7 +104,7 @@
102
104
  "bench": "cmake -B build -DBUILD_TESTING=ON && cmake --build build --target memory_bench &&./build/memory_bench",
103
105
  "build:native": "cmake-js compile --directory addon && node scripts/copy-prebuild.js",
104
106
  "build:native:debug": "cmake-js compile --debug --directory addon && node scripts/copy-prebuild.js",
105
- "build:ts": "tsc -p tsconfig.build.json",
107
+ "build:ts": "tsc -p tsconfig.build.json && tsc -p tsconfig.runtime-build.json",
106
108
  "clean": "cmake-js clean --directory addon && rm -rf prebuilds",
107
109
  "compare-minifiers": "node scripts/compare-minifiers.js --open",
108
110
  "publint": "publint",
@@ -11,6 +11,10 @@ declare module 'mikro/kv/shared' {
11
11
  export {KVError, makeCreateValue, mapKvError, type NativeKvFns} from './kv/shared.js'
12
12
  }
13
13
 
14
+ declare module 'mikro/ota/config' {
15
+ export {config} from './ota/config.js'
16
+ }
17
+
14
18
  declare module 'mikro/observable/lazy' {
15
19
  export {lazyEvent} from './observable/lazy.js'
16
20
  }
@@ -528,22 +532,24 @@ declare module 'native:mikro/udp' {
528
532
  export function bind(opts: BindOptions): Promise<Result<NativeUdpSocket, UdpError>>
529
533
  }
530
534
 
531
- declare module 'native:mikro/ota' {
532
- import type {NativeOta} from './ota/policy.js'
533
-
534
- // The native module exports the NativeOta contract's methods directly.
535
- export function stageBegin(checksum: string, size: number): ReturnType<NativeOta['stageBegin']>
536
- export function stageWrite(bytes: Uint8Array): ReturnType<NativeOta['stageWrite']>
537
- export function stageFinish(
538
- trialBoots: number,
539
- requireConfirm: boolean,
540
- installNow: boolean,
541
- ): ReturnType<NativeOta['stageFinish']>
542
- export function stageAbort(): void
543
- export function markValid(): void
544
- export function revert(): ReturnType<NativeOta['revert']>
545
- export function running(): ReturnType<NativeOta['running']>
546
- export function reconcile(): ReturnType<NativeOta['reconcile']>
535
+ declare module 'native:mikro/ota_client' {
536
+ import type {CheckOptions, CheckResult, Ota, Watcher, WatchOptions} from './ota/types.js'
537
+
538
+ // Backed by src/mik_ota_client.cpp, src/mik_ota_config.cpp and
539
+ // src/mik_ota_policy.cpp.
540
+ export function check(options?: CheckOptions): Promise<CheckResult>
541
+ export function watch(options?: WatchOptions): Watcher
542
+ export function config(): unknown
543
+
544
+ // The mikro/ota policy surface.
545
+ export const reconcile: Ota['reconcile']
546
+ export const running: Ota['running']
547
+ export const parseOffer: Ota['parseOffer']
548
+ export const applyOffer: Ota['applyOffer']
549
+ export const confirm: Ota['confirm']
550
+ export const revert: Ota['revert']
551
+ export const bearer: Ota['bearer']
552
+ export const registry: Ota['registry']
547
553
  }
548
554
 
549
555
  declare module 'native:mikro/i2s' {
@@ -71,11 +71,17 @@ export function makeCreateValue(native: NativeKvFns) {
71
71
  }
72
72
  return v
73
73
  } catch (e) {
74
- // Decode failure: corrupt data, delete it
75
- native.remove(key)
76
- const fallback = onReadError(e)
77
- if (fallback !== undefined) native.set(key, fallback)
78
- return fallback
74
+ // TypeError is the native's corruption marker (stored bytes that do
75
+ // not decode): delete and heal. Anything else is a transient read
76
+ // failure (nvs open/read starved of heap) — the stored value is
77
+ // intact, so surface the error without touching it.
78
+ if (e instanceof TypeError) {
79
+ native.remove(key)
80
+ const fallback = onReadError(e)
81
+ if (fallback !== undefined) native.set(key, fallback)
82
+ return fallback
83
+ }
84
+ return onReadError(e)
79
85
  }
80
86
  }
81
87
 
@@ -64,7 +64,7 @@ export type KVOptions<S> =
64
64
  /** Called when reading stored data fails (decode error, schema mismatch).
65
65
  * On decode failure, corrupt data is deleted. On schema mismatch, stored data is kept.
66
66
  * Return a fallback value, or undefined. Default: `() => undefined`. */
67
- onReadError: (error: KVError | SchemaError) => Infer<S>
67
+ onReadError: (error: KVError | SchemaError | Error) => Infer<S>
68
68
  }
69
69
  | {
70
70
  schema: OptionalSchema
@@ -72,7 +72,7 @@ export type KVOptions<S> =
72
72
  /** Called when reading stored data fails (decode error, schema mismatch).
73
73
  * On decode failure, corrupt data is deleted. On schema mismatch, stored data is kept.
74
74
  * Return a fallback value, or undefined. Default: `() => undefined`. */
75
- onReadError?: (error: KVError | SchemaError) => Infer<S>
75
+ onReadError?: (error: KVError | SchemaError | Error) => Infer<S>
76
76
  }
77
77
  | {
78
78
  schema?: never
@@ -80,7 +80,7 @@ export type KVOptions<S> =
80
80
  /** Called when reading stored data fails (decode error, schema mismatch).
81
81
  * On decode failure, corrupt data is deleted. On schema mismatch, stored data is kept.
82
82
  * Return a fallback value, or undefined. Default: `() => undefined`. */
83
- onReadError: (error: KVError) => unknown
83
+ onReadError: (error: KVError | Error) => unknown
84
84
  }
85
85
  | {
86
86
  schema?: never
@@ -88,7 +88,7 @@ export type KVOptions<S> =
88
88
  /** Called when reading stored data fails (decode error, schema mismatch).
89
89
  * On decode failure, corrupt data is deleted. On schema mismatch, stored data is kept.
90
90
  * Return a fallback value, or undefined. Default: `() => undefined`. */
91
- onReadError?: (error: KVError) => unknown
91
+ onReadError?: (error: KVError | Error) => unknown
92
92
  }
93
93
 
94
94
  export interface KVValue<T> {
@@ -1,20 +1,15 @@
1
- import {decode, encode} from 'mikro/cbor'
2
- import {request} from 'mikro/http/request'
3
- import {ota} from 'mikro/ota'
4
- import {sleep} from 'mikro/sleep'
5
- import {
6
- deviceId,
7
- deviceName,
8
- firmware,
9
- restart,
10
- setDeviceName,
11
- storageUsage,
12
- version,
13
- } from 'mikro/sys'
1
+ // The OTA check-in client. The state machine lives in C
2
+ // (src/mik_ota_client.cpp); this is the app-facing surface over it.
3
+ //
4
+ // There is deliberately no logic here. Every decision — the retry budget, the
5
+ // trial gates, the config slots, the jittered cadence, the download pump with
6
+ // its resume — is in the portable library, where host tests drive it against a
7
+ // fake platform.
14
8
 
15
- import {type ClientIo, createOtaClient, type LogLevel} from './client-impl.js'
9
+ import {check as nativeCheck, watch as nativeWatch} from 'native:mikro/ota_client'
16
10
 
17
11
  export type {
12
+ BeforeCheckResult,
18
13
  CheckError,
19
14
  CheckOptions,
20
15
  CheckResult,
@@ -22,41 +17,7 @@ export type {
22
17
  Teardown,
23
18
  Watcher,
24
19
  WatchOptions,
25
- } from './client-impl.js'
26
-
27
- /* eslint-disable no-console -- serial diagnostics are the client's only output channel */
28
- const consoleFor: Record<LogLevel, (format: string, ...args: unknown[]) => void> = {
29
- debug: (format, ...args) => console.debug(format, ...args),
30
- info: (format, ...args) => console.log(format, ...args),
31
- warn: (format, ...args) => console.warn(format, ...args),
32
- error: (format, ...args) => console.error(format, ...args),
33
- }
34
- /* eslint-enable no-console */
35
-
36
- /** The real device. Every runtime symbol the client uses is bound here and
37
- * nowhere else; `ota` members go through arrows because they are methods on
38
- * the builtin singleton. */
39
- const deviceIo: ClientIo = {
40
- sleep,
41
- request,
42
- random: Math.random,
43
- log: (level, format, ...args) => consoleFor[level](format, ...args),
44
- encode,
45
- decode,
46
- ota,
47
- identity: () => ({
48
- deviceId,
49
- firmware: version,
50
- firmwareHash: firmware.hash,
51
- bytecode: firmware.bytecodeVersion,
52
- }),
53
- storageFree: () => storageUsage()?.free,
54
- deviceName,
55
- setDeviceName,
56
- restart,
57
- }
58
-
59
- const client = createOtaClient(deviceIo)
20
+ } from './types.js'
60
21
 
61
22
  /**
62
23
  * One-shot update check for wake-cycle apps: check in with the registry,
@@ -65,7 +26,7 @@ const client = createOtaClient(deviceIo)
65
26
  * in-flight work is done. Connectivity is the app's business: call this with
66
27
  * the network already up.
67
28
  */
68
- export const check = client.check
29
+ export const check = nativeCheck
69
30
 
70
31
  /**
71
32
  * Periodic update checks for always-on apps: a detached background loop that
@@ -74,4 +35,4 @@ export const check = client.check
74
35
  * per round (its returned teardown runs after the round). Do not combine with
75
36
  * `check()` — the two modes are alternatives, one per app.
76
37
  */
77
- export const watch = client.watch
38
+ export const watch = nativeWatch
@@ -0,0 +1,18 @@
1
+ // `ota.config()`, backed by the C reader (src/mik_ota_config.cpp).
2
+ //
3
+ // Its own module rather than part of `mikro/ota`: an app that only reads config
4
+ // should not also pay for the policy surface, which most apps never touch.
5
+
6
+ import {config as nativeConfig} from 'native:mikro/ota_client'
7
+
8
+ import type {RegisteredConfig} from './types.js'
9
+
10
+ /**
11
+ * The effective config for the running build: the manifest defaults with the
12
+ * stored document spread over them, top level only. Always an object, and a
13
+ * fresh one on every call.
14
+ *
15
+ * Throws when there is nothing to serve: a build with no readable manifest and
16
+ * no stored document, which is a build that never went through `mikro deploy`.
17
+ */
18
+ export const config = nativeConfig as <T = RegisteredConfig>() => T