@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,296 @@
1
+ #include "mikrojs/ota_config.h"
2
+
3
+ #include <nanocbor/nanocbor.h>
4
+
5
+ #include <cstdio>
6
+ #include <cstdlib>
7
+ #include <cstring>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ #include "mikrojs/cbor_helpers.h"
12
+ #include "mikrojs/ota_slots.h"
13
+
14
+ namespace mikrojs {
15
+
16
+ namespace {
17
+
18
+ /* Copy `source`'s own enumerable string keys onto `target`, TOP LEVEL ONLY.
19
+ * This is the `{...defaults, ...doc}` spread and nothing more: a deviating
20
+ * value inside a nested unit ships the whole unit, so recursing here would
21
+ * merge across schema branches that have no common meaning. Returns false with
22
+ * the exception left pending. */
23
+ bool CopyOwnProps(JSContext* ctx, JSValue target, JSValue source) {
24
+ if (!JS_IsObject(source)) return true;
25
+ JSPropertyEnum* props = nullptr;
26
+ uint32_t count = 0;
27
+ if (JS_GetOwnPropertyNames(ctx, &props, &count, source,
28
+ JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) != 0) {
29
+ return false;
30
+ }
31
+ bool ok = true;
32
+ for (uint32_t i = 0; i < count; i++) {
33
+ JSValue value = JS_GetProperty(ctx, source, props[i].atom);
34
+ if (JS_IsException(value)) {
35
+ ok = false;
36
+ break;
37
+ }
38
+ /* Consumes `value`; the atom stays owned by the enum. */
39
+ if (JS_DefinePropertyValue(ctx, target, props[i].atom, value, JS_PROP_C_W_E) < 0) {
40
+ ok = false;
41
+ break;
42
+ }
43
+ }
44
+ JS_FreePropertyEnum(ctx, props, count);
45
+ return ok;
46
+ }
47
+
48
+ /* A fresh object carrying `source`'s top-level properties. Every read hands out
49
+ * one of these rather than the cached defaults themselves: an app is free to
50
+ * mutate what it got, and a mutation reaching the cache would poison every
51
+ * later read for the runtime's lifetime. */
52
+ JSValue ShallowCopy(JSContext* ctx, JSValue source) {
53
+ JSValue out = JS_NewObject(ctx);
54
+ if (JS_IsException(out)) return out;
55
+ if (!CopyOwnProps(ctx, out, source)) {
56
+ JS_FreeValue(ctx, out);
57
+ return JS_EXCEPTION;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ } // namespace
63
+
64
+ MIKOtaConfigReader::~MIKOtaConfigReader() {
65
+ if (has_last_good_ && last_good_ctx_) JS_FreeValue(last_good_ctx_, last_good_);
66
+ if (has_defaults_ && defaults_ctx_) JS_FreeValue(defaults_ctx_, defaults_);
67
+ }
68
+
69
+ JSValue MIKOtaConfigReader::ServeLastGood(JSContext* ctx) const {
70
+ if (!has_last_good_) return JS_UNDEFINED;
71
+ return JS_DupValue(ctx, last_good_);
72
+ }
73
+
74
+ void MIKOtaConfigReader::KeepLastGood(JSContext* ctx, JSValue value) {
75
+ if (has_last_good_ && last_good_ctx_) JS_FreeValue(last_good_ctx_, last_good_);
76
+ last_good_ = JS_DupValue(ctx, value);
77
+ last_good_ctx_ = ctx;
78
+ has_last_good_ = true;
79
+ }
80
+
81
+ JSValue MIKOtaConfigReader::ServeFallback(JSContext* ctx) {
82
+ JSValue held = ServeLastGood(ctx);
83
+ if (!JS_IsUndefined(held)) return held;
84
+ /* Nothing has read successfully yet this runtime, so there is nothing to
85
+ * hold on to: the defaults are the honest answer, and the flap the held
86
+ * value exists to prevent cannot happen before the first success. */
87
+ JSValue defaults = Defaults(ctx);
88
+ if (JS_IsUndefined(defaults)) {
89
+ return JS_ThrowPlainError(
90
+ ctx, "ota.config(): no config to read: the stored document could not be read and "
91
+ "this build carries no readable app manifest to take defaults from");
92
+ }
93
+ // Not kept as the last good value: only a read the store answered records
94
+ // one, so a recovered store still replaces the defaults with the document.
95
+ return ShallowCopy(ctx, defaults);
96
+ }
97
+
98
+ JSValue MIKOtaConfigReader::Read(JSContext* ctx) {
99
+ bool store_failed = false;
100
+ JSValue doc = LoadDoc(ctx, &store_failed);
101
+ bool serving_doc = !JS_IsUndefined(doc);
102
+
103
+ if (!store_failed) {
104
+ bool rolled_back = false;
105
+ if (!Account(serving_doc, &rolled_back) && serving_doc) {
106
+ /* The trial store could not answer, so nothing was written and the
107
+ * whole block retries on the next read. Serving the document
108
+ * meanwhile would run the app on values whose crash budget could
109
+ * not be charged, which is the one thing the accounting is for. */
110
+ JS_FreeValue(ctx, doc);
111
+ return ServeFallback(ctx);
112
+ }
113
+ if (rolled_back) {
114
+ /* The rollback replaced the stored document under us, and this very
115
+ * read returning the restored values is what breaks a crash loop. */
116
+ JS_FreeValue(ctx, doc);
117
+ doc = LoadDoc(ctx, &store_failed);
118
+ }
119
+ }
120
+ if (store_failed) {
121
+ JS_FreeValue(ctx, doc);
122
+ return ServeFallback(ctx);
123
+ }
124
+
125
+ JSValue defaults = Defaults(ctx); /* borrowed */
126
+ JSValue result;
127
+ if (JS_IsUndefined(defaults)) {
128
+ if (JS_IsUndefined(doc)) {
129
+ return JS_ThrowPlainError(
130
+ ctx, "ota.config(): no config to read: this build carries no readable app "
131
+ "manifest, so it has no config defaults. Deploy it with `mikro deploy`, "
132
+ "or run `mikro dev`");
133
+ }
134
+ /* No defaults to spread under it: the document stands alone, and it is
135
+ * decoded fresh on every read so the caller may mutate it. */
136
+ result = doc;
137
+ } else if (JS_IsUndefined(doc)) {
138
+ result = ShallowCopy(ctx, defaults);
139
+ } else {
140
+ result = JS_NewObject(ctx);
141
+ if (!JS_IsException(result) &&
142
+ !(CopyOwnProps(ctx, result, defaults) && CopyOwnProps(ctx, result, doc))) {
143
+ JS_FreeValue(ctx, result);
144
+ result = JS_EXCEPTION;
145
+ }
146
+ JS_FreeValue(ctx, doc);
147
+ }
148
+ if (JS_IsException(result)) return result;
149
+
150
+ KeepLastGood(ctx, result);
151
+ return result;
152
+ }
153
+
154
+ bool MIKOtaConfigReader::Account(bool serving_doc, bool* out_rolled_back) {
155
+ *out_rolled_back = false;
156
+
157
+ if (accounted_) {
158
+ if (!serving_doc) return true;
159
+ MIKOtaConfigTrial trial = {};
160
+ if (mik__ota_load_trial(env_, &trial) == MIK_OTA_KV_OK && !trial.read) {
161
+ /* A document delivered mid-boot: the boot was already accounted, but
162
+ * this read is what makes the trial adoptable. */
163
+ MIKOtaConfigTrial next = {trial.left, true};
164
+ mik__ota_store_trial(env_, next);
165
+ }
166
+ return true;
167
+ }
168
+
169
+ MIKOtaConfigTrial trial = {};
170
+ MIKOtaKvStatus status = mik__ota_load_trial(env_, &trial);
171
+ // A read that never happened must leave the trial untouched and retry on
172
+ // the next read, or a starved store silently burns the budget.
173
+ if (status == MIK_OTA_KV_ERROR) return false;
174
+
175
+ // Only a document this read actually serves is charged. A boot that fell
176
+ // back to the defaults (nothing stored, stamped for another version, bytes
177
+ // that would not decode) never ran the app on the document, and recording
178
+ // that it did is the defect this condition exists to prevent.
179
+ if (status == MIK_OTA_KV_OK && serving_doc) {
180
+ if (trial.left <= 0) {
181
+ // Reads come before the writes, so a failure here leaves the
182
+ // trial untouched and the whole block retries on the next read.
183
+ MIKOtaLoadedConfig failed = mik__ota_load_slot(env_, MIK_OTA_CFG_CURRENT);
184
+ if (failed.failed) return false;
185
+ MIKOtaLoadedConfig previous = mik__ota_load_slot(env_, MIK_OTA_CFG_PREV);
186
+ if (previous.failed) return false;
187
+
188
+ if (previous.present) {
189
+ mik__ota_store_slot(env_, MIK_OTA_CFG_CURRENT, previous.cfg);
190
+ } else {
191
+ mik__ota_clear_slot(env_, MIK_OTA_CFG_CURRENT);
192
+ }
193
+ mik__ota_clear_slot(env_, MIK_OTA_CFG_PREV);
194
+ mik__ota_clear_trial(env_);
195
+ *out_rolled_back = true;
196
+
197
+ if (failed.present && failed.cfg.rev[0]) {
198
+ MIKOtaConfigErrorReport report = {};
199
+ snprintf(report.rev, sizeof(report.rev), "%s", failed.cfg.rev);
200
+ snprintf(report.message, sizeof(report.message), "%s",
201
+ "rolled back: no completed check-in while the config was on trial");
202
+ mik__ota_store_config_error(env_, report);
203
+ }
204
+ } else {
205
+ // Burn one boot, and record that the app has read the document:
206
+ // adoption (in the client) waits for that, or an app reading
207
+ // config only at boot could have a never-executed document
208
+ // adopted under it, crash-looping at some later power cycle.
209
+ MIKOtaConfigTrial next = {trial.left - 1, true};
210
+ mik__ota_store_trial(env_, next);
211
+ }
212
+ }
213
+ // Only after the trial work fully succeeded: a retried read must never
214
+ // burn a second boot for the same power cycle. The boot has used its
215
+ // accounting slot even when there was no document to charge it against, so
216
+ // one delivered later in the same boot is marked read, not burned again.
217
+ accounted_ = true;
218
+ return true;
219
+ }
220
+
221
+ JSValue MIKOtaConfigReader::LoadDoc(JSContext* ctx, bool* out_failed) {
222
+ *out_failed = false;
223
+
224
+ MIKOtaLoadedConfig stored = mik__ota_load_slot(env_, MIK_OTA_CFG_CURRENT);
225
+ if (stored.failed) {
226
+ *out_failed = true;
227
+ return JS_UNDEFINED;
228
+ }
229
+ if (!stored.present || !stored.cfg.doc_cbor || stored.cfg.doc_cbor_len == 0) {
230
+ return JS_UNDEFINED;
231
+ }
232
+
233
+ char version[32] = {};
234
+ if (!env_ || !env_->read_app_version ||
235
+ !env_->read_app_version(env_->opaque, version, sizeof(version))) {
236
+ return JS_UNDEFINED;
237
+ }
238
+ // Stamped for another release: it was computed against that release's
239
+ // schema, so it says nothing about this one.
240
+ if (strcmp(stored.cfg.version, version) != 0) return JS_UNDEFINED;
241
+
242
+ // Decoded verbatim: the reader never walks into a value.
243
+ nanocbor_value_t it;
244
+ nanocbor_decoder_init(&it, stored.cfg.doc_cbor, stored.cfg.doc_cbor_len);
245
+ JSValue doc = mik__cbor_decode_value(ctx, &it, 0);
246
+ if (JS_IsException(doc)) {
247
+ // Bytes that will not decode are no more readable than none at all, and
248
+ // the defaults are a working config. Nothing is rewritten: the rev the
249
+ // device echoes is unchanged, so the registry re-serves the document.
250
+ JS_FreeValue(ctx, JS_GetException(ctx));
251
+ return JS_UNDEFINED;
252
+ }
253
+ return doc;
254
+ }
255
+
256
+ JSValue MIKOtaConfigReader::Defaults(JSContext* ctx) {
257
+ if (has_defaults_) return defaults_;
258
+
259
+ char* manifest = (env_ && env_->read_manifest) ? env_->read_manifest(env_->opaque) : nullptr;
260
+ if (!manifest) return JS_UNDEFINED;
261
+ JSValue parsed = JS_ParseJSON(ctx, manifest, strlen(manifest), "mikro.app.json");
262
+ free(manifest);
263
+ if (JS_IsException(parsed)) {
264
+ JS_FreeValue(ctx, JS_GetException(ctx));
265
+ return JS_UNDEFINED;
266
+ }
267
+
268
+ JSValue value = JS_GetPropertyStr(ctx, parsed, "configDefaults");
269
+ if (!JS_IsObject(value)) {
270
+ JS_FreeValue(ctx, value);
271
+ value = JS_UNDEFINED;
272
+ }
273
+ JS_FreeValue(ctx, parsed);
274
+ if (JS_HasException(ctx)) JS_FreeValue(ctx, JS_GetException(ctx));
275
+
276
+ if (JS_IsUndefined(value)) {
277
+ // A manifest without materialized defaults: the app declares no config
278
+ // schema, so an empty object is its config, not a failure. Pack writes
279
+ // `configDefaults` (possibly {}) whenever a schema is declared.
280
+ value = JS_NewObject(ctx);
281
+ if (JS_IsException(value)) {
282
+ JS_FreeValue(ctx, JS_GetException(ctx));
283
+ return JS_UNDEFINED;
284
+ }
285
+ }
286
+
287
+ // Cached on success only. A manifest that could not be read or parsed under
288
+ // heap pressure is retried on the next call rather than remembered as "this
289
+ // build has no defaults".
290
+ defaults_ = value;
291
+ defaults_ctx_ = ctx;
292
+ has_defaults_ = true;
293
+ return defaults_;
294
+ }
295
+
296
+ } // namespace mikrojs
@@ -0,0 +1,190 @@
1
+ #include "mikrojs/ota_js_hooks.h"
2
+
3
+ #include <map>
4
+
5
+ #include "mikrojs/utils.h"
6
+
7
+ namespace mikrojs {
8
+
9
+ namespace {
10
+
11
+ /* The hooks instance rides to the promise callbacks as a pair of integers in
12
+ * func_data, which carries JSValues and not pointers.
13
+ *
14
+ * A JS class with an opaque would be the tidier vehicle, but class ids are
15
+ * per-runtime and this unit has no per-runtime init to register one in — doing
16
+ * it lazily inside a call aborts the second runtime in a process. Two ints have
17
+ * no lifecycle at all.
18
+ *
19
+ * What travels is an id and not the address: a pending beforeCheck outlives the
20
+ * hooks object whenever watch() is called a second time or the client is torn
21
+ * down mid-round, and the allocator is free to hand the same address to the
22
+ * replacement. An id is never reused, so a continuation from a destroyed
23
+ * instance finds nothing rather than settling its successor. */
24
+ uint64_t next_hook_id() {
25
+ static uint64_t counter = 0;
26
+ return ++counter;
27
+ }
28
+
29
+ std::map<uint64_t, MIKOtaJsHooks*>& live_hooks() {
30
+ static std::map<uint64_t, MIKOtaJsHooks*> hooks;
31
+ return hooks;
32
+ }
33
+
34
+ void put_self(JSContext* ctx, uint64_t id, JSValue out[2]) {
35
+ out[0] = JS_NewUint32(ctx, static_cast<uint32_t>(id & 0xffffffffu));
36
+ out[1] = JS_NewUint32(ctx, static_cast<uint32_t>((id >> 32) & 0xffffffffu));
37
+ }
38
+
39
+ MIKOtaJsHooks* take_self(JSContext* ctx, JSValue* func_data) {
40
+ if (!func_data) return nullptr;
41
+ uint32_t low = 0;
42
+ uint32_t high = 0;
43
+ if (JS_ToUint32(ctx, &low, func_data[0]) < 0) return nullptr;
44
+ if (JS_ToUint32(ctx, &high, func_data[1]) < 0) return nullptr;
45
+ uint64_t id = (static_cast<uint64_t>(high) << 32) | low;
46
+ auto it = live_hooks().find(id);
47
+ return it == live_hooks().end() ? nullptr : it->second;
48
+ }
49
+
50
+ /* A Result is an object carrying a boolean `ok`. Told apart from a bare
51
+ * teardown, which is a function, and from nothing at all. */
52
+ bool is_result(JSContext* ctx, JSValueConst value) {
53
+ if (!JS_IsObject(value) || JS_IsFunction(ctx, value)) return false;
54
+ JSValue ok = JS_GetPropertyStr(ctx, value, "ok");
55
+ bool looks_like = JS_IsBool(ok);
56
+ JS_FreeValue(ctx, ok);
57
+ return looks_like;
58
+ }
59
+
60
+ } // namespace
61
+
62
+ MIKOtaJsHooks::MIKOtaJsHooks(JSContext* ctx, JSValue before_check)
63
+ : ctx_(ctx), before_check_(before_check), id_(next_hook_id()) {
64
+ live_hooks()[id_] = this;
65
+ }
66
+
67
+ MIKOtaJsHooks::~MIKOtaJsHooks() {
68
+ live_hooks().erase(id_);
69
+ JS_FreeValue(ctx_, before_check_);
70
+ JS_FreeValue(ctx_, teardown_);
71
+ }
72
+
73
+ bool MIKOtaJsHooks::has_teardown() const { return JS_IsFunction(ctx_, teardown_); }
74
+
75
+ bool MIKOtaJsHooks::BeginBeforeCheck() {
76
+ if (!JS_IsFunction(ctx_, before_check_)) return false;
77
+ awaiting_before_check_ = true;
78
+ state_ = MIKOtaHookState::kPending;
79
+ JS_FreeValue(ctx_, teardown_);
80
+ teardown_ = JS_UNDEFINED;
81
+ Call(before_check_);
82
+ return true;
83
+ }
84
+
85
+ MIKOtaHookState MIKOtaJsHooks::PollBeforeCheck() { return state_; }
86
+
87
+ bool MIKOtaJsHooks::BeginTeardown() {
88
+ if (!JS_IsFunction(ctx_, teardown_)) return false;
89
+ awaiting_before_check_ = false;
90
+ state_ = MIKOtaHookState::kPending;
91
+ /* Held across the call: settling clears it, and the call needs it alive. */
92
+ JSValue teardown = JS_DupValue(ctx_, teardown_);
93
+ Call(teardown);
94
+ JS_FreeValue(ctx_, teardown);
95
+ return true;
96
+ }
97
+
98
+ MIKOtaHookState MIKOtaJsHooks::PollTeardown() { return state_; }
99
+
100
+ void MIKOtaJsHooks::Call(JSValue fn) {
101
+ JSValue result = JS_Call(ctx_, fn, JS_UNDEFINED, 0, nullptr);
102
+ if (JS_IsException(result)) {
103
+ /* A hook that threw is a hook that failed. Report it on serial rather
104
+ * than leaving the exception on the context for something unrelated to
105
+ * trip over later. */
106
+ mik_dump_error(ctx_);
107
+ JS_FreeValue(ctx_, result);
108
+ state_ = MIKOtaHookState::kFailed;
109
+ return;
110
+ }
111
+
112
+ /* A hook may return nothing at all, and reading a property off undefined
113
+ * throws rather than answering — so ask what it is before asking anything
114
+ * of it. */
115
+ JSValue then = JS_IsObject(result) ? JS_GetPropertyStr(ctx_, result, "then") : JS_UNDEFINED;
116
+ if (!JS_IsFunction(ctx_, then)) {
117
+ JS_FreeValue(ctx_, then);
118
+ Settle(result);
119
+ JS_FreeValue(ctx_, result);
120
+ return;
121
+ }
122
+
123
+ JSValue data[2];
124
+ put_self(ctx_, id_, data);
125
+ JSValue on_ok = JS_NewCFunctionData(ctx_, OnFulfilled, 1, /*magic=*/1, 2, data);
126
+ JSValue on_err = JS_NewCFunctionData(ctx_, OnFulfilled, 1, /*magic=*/0, 2, data);
127
+ JS_FreeValue(ctx_, data[0]);
128
+ JS_FreeValue(ctx_, data[1]);
129
+
130
+ JSValue args[2] = {on_ok, on_err};
131
+ JSValue chained = JS_Call(ctx_, then, result, 2, args);
132
+ if (JS_IsException(chained)) {
133
+ JS_FreeValue(ctx_, JS_GetException(ctx_));
134
+ state_ = MIKOtaHookState::kFailed;
135
+ }
136
+ JS_FreeValue(ctx_, chained);
137
+ JS_FreeValue(ctx_, on_ok);
138
+ JS_FreeValue(ctx_, on_err);
139
+ JS_FreeValue(ctx_, then);
140
+ JS_FreeValue(ctx_, result);
141
+ }
142
+
143
+ void MIKOtaJsHooks::Settle(JSValue value) {
144
+ if (!awaiting_before_check_) {
145
+ /* Whatever a teardown returns is ignored: it has nothing left to say. */
146
+ state_ = MIKOtaHookState::kOk;
147
+ return;
148
+ }
149
+
150
+ /* A bare function is the teardown, with no Result to unwrap. */
151
+ if (JS_IsFunction(ctx_, value)) {
152
+ JS_FreeValue(ctx_, teardown_);
153
+ teardown_ = JS_DupValue(ctx_, value);
154
+ state_ = MIKOtaHookState::kOk;
155
+ return;
156
+ }
157
+
158
+ if (is_result(ctx_, value)) {
159
+ JSValue ok = JS_GetPropertyStr(ctx_, value, "ok");
160
+ bool succeeded = JS_ToBool(ctx_, ok);
161
+ JS_FreeValue(ctx_, ok);
162
+ if (!succeeded) {
163
+ state_ = MIKOtaHookState::kFailed;
164
+ return;
165
+ }
166
+ JS_FreeValue(ctx_, teardown_);
167
+ teardown_ = JS_GetPropertyStr(ctx_, value, "value");
168
+ state_ = MIKOtaHookState::kOk;
169
+ return;
170
+ }
171
+
172
+ /* Nothing, or something that is neither: the round runs, with no teardown. */
173
+ state_ = MIKOtaHookState::kOk;
174
+ }
175
+
176
+ JSValue MIKOtaJsHooks::OnFulfilled(JSContext* ctx, JSValueConst this_val, int argc,
177
+ JSValueConst* argv, int magic, JSValue* func_data) {
178
+ (void)this_val;
179
+ MIKOtaJsHooks* hooks = take_self(ctx, func_data);
180
+ if (!hooks) return JS_UNDEFINED;
181
+ if (magic == 0) {
182
+ /* Rejected: a failed hook, whichever half was running. */
183
+ hooks->state_ = MIKOtaHookState::kFailed;
184
+ return JS_UNDEFINED;
185
+ }
186
+ hooks->Settle(argc > 0 ? argv[0] : JS_UNDEFINED);
187
+ return JS_UNDEFINED;
188
+ }
189
+
190
+ } // namespace mikrojs