@mikrojs/native 0.20.1 → 0.21.0-next.20260913195055

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 (56) hide show
  1. package/CMakeLists.txt +3 -1
  2. package/cmake/mikrojs_bytecode.cmake +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/runtime/result/native-result.node-shim.js.map +1 -1
  6. package/dist/runtime/result/types.d.ts +5 -5
  7. package/dist/runtime/result/types.d.ts.map +1 -1
  8. package/dist/types.d.ts +1 -1
  9. package/dist/types.d.ts.map +1 -1
  10. package/include/mikrojs/mikrojs.h +26 -2
  11. package/include/mikrojs/ota_env.h +8 -6
  12. package/include/mikrojs/ota_policy.h +16 -0
  13. package/include/mikrojs/private.h +13 -0
  14. package/include/mikrojs/utils.h +24 -0
  15. package/package.json +10 -10
  16. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  17. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  18. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  19. package/runtime/gpio/types.ts +116 -0
  20. package/runtime/i2c/types.ts +25 -31
  21. package/runtime/i2s/types.ts +23 -12
  22. package/runtime/internal.d.ts +1 -174
  23. package/runtime/kv/shared.ts +13 -11
  24. package/runtime/neopixel/types.ts +24 -6
  25. package/runtime/observable/native-observable.node-shim.ts +73 -37
  26. package/runtime/observable/native-operators.node-shim.ts +417 -0
  27. package/runtime/observable/observable.ts +76 -1
  28. package/runtime/observable/operators.ts +156 -89
  29. package/runtime/observable/types.ts +61 -6
  30. package/runtime/pwm/types.ts +31 -11
  31. package/runtime/result/native-result.node-shim.ts +1 -1
  32. package/runtime/result/types.ts +5 -5
  33. package/runtime/sleep/types.ts +10 -8
  34. package/runtime/spi/types.ts +16 -14
  35. package/runtime/uart/types.ts +27 -15
  36. package/src/gpio_claim.cpp +63 -0
  37. package/src/mik_abort.cpp +1 -2
  38. package/src/mik_console.cpp +192 -115
  39. package/src/mik_http_client.cpp +3 -14
  40. package/src/mik_inspect.cpp +86 -9
  41. package/src/mik_observable.cpp +1028 -80
  42. package/src/mik_ota_client.cpp +5 -2
  43. package/src/mik_ota_policy.cpp +65 -18
  44. package/src/mik_result.cpp +10 -9
  45. package/src/mikrojs.cpp +26 -13
  46. package/src/modules.cpp +17 -6
  47. package/src/timers.cpp +6 -2
  48. package/src/utils.cpp +106 -0
  49. package/runtime/i2c/i2c.ts +0 -35
  50. package/runtime/i2s/i2s.ts +0 -46
  51. package/runtime/neopixel/neopixel.ts +0 -38
  52. package/runtime/pin/pin.ts +0 -51
  53. package/runtime/pin/types.ts +0 -49
  54. package/runtime/pwm/pwm.ts +0 -32
  55. package/runtime/spi/spi.ts +0 -31
  56. package/runtime/uart/uart.ts +0 -52
@@ -4,11 +4,21 @@
4
4
  *
5
5
  * Classes registered:
6
6
  * - Observable: constructor(cb), subscribe(observer), pipe(...ops),
7
- * static from(src), static withEmitters()
7
+ * static withEmitters()
8
+ * (cb may return a function; it is registered as a teardown)
9
+ * - from(src): module function, promise or iterable -> Observable
10
+ * - of(...values): module function, from() over the argument array
8
11
  * - Subscriber (internal handle passed to subscribe callback): next, complete,
9
12
  * addTeardown, closed
10
13
  * - Subscription: unsubscribe
11
14
  *
15
+ * A second module, mikro/observable/operators, holds every operator (map,
16
+ * filter, take, timer, switchMap, debounceTime, ...). It is loaded lazily
17
+ * through the C-module table in modules.cpp, so runtimes that never import
18
+ * it pay nothing. Per-subscription operator state lives in a C struct and
19
+ * upstream subscribers dispatch straight into C handlers, so a chain costs no
20
+ * JS closures per value or per subscription and no bytecode per app.
21
+ *
12
22
  * Error semantics: a throw inside an observer, operator, or teardown callback
13
23
  * is caught at the dispatch boundary only to report it and apply the app's
14
24
  * onPanic policy; it is an application crash like any other uncaught error.
@@ -53,6 +63,7 @@
53
63
  #include <cstdint>
54
64
  #include <vector>
55
65
 
66
+ #include "mikrojs/platform.h"
56
67
  #include "mikrojs/private.h"
57
68
  #include "mikrojs/utils.h"
58
69
 
@@ -95,7 +106,14 @@ struct SubscriberData {
95
106
  JSValue observer;
96
107
  JSValue next_fn;
97
108
  JSValue complete_fn;
109
+ /* Teardowns are functions, or upstream Subscriber objects that are
110
+ * unsubscribed in place of a call (see run_teardown_entry). */
98
111
  std::vector<JSValue> teardowns;
112
+ /* Set for a native operator's upstream subscriber: deliveries go to
113
+ * op_on_next/op_on_complete with this state instead of next_fn/complete_fn.
114
+ * Released once the subscriber closes. */
115
+ JSValue op_state = JS_UNDEFINED;
116
+ int op_index = 0;
99
117
  };
100
118
 
101
119
  struct SubscriptionData {
@@ -123,15 +141,19 @@ static void panic(JSContext* ctx, JSValue exception) {
123
141
  JS_FreeValue(ctx, exception);
124
142
  }
125
143
 
126
- /* Call `fn(argv...)` synchronously; a throw panics. Caller is not informed. */
127
- static void run_safely(JSContext* ctx, JSValue fn, int argc, JSValue* argv) {
144
+ /* Call `fn(argv...)` synchronously and return its result (owned); a throw
145
+ * panics and yields JS_EXCEPTION. */
146
+ static JSValue call_or_panic(JSContext* ctx, JSValueConst fn, int argc, JSValueConst* argv) {
128
147
  JSValue ret = JS_Call(ctx, fn, JS_UNDEFINED, argc, argv);
129
148
  if (JS_IsException(ret)) {
130
- JSValue exc = JS_GetException(ctx);
131
- panic(ctx, exc);
132
- } else {
133
- JS_FreeValue(ctx, ret);
149
+ panic(ctx, JS_GetException(ctx));
134
150
  }
151
+ return ret;
152
+ }
153
+
154
+ /* Call `fn(argv...)` synchronously; a throw panics. Caller is not informed. */
155
+ static void run_safely(JSContext* ctx, JSValue fn, int argc, JSValue* argv) {
156
+ JS_FreeValue(ctx, call_or_panic(ctx, fn, argc, argv));
135
157
  }
136
158
 
137
159
  /* Same panic-on-throw semantics as run_safely, for invoke-by-method calls
@@ -148,21 +170,63 @@ static void invoke_safely(JSContext* ctx, JSValueConst this_val, JSAtom method,
148
170
  }
149
171
  }
150
172
 
173
+ static void close_subscriber(JSContext* ctx, SubscriberData* d);
174
+ static bool op_teardown(JSContext* ctx, JSValueConst entry);
175
+
176
+ /* A teardown entry is a function to call, an upstream subscriber to close, or
177
+ * an operator state whose timer and inner subscription end with it. */
178
+ static void run_teardown_entry(JSContext* ctx, JSValueConst entry) {
179
+ auto* up = static_cast<SubscriberData*>(JS_GetOpaque(entry, subscriber_class_id));
180
+ if (up) {
181
+ close_subscriber(ctx, up);
182
+ } else if (!op_teardown(ctx, entry)) {
183
+ run_safely(ctx, entry, 0, nullptr);
184
+ }
185
+ }
186
+
151
187
  /* Run all registered teardowns in reverse insertion order. A throw panics,
152
188
  * but the remaining teardowns still run: they release resources for work
153
189
  * that is already ending. */
154
190
  static void run_teardowns(JSContext* ctx, SubscriberData* d) {
155
191
  /* Swap into a local list. If a teardown calls addTeardown synchronously,
156
192
  * the SubscriberData.closed flag is already true so addTeardown fires the
157
- * new callback immediately (handled in subscriber_add_teardown). */
193
+ * new callback immediately (handled in add_teardown). */
158
194
  std::vector<JSValue> list;
159
195
  list.swap(d->teardowns);
160
196
  for (auto it = list.rbegin(); it != list.rend(); ++it) {
161
- run_safely(ctx, *it, 0, nullptr);
197
+ run_teardown_entry(ctx, *it);
162
198
  JS_FreeValue(ctx, *it);
163
199
  }
164
200
  }
165
201
 
202
+ /* Register `entry` (owned) as a teardown; on a closed subscriber it runs at
203
+ * once instead, so a late registration can never leak. */
204
+ static void add_teardown(JSContext* ctx, SubscriberData* d, JSValue entry) {
205
+ if (d->closed) {
206
+ run_teardown_entry(ctx, entry);
207
+ JS_FreeValue(ctx, entry);
208
+ } else {
209
+ d->teardowns.push_back(entry);
210
+ }
211
+ }
212
+
213
+ static void release_op_state(JSContext* ctx, SubscriberData* d) {
214
+ JSValue state = d->op_state;
215
+ d->op_state = JS_UNDEFINED;
216
+ JS_FreeValue(ctx, state);
217
+ }
218
+
219
+ /* Silent close: teardowns run, the observer is not told. Idempotent. A
220
+ * pending complete owns the close: its queued entry delivers complete_fn +
221
+ * teardowns, matching the recursive-dispatch order where the complete had
222
+ * already run before unsubscribe could. */
223
+ static void close_subscriber(JSContext* ctx, SubscriberData* d) {
224
+ if (d->closed || d->complete_pending) return;
225
+ d->closed = true;
226
+ run_teardowns(ctx, d);
227
+ release_op_state(ctx, d);
228
+ }
229
+
166
230
  /* ── Dispatch trampoline ────────────────────────────────────────── */
167
231
 
168
232
  static MIKObservableDispatch* dispatch_state(JSContext* ctx) {
@@ -178,9 +242,18 @@ static bool dispatch_stopped(JSContext* ctx) {
178
242
  return mik_rt && MIK_IsStopRequested(mik_rt);
179
243
  }
180
244
 
245
+ static void op_on_next(JSContext* ctx, JSValueConst state, int index, JSValueConst value);
246
+ static void op_on_complete(JSContext* ctx, JSValueConst state, int index, SubscriberData* from);
247
+
181
248
  /* Deliver a value to the subscriber's next handler. `value` is borrowed. */
182
249
  static void deliver_next(JSContext* ctx, SubscriberData* d, JSValue value) {
183
- if (!JS_IsUndefined(d->next_fn)) {
250
+ if (!JS_IsUndefined(d->op_state)) {
251
+ /* Hold the state: the handler may close this subscriber, which
252
+ * releases d->op_state. */
253
+ JSValue state = JS_DupValue(ctx, d->op_state);
254
+ op_on_next(ctx, state, d->op_index, value);
255
+ JS_FreeValue(ctx, state);
256
+ } else if (!JS_IsUndefined(d->next_fn)) {
184
257
  run_safely(ctx, d->next_fn, 1, &value);
185
258
  }
186
259
  }
@@ -188,10 +261,15 @@ static void deliver_next(JSContext* ctx, SubscriberData* d, JSValue value) {
188
261
  static void deliver_complete(JSContext* ctx, SubscriberData* d) {
189
262
  d->complete_pending = false;
190
263
  d->closed = true;
191
- if (!JS_IsUndefined(d->complete_fn)) {
264
+ if (!JS_IsUndefined(d->op_state)) {
265
+ JSValue state = JS_DupValue(ctx, d->op_state);
266
+ op_on_complete(ctx, state, d->op_index, d);
267
+ JS_FreeValue(ctx, state);
268
+ } else if (!JS_IsUndefined(d->complete_fn)) {
192
269
  run_safely(ctx, d->complete_fn, 0, nullptr);
193
270
  }
194
271
  run_teardowns(ctx, d);
272
+ release_op_state(ctx, d);
195
273
  }
196
274
 
197
275
  /* Drain queued deliveries in FIFO order. Deliveries may enqueue more; the
@@ -231,6 +309,7 @@ static void subscriber_finalizer(JSRuntime* rt, JSValue val) {
231
309
  JS_FreeValueRT(rt, d->observer);
232
310
  JS_FreeValueRT(rt, d->next_fn);
233
311
  JS_FreeValueRT(rt, d->complete_fn);
312
+ JS_FreeValueRT(rt, d->op_state);
234
313
  for (auto& td : d->teardowns) JS_FreeValueRT(rt, td);
235
314
  delete d;
236
315
  }
@@ -241,6 +320,7 @@ static void subscriber_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_fun
241
320
  JS_MarkValue(rt, d->observer, mark_func);
242
321
  JS_MarkValue(rt, d->next_fn, mark_func);
243
322
  JS_MarkValue(rt, d->complete_fn, mark_func);
323
+ JS_MarkValue(rt, d->op_state, mark_func);
244
324
  for (auto& td : d->teardowns) JS_MarkValue(rt, td, mark_func);
245
325
  }
246
326
 
@@ -306,12 +386,7 @@ static JSValue subscriber_add_teardown(JSContext* ctx, JSValueConst this_val, in
306
386
  if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
307
387
  return JS_ThrowTypeError(ctx, "addTeardown: argument must be a function");
308
388
  }
309
- if (d->closed) {
310
- /* Late teardown registration — fire immediately to avoid leaks. */
311
- run_safely(ctx, argv[0], 0, nullptr);
312
- } else {
313
- d->teardowns.push_back(JS_DupValue(ctx, argv[0]));
314
- }
389
+ add_teardown(ctx, d, JS_DupValue(ctx, argv[0]));
315
390
  return JS_UNDEFINED;
316
391
  }
317
392
 
@@ -360,14 +435,9 @@ static JSValue subscription_unsubscribe(JSContext* ctx, JSValueConst this_val, i
360
435
  if (!sd) return JS_EXCEPTION;
361
436
  auto* sub = static_cast<SubscriberData*>(JS_GetOpaque(sd->subscriber_value,
362
437
  subscriber_class_id));
363
- /* A pending complete owns the close: its queued entry delivers
364
- * complete_fn + teardowns, matching the recursive-dispatch order where
365
- * the complete had already run before unsubscribe could. */
366
- if (!sub || sub->closed || sub->complete_pending) return JS_UNDEFINED;
367
- sub->closed = true;
368
438
  /* unsubscribe() is silent — does NOT call observer.complete().
369
439
  * Only natural producer-driven completion fires observer.complete(). */
370
- run_teardowns(ctx, sub);
440
+ if (sub) close_subscriber(ctx, sub);
371
441
  return JS_UNDEFINED;
372
442
  }
373
443
 
@@ -411,6 +481,43 @@ static JSClassDef observable_class_def = {
411
481
  nullptr,
412
482
  };
413
483
 
484
+ /* Run the producer against a freshly built subscriber. Sync emission is
485
+ * allowed: the producer may call next/complete inside the callback, so the
486
+ * subscriber may already be closed on return; idempotent unsubscribe handles
487
+ * it. Returns subscriber_val (ownership passes back), or JS_EXCEPTION with it
488
+ * freed. */
489
+ static JSValue start_subscriber(JSContext* ctx, JSValueConst subscribe_cb, JSValue subscriber_val,
490
+ SubscriberData* d) {
491
+ JSValue cb_result = JS_Call(ctx, subscribe_cb, JS_UNDEFINED, 1, &subscriber_val);
492
+ if (JS_IsException(cb_result)) {
493
+ /* Producer setup threw — bubble up, but release whatever it already
494
+ * acquired first. No Subscription reaches the caller, so a teardown
495
+ * skipped here can never run: the handle it would close is
496
+ * unreachable for the rest of the runtime's life. Park the pending
497
+ * exception while the teardowns run, since they are JS calls and
498
+ * must not inherit it, then restore it for the caller.
499
+ * A queued completion is different: that entry owns the close and
500
+ * runs the teardowns when it drains. */
501
+ if (!d->complete_pending) {
502
+ d->closed = true;
503
+ JSValue pending = JS_GetException(ctx);
504
+ run_teardowns(ctx, d);
505
+ JS_Throw(ctx, pending);
506
+ }
507
+ JS_FreeValue(ctx, subscriber_val);
508
+ return cb_result;
509
+ }
510
+ if (JS_IsFunction(ctx, cb_result)) {
511
+ /* A returned function is a teardown, exactly as if the producer had
512
+ * passed it to addTeardown() last (owned; runs now if setup already
513
+ * closed the subscriber). */
514
+ add_teardown(ctx, d, cb_result);
515
+ } else {
516
+ JS_FreeValue(ctx, cb_result);
517
+ }
518
+ return subscriber_val;
519
+ }
520
+
414
521
  /* Run a subscribe callback against an observer, return a Subscription.
415
522
  * subscribe_cb is borrowed (not freed). Observer is borrowed.
416
523
  * Used by both Observable.prototype.subscribe and the static factories. */
@@ -461,34 +568,10 @@ static JSValue subscribe_with_callback(JSContext* ctx, JSValueConst subscribe_cb
461
568
  };
462
569
  JS_SetOpaque(subscriber_val, d);
463
570
 
464
- /* Sync emission is allowed: producer may call next/complete inside the
465
- * subscribe callback. The Subscription returned to the caller may already
466
- * be in a closed state by the time we return it — that's fine, idempotent
467
- * unsubscribe handles it. */
468
- JSValue cb_result = JS_Call(ctx, subscribe_cb, JS_UNDEFINED, 1, &subscriber_val);
469
- if (JS_IsException(cb_result)) {
470
- /* Producer setup threw — bubble up, but release whatever it already
471
- * acquired first. No Subscription reaches the caller, so a teardown
472
- * skipped here can never run: the handle it would close is
473
- * unreachable for the rest of the runtime's life. Park the pending
474
- * exception while the teardowns run, since they are JS calls and
475
- * must not inherit it, then restore it for the caller.
476
- * A queued completion is different: that entry owns the close and
477
- * runs the teardowns when it drains. */
478
- if (!d->complete_pending) {
479
- d->closed = true;
480
- JSValue pending = JS_GetException(ctx);
481
- run_teardowns(ctx, d);
482
- JS_Throw(ctx, pending);
483
- }
484
- JS_FreeValue(ctx, subscriber_val);
485
- return cb_result;
486
- }
487
- JS_FreeValue(ctx, cb_result);
488
-
571
+ subscriber_val = start_subscriber(ctx, subscribe_cb, subscriber_val, d);
572
+ if (JS_IsException(subscriber_val)) return subscriber_val;
489
573
  return make_subscription(ctx, subscriber_val);
490
574
  }
491
-
492
575
  static JSValue observable_subscribe(JSContext* ctx, JSValueConst this_val, int argc,
493
576
  JSValueConst* argv) {
494
577
  auto* d = static_cast<ObservableData*>(JS_GetOpaque2(ctx, this_val, observable_class_id));
@@ -526,7 +609,7 @@ static JSValue observable_constructor(JSContext* ctx, JSValueConst new_target, i
526
609
  return obj;
527
610
  }
528
611
 
529
- /* ── Observable.from ─────────────────────────────────────────────── */
612
+ /* ── from / of ────────────────────────────────────────────────────── */
530
613
 
531
614
  /* Build an Observable from an arbitrary subscribe callback (C function). */
532
615
  static JSValue make_observable_with_cb(JSContext* ctx, JSValue subscribe_cb_taking_ownership) {
@@ -596,7 +679,7 @@ static JSValue iterator_next(JSContext* ctx, JSValueConst iterator, bool* done)
596
679
  return value;
597
680
  }
598
681
 
599
- /* Helper: Observable.from(iterable) — sync drain. The iterable is captured
682
+ /* Helper: from(iterable) — sync drain. The iterable is captured
600
683
  * via a small wrapper object so the C function can recover it on subscribe. */
601
684
 
602
685
  /* Closure data attached to a from-iterable subscribe callback. */
@@ -749,17 +832,11 @@ static JSValue observable_from(JSContext* ctx, JSValueConst this_val, int argc,
749
832
  JSValueConst* argv) {
750
833
  (void)this_val;
751
834
  if (argc < 1) {
752
- return JS_ThrowTypeError(ctx, "Observable.from: requires a source argument");
835
+ return JS_ThrowTypeError(ctx, "from: requires a source argument");
753
836
  }
754
837
  JSValueConst src = argv[0];
755
838
 
756
- /* (1) Already an Observable — passthrough. */
757
- if (JS_IsObject(src) &&
758
- JS_GetOpaque(src, observable_class_id) != nullptr) {
759
- return JS_DupValue(ctx, src);
760
- }
761
-
762
- /* (2) PromiseLike — has a callable .then */
839
+ /* (1) PromiseLike — has a callable .then */
763
840
  if (JS_IsObject(src)) {
764
841
  JSValue then_method = JS_GetPropertyStr(ctx, src, "then");
765
842
  bool is_thenable = JS_IsFunction(ctx, then_method);
@@ -776,7 +853,7 @@ static JSValue observable_from(JSContext* ctx, JSValueConst this_val, int argc,
776
853
  }
777
854
  }
778
855
 
779
- /* (3) Iterable — has @@iterator. */
856
+ /* (2) Iterable — has @@iterator. */
780
857
  if (JS_IsObject(src) || JS_IsString(src)) {
781
858
  JSValue iter_test = get_iterator(ctx, src);
782
859
  if (!JS_IsException(iter_test)) {
@@ -795,8 +872,22 @@ static JSValue observable_from(JSContext* ctx, JSValueConst this_val, int argc,
795
872
  JS_GetException(ctx);
796
873
  }
797
874
 
798
- return JS_ThrowTypeError(
799
- ctx, "Observable.from: source must be a Promise, Iterable, or Observable");
875
+ return JS_ThrowTypeError(ctx, "from: source must be a Promise or an Iterable");
876
+ }
877
+
878
+ /* of(...values): from() over the argument array. */
879
+ static JSValue observable_of(JSContext* ctx, JSValueConst this_val, int argc,
880
+ JSValueConst* argv) {
881
+ (void)this_val;
882
+ std::vector<JSValue> owned;
883
+ owned.reserve(argc);
884
+ for (int i = 0; i < argc; i++) owned.push_back(JS_DupValue(ctx, argv[i]));
885
+ /* Takes ownership of the values, and frees them itself on failure. */
886
+ JSValue values = JS_NewArrayFrom(ctx, argc, owned.data());
887
+ if (JS_IsException(values)) return values;
888
+ JSValue result = observable_from(ctx, JS_UNDEFINED, 1, &values);
889
+ JS_FreeValue(ctx, values);
890
+ return result;
800
891
  }
801
892
 
802
893
  /* ── Observable.withEmitters ────────────────────────────────────── */
@@ -961,27 +1052,37 @@ static JSValue multicast_emit_complete(JSContext* ctx, JSValueConst this_val, in
961
1052
  return JS_UNDEFINED;
962
1053
  }
963
1054
 
964
- static JSValue observable_with_emitters(JSContext* ctx, JSValueConst this_val, int argc,
965
- JSValueConst* argv) {
966
- (void)this_val;
967
- (void)argc;
968
- (void)argv;
969
-
1055
+ /* The {observable, next, complete} triple behind withEmitters(). Returns -1
1056
+ * with an exception pending; on success the caller owns all three values. */
1057
+ static int multicast_new(JSContext* ctx, JSValue* observable, JSValue* next,
1058
+ JSValue* complete) {
970
1059
  JSValue mc_obj = JS_NewObjectClass(ctx, multicast_class_id);
971
- if (JS_IsException(mc_obj)) return mc_obj;
1060
+ if (JS_IsException(mc_obj)) return -1;
972
1061
  auto* m = new MulticastState{ctx, false, {}};
973
1062
  JS_SetOpaque(mc_obj, m);
974
1063
 
975
1064
  JSValue subscribe_cb = JS_NewCFunctionData(ctx, multicast_subscribe, 1, 0, 1, &mc_obj);
976
- JSValue observable = make_observable_with_cb(ctx, subscribe_cb);
977
- if (JS_IsException(observable)) {
1065
+ JSValue obs = make_observable_with_cb(ctx, subscribe_cb);
1066
+ if (JS_IsException(obs)) {
978
1067
  JS_FreeValue(ctx, mc_obj);
979
- return observable;
1068
+ return -1;
980
1069
  }
981
1070
 
982
- JSValue next_fn = JS_NewCFunctionData(ctx, multicast_emit_next, 1, 0, 1, &mc_obj);
983
- JSValue complete_fn = JS_NewCFunctionData(ctx, multicast_emit_complete, 0, 0, 1, &mc_obj);
1071
+ *observable = obs;
1072
+ *next = JS_NewCFunctionData(ctx, multicast_emit_next, 1, 0, 1, &mc_obj);
1073
+ *complete = JS_NewCFunctionData(ctx, multicast_emit_complete, 0, 0, 1, &mc_obj);
984
1074
  JS_FreeValue(ctx, mc_obj);
1075
+ return 0;
1076
+ }
1077
+
1078
+ static JSValue observable_with_emitters(JSContext* ctx, JSValueConst this_val, int argc,
1079
+ JSValueConst* argv) {
1080
+ (void)this_val;
1081
+ (void)argc;
1082
+ (void)argv;
1083
+
1084
+ JSValue observable, next_fn, complete_fn;
1085
+ if (multicast_new(ctx, &observable, &next_fn, &complete_fn) < 0) return JS_EXCEPTION;
985
1086
 
986
1087
  JSValue result = JS_NewObject(ctx);
987
1088
  JS_DefinePropertyValueStr(ctx, result, "observable", observable, JS_PROP_C_W_E);
@@ -990,6 +1091,816 @@ static JSValue observable_with_emitters(JSContext* ctx, JSValueConst this_val, i
990
1091
  return result;
991
1092
  }
992
1093
 
1094
+ /* ── Operators (mikro/observable/operators) ─────────────────────────── */
1095
+
1096
+ static void ensure_protos(JSContext* ctx);
1097
+
1098
+ /* Each factory (map(fn), take(n), ...) validates its arguments and returns
1099
+ * an operator function whose func_data holds them; applying it to a source
1100
+ * builds an Observable whose subscribe callback (op_subscribe) carries
1101
+ * [source, args...]. A C-function `magic` packs the kind in its low byte and
1102
+ * the captured argument count above it. */
1103
+ enum OpKind : int {
1104
+ OP_MAP,
1105
+ OP_FILTER,
1106
+ OP_TAP,
1107
+ OP_TAKE,
1108
+ OP_SKIP,
1109
+ OP_SCAN,
1110
+ OP_DISTINCT,
1111
+ OP_START_WITH,
1112
+ OP_FINALIZE,
1113
+ OP_TAKE_UNTIL,
1114
+ OP_MERGE_WITH,
1115
+ OP_WITH_LATEST_FROM,
1116
+ OP_COMBINE_LATEST,
1117
+ OP_SWITCH_MAP,
1118
+ OP_TIMER,
1119
+ OP_DEBOUNCE,
1120
+ OP_THROTTLE,
1121
+ /* takeUntil(predicate), chosen by the takeUntil factory; not exported. */
1122
+ OP_TAKE_WHILE,
1123
+ };
1124
+
1125
+ /* Indexed by OpKind; export names, and the prefix of every error message. */
1126
+ static const char* const op_names[] = {
1127
+ "map", "filter", "tap", "take", "skip", "scan",
1128
+ "distinctUntilChanged", "startWith", "finalize", "takeUntil", "mergeWith",
1129
+ "withLatestFrom", "combineLatest", "switchMap", "timer",
1130
+ "debounceTime", "throttleTime", "takeUntil",
1131
+ };
1132
+ static_assert(sizeof(op_names) / sizeof(op_names[0]) == OP_TAKE_WHILE + 1,
1133
+ "op_names must name every OpKind");
1134
+
1135
+ static inline int op_kind(int magic) { return magic & 0xff; }
1136
+ static inline int op_argc(int magic) { return magic >> 8; }
1137
+ static inline int op_magic(int kind, int argc) { return kind | (argc << 8); }
1138
+
1139
+ /* Per-subscription operator state, shared by every upstream subscriber the
1140
+ * operator opens. `down` is the subscriber the operator emits to. */
1141
+ struct OpState {
1142
+ int kind = 0;
1143
+ JSValue down = JS_UNDEFINED;
1144
+ JSValue fn = JS_UNDEFINED; /* callable argument, or JS_UNDEFINED */
1145
+ JSValue value = JS_UNDEFINED; /* accumulator, last value, latest of `other`, pending value */
1146
+ JSValue inner = JS_UNDEFINED; /* switchMap: the current inner upstream subscriber */
1147
+ /* combineLatest: latest value per source, JS_UNINITIALIZED until seen. */
1148
+ std::vector<JSValue> values;
1149
+ int32_t count = 0; /* take/skip: remaining; mergeWith/combineLatest: open; timer: ticks */
1150
+ int32_t missing = 0; /* combineLatest: sources yet to emit */
1151
+ int32_t ms = 0; /* debounce/throttle window; timer period (-1: one-shot) */
1152
+ uint32_t timer_id = 0;
1153
+ bool flag = false; /* has a value (distinct, withLatestFrom); inclusive; timer repeating */
1154
+ bool pending = false; /* debounce/throttle: `value` waits for the window to end */
1155
+ bool leading = false;
1156
+ bool trailing = false;
1157
+ bool inner_open = false;
1158
+ bool source_done = false;
1159
+ };
1160
+
1161
+ static JSClassID op_state_class_id;
1162
+
1163
+ static void op_state_finalizer(JSRuntime* rt, JSValue val) {
1164
+ auto* s = static_cast<OpState*>(JS_GetOpaque(val, op_state_class_id));
1165
+ if (!s) return;
1166
+ JS_FreeValueRT(rt, s->down);
1167
+ JS_FreeValueRT(rt, s->fn);
1168
+ JS_FreeValueRT(rt, s->value);
1169
+ JS_FreeValueRT(rt, s->inner);
1170
+ for (auto& v : s->values) JS_FreeValueRT(rt, v);
1171
+ delete s;
1172
+ }
1173
+
1174
+ static void op_state_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
1175
+ auto* s = static_cast<OpState*>(JS_GetOpaque(val, op_state_class_id));
1176
+ if (!s) return;
1177
+ JS_MarkValue(rt, s->down, mark_func);
1178
+ JS_MarkValue(rt, s->fn, mark_func);
1179
+ JS_MarkValue(rt, s->value, mark_func);
1180
+ JS_MarkValue(rt, s->inner, mark_func);
1181
+ for (auto& v : s->values) JS_MarkValue(rt, v, mark_func);
1182
+ }
1183
+
1184
+ static JSClassDef op_state_class_def = {
1185
+ "OperatorState", op_state_finalizer, op_state_gc_mark, nullptr, nullptr,
1186
+ };
1187
+
1188
+ static SubscriberData* op_down(OpState* s) {
1189
+ return static_cast<SubscriberData*>(JS_GetOpaque(s->down, subscriber_class_id));
1190
+ }
1191
+
1192
+ static bool down_closed(OpState* s) {
1193
+ SubscriberData* d = op_down(s);
1194
+ return !d || d->closed || d->complete_pending;
1195
+ }
1196
+
1197
+ /* Emit downstream through the regular next/complete entry points, so the
1198
+ * dispatch trampoline and closed checks apply exactly as for JS callers. */
1199
+ static void emit(JSContext* ctx, OpState* s, JSValueConst value) {
1200
+ JSValue arg = value;
1201
+ JS_FreeValue(ctx, subscriber_next(ctx, s->down, 1, &arg));
1202
+ }
1203
+
1204
+ static void finish(JSContext* ctx, OpState* s) {
1205
+ JS_FreeValue(ctx, subscriber_complete(ctx, s->down, 0, nullptr));
1206
+ }
1207
+
1208
+ /* Emit a fresh array of `count` values (owned; consumed either way). */
1209
+ static void emit_array(JSContext* ctx, OpState* s, int count, JSValue* owned) {
1210
+ JSValue arr = JS_NewArrayFrom(ctx, count, owned);
1211
+ if (JS_IsException(arr)) {
1212
+ panic(ctx, JS_GetException(ctx));
1213
+ return;
1214
+ }
1215
+ emit(ctx, s, arr);
1216
+ JS_FreeValue(ctx, arr);
1217
+ }
1218
+
1219
+ /* Call `fn(argv...)` for its boolean result; a throw panics and yields false
1220
+ * with *ok cleared. */
1221
+ static bool call_for_bool(JSContext* ctx, JSValueConst fn, int argc, JSValueConst* argv, bool* ok) {
1222
+ JSValue ret = call_or_panic(ctx, fn, argc, argv);
1223
+ *ok = !JS_IsException(ret);
1224
+ if (!*ok) return false;
1225
+ bool result = JS_ToBool(ctx, ret) == 1;
1226
+ JS_FreeValue(ctx, ret);
1227
+ return result;
1228
+ }
1229
+
1230
+ static JSValue op_timer_fire(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1231
+ int magic, JSValueConst* func_data);
1232
+
1233
+ /* Arm the operator's timer through the registry (never the JS setTimeout
1234
+ * global). The registry holds the callback, and with it `state`. */
1235
+ static void schedule(JSContext* ctx, OpState* s, JSValueConst state, int32_t ms, bool repeat) {
1236
+ JSValue fn = JS_NewCFunctionData(ctx, op_timer_fire, 0, 0, 1, &state);
1237
+ if (JS_IsException(fn)) {
1238
+ panic(ctx, JS_GetException(ctx));
1239
+ return;
1240
+ }
1241
+ s->timer_id = MIK_Timer_Schedule(MIK_GetRuntime(ctx)->timers, ctx, fn, 0, nullptr,
1242
+ static_cast<int64_t>(ms) * 1000, repeat,
1243
+ MIK_GetPlatform()->get_boot_us());
1244
+ JS_FreeValue(ctx, fn);
1245
+ }
1246
+
1247
+ static void unschedule(JSContext* ctx, OpState* s) {
1248
+ if (s->timer_id == 0) return;
1249
+ MIK_Timer_UnSchedule(MIK_GetRuntime(ctx)->timers, ctx, s->timer_id);
1250
+ s->timer_id = 0;
1251
+ }
1252
+
1253
+ /* Emit the pending value, if any. Held across the emit: a synchronous
1254
+ * delivery may replace `s->value` before it returns. */
1255
+ static void flush(JSContext* ctx, OpState* s) {
1256
+ if (!s->pending) return;
1257
+ s->pending = false;
1258
+ JSValue v = JS_DupValue(ctx, s->value);
1259
+ emit(ctx, s, v);
1260
+ JS_FreeValue(ctx, v);
1261
+ }
1262
+
1263
+ static void set_pending(JSContext* ctx, OpState* s, JSValueConst value, bool pending) {
1264
+ JS_FreeValue(ctx, s->value);
1265
+ s->value = JS_DupValue(ctx, value);
1266
+ s->pending = pending;
1267
+ }
1268
+
1269
+ static void close_inner(JSContext* ctx, OpState* s) {
1270
+ if (JS_IsUndefined(s->inner)) return;
1271
+ auto* d = static_cast<SubscriberData*>(JS_GetOpaque(s->inner, subscriber_class_id));
1272
+ if (d) close_subscriber(ctx, d);
1273
+ JSValue inner = s->inner;
1274
+ s->inner = JS_UNDEFINED;
1275
+ JS_FreeValue(ctx, inner);
1276
+ }
1277
+
1278
+ /* Teardown for an operator state registered on its downstream subscriber:
1279
+ * ends the timer and the inner subscription. False if `entry` is no state. */
1280
+ static bool op_teardown(JSContext* ctx, JSValueConst entry) {
1281
+ auto* s = static_cast<OpState*>(JS_GetOpaque(entry, op_state_class_id));
1282
+ if (!s) return false;
1283
+ unschedule(ctx, s);
1284
+ close_inner(ctx, s);
1285
+ return true;
1286
+ }
1287
+
1288
+ static JSValue op_timer_fire(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1289
+ int magic, JSValueConst* func_data) {
1290
+ (void)this_val;
1291
+ (void)argc;
1292
+ (void)argv;
1293
+ (void)magic;
1294
+ JSValueConst state = func_data[0];
1295
+ auto* s = static_cast<OpState*>(JS_GetOpaque(state, op_state_class_id));
1296
+ if (!s || down_closed(s)) return JS_UNDEFINED;
1297
+ switch (s->kind) {
1298
+ case OP_TIMER: {
1299
+ /* The registry drops a one-shot entry after it fires. */
1300
+ bool first = !s->flag;
1301
+ if (first) s->timer_id = 0;
1302
+ emit(ctx, s, JS_NewInt32(ctx, s->count++));
1303
+ if (!first || down_closed(s)) return JS_UNDEFINED;
1304
+ if (s->ms < 0) {
1305
+ finish(ctx, s);
1306
+ return JS_UNDEFINED;
1307
+ }
1308
+ s->flag = true;
1309
+ schedule(ctx, s, state, s->ms, true);
1310
+ return JS_UNDEFINED;
1311
+ }
1312
+ case OP_DEBOUNCE:
1313
+ s->timer_id = 0;
1314
+ flush(ctx, s);
1315
+ return JS_UNDEFINED;
1316
+ case OP_THROTTLE:
1317
+ s->timer_id = 0;
1318
+ if (!s->pending) return JS_UNDEFINED;
1319
+ flush(ctx, s);
1320
+ /* A trailing emission opens the next window. */
1321
+ if (!down_closed(s)) schedule(ctx, s, state, s->ms, false);
1322
+ return JS_UNDEFINED;
1323
+ default:
1324
+ return JS_UNDEFINED;
1325
+ }
1326
+ }
1327
+
1328
+ /* Build an upstream subscriber that dispatches into `state` as number
1329
+ * `index`. Not started. */
1330
+ static JSValue make_upstream(JSContext* ctx, JSValueConst state, int index) {
1331
+ JSValue sub_val = JS_NewObjectClass(ctx, subscriber_class_id);
1332
+ if (JS_IsException(sub_val)) return sub_val;
1333
+ auto* d = new SubscriberData{
1334
+ ctx, false, false, JS_UNDEFINED, JS_UNDEFINED, JS_UNDEFINED, {}, JS_DupValue(ctx, state),
1335
+ index,
1336
+ };
1337
+ JS_SetOpaque(sub_val, d);
1338
+ return sub_val;
1339
+ }
1340
+
1341
+ /* switchMap: replace the inner subscription with one to `project(value)`.
1342
+ * Runs inside a dispatch, so failures panic rather than throw. */
1343
+ static void switch_inner(JSContext* ctx, OpState* s, JSValueConst state, JSValueConst value) {
1344
+ close_inner(ctx, s);
1345
+ JSValue source = call_or_panic(ctx, s->fn, 1, &value);
1346
+ if (JS_IsException(source)) return;
1347
+ auto* src = static_cast<ObservableData*>(JS_GetOpaque(source, observable_class_id));
1348
+ if (!src) {
1349
+ JS_FreeValue(ctx, source);
1350
+ JS_ThrowTypeError(ctx, "switchMap: project must return an Observable");
1351
+ panic(ctx, JS_GetException(ctx));
1352
+ return;
1353
+ }
1354
+ JSValue sub_val = make_upstream(ctx, state, 1);
1355
+ if (JS_IsException(sub_val)) {
1356
+ JS_FreeValue(ctx, source);
1357
+ panic(ctx, JS_GetException(ctx));
1358
+ return;
1359
+ }
1360
+ s->inner_open = true;
1361
+ s->inner = JS_DupValue(ctx, sub_val);
1362
+ auto* d = static_cast<SubscriberData*>(JS_GetOpaque(sub_val, subscriber_class_id));
1363
+ JSValue started = start_subscriber(ctx, src->subscribe_cb, sub_val, d);
1364
+ JS_FreeValue(ctx, source);
1365
+ if (JS_IsException(started)) {
1366
+ panic(ctx, JS_GetException(ctx));
1367
+ return;
1368
+ }
1369
+ JS_FreeValue(ctx, started);
1370
+ }
1371
+
1372
+ static void op_on_next(JSContext* ctx, JSValueConst state, int index, JSValueConst value) {
1373
+ auto* s = static_cast<OpState*>(JS_GetOpaque(state, op_state_class_id));
1374
+ bool ok = true;
1375
+ switch (s->kind) {
1376
+ case OP_MAP: {
1377
+ JSValue out = call_or_panic(ctx, s->fn, 1, &value);
1378
+ if (JS_IsException(out)) return;
1379
+ emit(ctx, s, out);
1380
+ JS_FreeValue(ctx, out);
1381
+ return;
1382
+ }
1383
+ case OP_FILTER:
1384
+ if (call_for_bool(ctx, s->fn, 1, &value, &ok)) emit(ctx, s, value);
1385
+ return;
1386
+ case OP_TAP: {
1387
+ JSValue ret = call_or_panic(ctx, s->fn, 1, &value);
1388
+ if (JS_IsException(ret)) return;
1389
+ JS_FreeValue(ctx, ret);
1390
+ emit(ctx, s, value);
1391
+ return;
1392
+ }
1393
+ case OP_TAKE:
1394
+ if (s->count <= 0) return;
1395
+ s->count--;
1396
+ emit(ctx, s, value);
1397
+ if (s->count == 0) finish(ctx, s);
1398
+ return;
1399
+ case OP_SKIP:
1400
+ if (s->count > 0) {
1401
+ s->count--;
1402
+ return;
1403
+ }
1404
+ emit(ctx, s, value);
1405
+ return;
1406
+ case OP_SCAN: {
1407
+ JSValue args[2] = {s->value, value};
1408
+ JSValue acc = call_or_panic(ctx, s->fn, 2, args);
1409
+ if (JS_IsException(acc)) return;
1410
+ JS_FreeValue(ctx, s->value);
1411
+ s->value = acc;
1412
+ emit(ctx, s, acc);
1413
+ return;
1414
+ }
1415
+ case OP_DISTINCT: {
1416
+ if (s->flag) {
1417
+ bool same;
1418
+ if (JS_IsUndefined(s->fn)) {
1419
+ same = JS_IsStrictEqual(ctx, s->value, value);
1420
+ } else {
1421
+ JSValue args[2] = {s->value, value};
1422
+ same = call_for_bool(ctx, s->fn, 2, args, &ok);
1423
+ if (!ok) return;
1424
+ }
1425
+ if (same) return;
1426
+ }
1427
+ s->flag = true;
1428
+ JS_FreeValue(ctx, s->value);
1429
+ s->value = JS_DupValue(ctx, value);
1430
+ emit(ctx, s, value);
1431
+ return;
1432
+ }
1433
+ case OP_TAKE_UNTIL:
1434
+ /* index 1 is the notifier: any value ends the stream. */
1435
+ if (index == 1) {
1436
+ finish(ctx, s);
1437
+ } else {
1438
+ emit(ctx, s, value);
1439
+ }
1440
+ return;
1441
+ case OP_TAKE_WHILE: {
1442
+ bool stop = call_for_bool(ctx, s->fn, 1, &value, &ok);
1443
+ if (!ok) return;
1444
+ if (!stop || s->flag) emit(ctx, s, value);
1445
+ if (stop) finish(ctx, s);
1446
+ return;
1447
+ }
1448
+ case OP_WITH_LATEST_FROM: {
1449
+ /* index 1 is `other`: remember its latest value. */
1450
+ if (index == 1) {
1451
+ s->flag = true;
1452
+ JS_FreeValue(ctx, s->value);
1453
+ s->value = JS_DupValue(ctx, value);
1454
+ return;
1455
+ }
1456
+ if (!s->flag) return;
1457
+ JSValue pair[2] = {JS_DupValue(ctx, value), JS_DupValue(ctx, s->value)};
1458
+ emit_array(ctx, s, 2, pair);
1459
+ return;
1460
+ }
1461
+ case OP_COMBINE_LATEST: {
1462
+ if (JS_IsUninitialized(s->values[index])) {
1463
+ s->missing--;
1464
+ } else {
1465
+ JS_FreeValue(ctx, s->values[index]);
1466
+ }
1467
+ s->values[index] = JS_DupValue(ctx, value);
1468
+ if (s->missing > 0) return;
1469
+ int n = static_cast<int>(s->values.size());
1470
+ std::vector<JSValue> copy;
1471
+ copy.reserve(n);
1472
+ for (auto& v : s->values) copy.push_back(JS_DupValue(ctx, v));
1473
+ emit_array(ctx, s, n, copy.data());
1474
+ return;
1475
+ }
1476
+ case OP_SWITCH_MAP:
1477
+ if (index == 1) {
1478
+ emit(ctx, s, value);
1479
+ } else {
1480
+ switch_inner(ctx, s, state, value);
1481
+ }
1482
+ return;
1483
+ case OP_DEBOUNCE: {
1484
+ bool quiet = s->timer_id == 0;
1485
+ unschedule(ctx, s);
1486
+ schedule(ctx, s, state, s->ms, false);
1487
+ if (quiet && s->leading) {
1488
+ emit(ctx, s, value);
1489
+ } else {
1490
+ set_pending(ctx, s, value, s->trailing);
1491
+ }
1492
+ return;
1493
+ }
1494
+ case OP_THROTTLE:
1495
+ if (s->timer_id != 0) {
1496
+ set_pending(ctx, s, value, s->trailing);
1497
+ return;
1498
+ }
1499
+ if (s->leading) {
1500
+ emit(ctx, s, value);
1501
+ } else {
1502
+ set_pending(ctx, s, value, s->trailing);
1503
+ }
1504
+ if (!down_closed(s)) schedule(ctx, s, state, s->ms, false);
1505
+ return;
1506
+ default:
1507
+ /* startWith, finalize, mergeWith: pass through. */
1508
+ emit(ctx, s, value);
1509
+ return;
1510
+ }
1511
+ }
1512
+
1513
+ /* `from` is the upstream subscriber that completed. */
1514
+ static void op_on_complete(JSContext* ctx, JSValueConst state, int index, SubscriberData* from) {
1515
+ auto* s = static_cast<OpState*>(JS_GetOpaque(state, op_state_class_id));
1516
+ switch (s->kind) {
1517
+ case OP_MERGE_WITH:
1518
+ if (--s->count == 0) finish(ctx, s);
1519
+ return;
1520
+ case OP_COMBINE_LATEST:
1521
+ /* A source ending without a value means no tuple can ever form. */
1522
+ if (JS_IsUninitialized(s->values[index]) || --s->count == 0) finish(ctx, s);
1523
+ return;
1524
+ case OP_TAKE_UNTIL:
1525
+ case OP_WITH_LATEST_FROM:
1526
+ /* The notifier or `other` completing does not end the stream. */
1527
+ if (index != 1) finish(ctx, s);
1528
+ return;
1529
+ case OP_SWITCH_MAP:
1530
+ /* Completes once the source and the last inner stream both have.
1531
+ * An inner that was switched away while its completion was still
1532
+ * queued (complete_pending) drains here too; only the current
1533
+ * inner's completion counts. */
1534
+ if (index == 1) {
1535
+ if (from == JS_GetOpaque(s->inner, subscriber_class_id)) s->inner_open = false;
1536
+ } else {
1537
+ s->source_done = true;
1538
+ }
1539
+ if (s->source_done && !s->inner_open) finish(ctx, s);
1540
+ return;
1541
+ case OP_DEBOUNCE:
1542
+ unschedule(ctx, s);
1543
+ flush(ctx, s);
1544
+ finish(ctx, s);
1545
+ return;
1546
+ case OP_THROTTLE:
1547
+ unschedule(ctx, s);
1548
+ flush(ctx, s);
1549
+ finish(ctx, s);
1550
+ return;
1551
+ default:
1552
+ finish(ctx, s);
1553
+ return;
1554
+ }
1555
+ }
1556
+
1557
+ /* Route `source` into the operator as upstream number `index`; the upstream
1558
+ * ends with `down`. The upstream subscriber joins `down`'s teardowns before
1559
+ * the producer runs, so a chain that completes during setup (startWith into
1560
+ * take(1)) already closes it, and a closed `down` subscribes nothing. */
1561
+ static bool forward(JSContext* ctx, OpState* s, JSValueConst source, JSValueConst state,
1562
+ int index) {
1563
+ auto* src = static_cast<ObservableData*>(JS_GetOpaque2(ctx, source, observable_class_id));
1564
+ if (!src) return false;
1565
+ if (down_closed(s)) return true;
1566
+ JSValue sub_val = make_upstream(ctx, state, index);
1567
+ if (JS_IsException(sub_val)) return false;
1568
+ auto* d = static_cast<SubscriberData*>(JS_GetOpaque(sub_val, subscriber_class_id));
1569
+ op_down(s)->teardowns.push_back(JS_DupValue(ctx, sub_val));
1570
+ JSValue started = start_subscriber(ctx, src->subscribe_cb, sub_val, d);
1571
+ if (JS_IsException(started)) return false;
1572
+ JS_FreeValue(ctx, started);
1573
+ return true;
1574
+ }
1575
+
1576
+ /* Counts and durations: non-finite or huge values saturate, NaN counts as 0. */
1577
+ static int32_t to_count(JSContext* ctx, JSValueConst v) {
1578
+ double n = 0;
1579
+ JS_ToFloat64(ctx, &n, v);
1580
+ if (!(n > 0)) return 0;
1581
+ if (n >= INT32_MAX) return INT32_MAX;
1582
+ return static_cast<int32_t>(n);
1583
+ }
1584
+
1585
+ /* The subscribe callback of an operator-built Observable.
1586
+ * func_data = [source, args...]; argv[0] = the downstream subscriber. */
1587
+ static JSValue op_subscribe(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1588
+ int magic, JSValueConst* func_data) {
1589
+ (void)this_val;
1590
+ if (argc < 1) return JS_UNDEFINED;
1591
+ int kind = op_kind(magic);
1592
+ JSValueConst source = func_data[0];
1593
+ JSValueConst* args = func_data + 1;
1594
+
1595
+ JSValue state = JS_NewObjectClass(ctx, op_state_class_id);
1596
+ if (JS_IsException(state)) return state;
1597
+ auto* s = new OpState();
1598
+ s->kind = kind;
1599
+ s->down = JS_DupValue(ctx, argv[0]);
1600
+ JS_SetOpaque(state, s);
1601
+
1602
+ bool ok = true;
1603
+ bool subscribed = false;
1604
+ switch (kind) {
1605
+ case OP_MAP:
1606
+ case OP_FILTER:
1607
+ case OP_TAP:
1608
+ s->fn = JS_DupValue(ctx, args[0]);
1609
+ break;
1610
+ case OP_TAKE_WHILE:
1611
+ s->fn = JS_DupValue(ctx, args[0]);
1612
+ s->flag = JS_ToBool(ctx, args[1]) == 1;
1613
+ break;
1614
+ case OP_SCAN:
1615
+ s->fn = JS_DupValue(ctx, args[0]);
1616
+ s->value = JS_DupValue(ctx, args[1]);
1617
+ break;
1618
+ case OP_DISTINCT:
1619
+ s->fn = JS_DupValue(ctx, args[0]);
1620
+ break;
1621
+ case OP_TAKE:
1622
+ s->count = to_count(ctx, args[0]);
1623
+ /* take(0): complete without ever subscribing upstream. */
1624
+ if (s->count == 0) {
1625
+ finish(ctx, s);
1626
+ subscribed = true;
1627
+ }
1628
+ break;
1629
+ case OP_SKIP:
1630
+ s->count = to_count(ctx, args[0]);
1631
+ break;
1632
+ case OP_START_WITH:
1633
+ emit(ctx, s, args[0]);
1634
+ /* The first value may have closed the chain (take(1)). */
1635
+ if (down_closed(s)) subscribed = true;
1636
+ break;
1637
+ case OP_FINALIZE:
1638
+ /* Registered first, so it runs after the upstream unsubscribe. */
1639
+ add_teardown(ctx, op_down(s), JS_DupValue(ctx, args[0]));
1640
+ break;
1641
+ case OP_TAKE_UNTIL:
1642
+ /* Notifier first, as in RxJS: one that fires during its own
1643
+ * subscribe ends the stream before the source is subscribed. */
1644
+ ok = forward(ctx, s, args[0], state, 1) && forward(ctx, s, source, state, 0);
1645
+ subscribed = true;
1646
+ break;
1647
+ case OP_MERGE_WITH: {
1648
+ int n = op_argc(magic);
1649
+ s->count = n + 1;
1650
+ ok = forward(ctx, s, source, state, 0);
1651
+ for (int i = 0; ok && i < n; i++) ok = forward(ctx, s, args[i], state, i + 1);
1652
+ subscribed = true;
1653
+ break;
1654
+ }
1655
+ case OP_WITH_LATEST_FROM:
1656
+ ok = forward(ctx, s, args[0], state, 1) && forward(ctx, s, source, state, 0);
1657
+ subscribed = true;
1658
+ break;
1659
+ case OP_COMBINE_LATEST: {
1660
+ /* `source` is the array of sources. */
1661
+ int64_t len = 0;
1662
+ if (JS_GetLength(ctx, source, &len) < 0) {
1663
+ ok = false;
1664
+ subscribed = true;
1665
+ break;
1666
+ }
1667
+ int n = static_cast<int>(len);
1668
+ s->values.assign(n, JS_UNINITIALIZED);
1669
+ s->count = n;
1670
+ s->missing = n;
1671
+ if (n == 0) {
1672
+ emit_array(ctx, s, 0, nullptr);
1673
+ finish(ctx, s);
1674
+ }
1675
+ for (int i = 0; ok && i < n; i++) {
1676
+ /* A source that completed without a value already closed
1677
+ * `down`; the rest would be unsubscribed at once anyway. */
1678
+ if (down_closed(s)) break;
1679
+ JSValue elem = JS_GetPropertyUint32(ctx, source, i);
1680
+ ok = !JS_IsException(elem) && forward(ctx, s, elem, state, i);
1681
+ JS_FreeValue(ctx, elem);
1682
+ }
1683
+ subscribed = true;
1684
+ break;
1685
+ }
1686
+ case OP_SWITCH_MAP:
1687
+ s->fn = JS_DupValue(ctx, args[0]);
1688
+ add_teardown(ctx, op_down(s), JS_DupValue(ctx, state));
1689
+ break;
1690
+ case OP_TIMER:
1691
+ /* `source` is the delay; args[0] the period, or undefined. */
1692
+ s->ms = JS_IsUndefined(args[0]) ? -1 : to_count(ctx, args[0]);
1693
+ add_teardown(ctx, op_down(s), JS_DupValue(ctx, state));
1694
+ schedule(ctx, s, state, to_count(ctx, source), false);
1695
+ subscribed = true;
1696
+ break;
1697
+ case OP_DEBOUNCE:
1698
+ case OP_THROTTLE:
1699
+ s->ms = to_count(ctx, args[0]);
1700
+ s->leading = JS_ToBool(ctx, args[1]) == 1;
1701
+ s->trailing = JS_ToBool(ctx, args[2]) == 1;
1702
+ add_teardown(ctx, op_down(s), JS_DupValue(ctx, state));
1703
+ break;
1704
+ default:
1705
+ break;
1706
+ }
1707
+ if (ok && !subscribed) ok = forward(ctx, s, source, state, 0);
1708
+ JS_FreeValue(ctx, state);
1709
+ return ok ? JS_UNDEFINED : JS_EXCEPTION;
1710
+ }
1711
+
1712
+ /* The operator function returned by a factory: (source) => Observable.
1713
+ * func_data = the factory's arguments. */
1714
+ static JSValue op_apply(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1715
+ int magic, JSValueConst* func_data) {
1716
+ (void)this_val;
1717
+ if (argc < 1 || !JS_GetOpaque(argv[0], observable_class_id)) {
1718
+ return JS_ThrowTypeError(ctx, "%s: source must be an Observable",
1719
+ op_names[op_kind(magic)]);
1720
+ }
1721
+ int n = op_argc(magic);
1722
+ std::vector<JSValue> data;
1723
+ data.reserve(n + 1);
1724
+ data.push_back(argv[0]);
1725
+ for (int i = 0; i < n; i++) data.push_back(func_data[i]);
1726
+ /* JS_NewCFunctionData duplicates the data values. */
1727
+ JSValue cb = JS_NewCFunctionData(ctx, op_subscribe, 1, magic, n + 1, data.data());
1728
+ if (JS_IsException(cb)) return cb;
1729
+ return make_observable_with_cb(ctx, cb);
1730
+ }
1731
+
1732
+ static bool is_observable(JSValueConst v) {
1733
+ return JS_GetOpaque(v, observable_class_id) != nullptr;
1734
+ }
1735
+
1736
+ /* One factory for every operator; `magic` is the OpKind. */
1737
+ static JSValue op_factory(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1738
+ int magic) {
1739
+ (void)this_val;
1740
+ int kind = magic;
1741
+ const char* name = op_names[kind];
1742
+ JSValueConst first = argc > 0 ? argv[0] : JS_UNDEFINED;
1743
+ std::vector<JSValue> data;
1744
+ JSValue owned = JS_UNDEFINED;
1745
+ switch (kind) {
1746
+ case OP_MAP:
1747
+ case OP_FILTER:
1748
+ case OP_TAP:
1749
+ case OP_FINALIZE:
1750
+ case OP_SCAN:
1751
+ case OP_SWITCH_MAP:
1752
+ if (!JS_IsFunction(ctx, first)) {
1753
+ return JS_ThrowTypeError(ctx, "%s: argument must be a function", name);
1754
+ }
1755
+ data.push_back(first);
1756
+ if (kind == OP_SCAN) data.push_back(argc > 1 ? argv[1] : JS_UNDEFINED);
1757
+ break;
1758
+ case OP_DISTINCT:
1759
+ if (!JS_IsUndefined(first) && !JS_IsFunction(ctx, first)) {
1760
+ return JS_ThrowTypeError(ctx, "%s: argument must be a function", name);
1761
+ }
1762
+ data.push_back(first);
1763
+ break;
1764
+ case OP_TAKE:
1765
+ case OP_SKIP:
1766
+ if (!JS_IsNumber(first)) {
1767
+ return JS_ThrowTypeError(ctx, "%s: count must be a number", name);
1768
+ }
1769
+ data.push_back(first);
1770
+ break;
1771
+ case OP_START_WITH:
1772
+ data.push_back(first);
1773
+ break;
1774
+ case OP_TAKE_UNTIL: {
1775
+ if (JS_IsFunction(ctx, first)) {
1776
+ kind = OP_TAKE_WHILE;
1777
+ bool inclusive = true;
1778
+ if (argc > 1 && JS_IsObject(argv[1])) {
1779
+ JSValue opt = JS_GetPropertyStr(ctx, argv[1], "inclusive");
1780
+ if (JS_IsException(opt)) return opt;
1781
+ if (!JS_IsUndefined(opt)) inclusive = JS_ToBool(ctx, opt) == 1;
1782
+ JS_FreeValue(ctx, opt);
1783
+ }
1784
+ owned = JS_NewBool(ctx, inclusive);
1785
+ data.push_back(first);
1786
+ data.push_back(owned);
1787
+ break;
1788
+ }
1789
+ if (!is_observable(first)) {
1790
+ return JS_ThrowTypeError(ctx, "%s: argument must be an Observable or a predicate",
1791
+ name);
1792
+ }
1793
+ data.push_back(first);
1794
+ break;
1795
+ }
1796
+ case OP_MERGE_WITH:
1797
+ case OP_WITH_LATEST_FROM:
1798
+ for (int i = 0; i < (kind == OP_MERGE_WITH ? argc : 1); i++) {
1799
+ JSValueConst v = i < argc ? argv[i] : JS_UNDEFINED;
1800
+ if (!is_observable(v)) {
1801
+ return JS_ThrowTypeError(ctx, "%s: arguments must be Observables", name);
1802
+ }
1803
+ data.push_back(v);
1804
+ }
1805
+ break;
1806
+ case OP_TIMER: {
1807
+ if (!JS_IsNumber(first) || (argc > 1 && !JS_IsUndefined(argv[1]) && !JS_IsNumber(argv[1]))) {
1808
+ return JS_ThrowTypeError(ctx, "%s: delay and period must be numbers", name);
1809
+ }
1810
+ /* Not an operator: the delay is the "source" of op_subscribe. */
1811
+ JSValue timer_args[2] = {first, argc > 1 ? argv[1] : JS_UNDEFINED};
1812
+ JSValue cb =
1813
+ JS_NewCFunctionData(ctx, op_subscribe, 1, op_magic(kind, 1), 2, timer_args);
1814
+ if (JS_IsException(cb)) return cb;
1815
+ return make_observable_with_cb(ctx, cb);
1816
+ }
1817
+ case OP_DEBOUNCE:
1818
+ case OP_THROTTLE: {
1819
+ if (!JS_IsNumber(first)) {
1820
+ return JS_ThrowTypeError(ctx, "%s: duration must be a number", name);
1821
+ }
1822
+ /* {leading, trailing}: debounce defaults to the trailing edge,
1823
+ * throttle to the leading one. */
1824
+ bool leading = kind == OP_THROTTLE;
1825
+ bool trailing = kind == OP_DEBOUNCE;
1826
+ if (argc > 1 && JS_IsObject(argv[1])) {
1827
+ JSValue l = JS_GetPropertyStr(ctx, argv[1], "leading");
1828
+ if (JS_IsException(l)) return l;
1829
+ JSValue t = JS_GetPropertyStr(ctx, argv[1], "trailing");
1830
+ if (JS_IsException(t)) {
1831
+ JS_FreeValue(ctx, l);
1832
+ return t;
1833
+ }
1834
+ if (!JS_IsUndefined(l)) leading = JS_ToBool(ctx, l) == 1;
1835
+ if (!JS_IsUndefined(t)) trailing = JS_ToBool(ctx, t) == 1;
1836
+ JS_FreeValue(ctx, l);
1837
+ JS_FreeValue(ctx, t);
1838
+ }
1839
+ data.push_back(first);
1840
+ data.push_back(JS_NewBool(ctx, leading));
1841
+ data.push_back(JS_NewBool(ctx, trailing));
1842
+ break;
1843
+ }
1844
+ case OP_COMBINE_LATEST: {
1845
+ int64_t len = -1;
1846
+ if (!JS_IsArray(first) || JS_GetLength(ctx, first, &len) < 0) {
1847
+ return JS_ThrowTypeError(ctx, "%s: argument must be an array of Observables",
1848
+ name);
1849
+ }
1850
+ for (int64_t i = 0; i < len; i++) {
1851
+ JSValue elem = JS_GetPropertyUint32(ctx, first, static_cast<uint32_t>(i));
1852
+ bool valid = is_observable(elem);
1853
+ JS_FreeValue(ctx, elem);
1854
+ if (!valid) {
1855
+ return JS_ThrowTypeError(ctx, "%s: argument must be an array of Observables",
1856
+ name);
1857
+ }
1858
+ }
1859
+ /* Not an operator: the array is the "source" of op_subscribe. */
1860
+ JSValue cb = JS_NewCFunctionData(ctx, op_subscribe, 1, kind, 1, &first);
1861
+ if (JS_IsException(cb)) return cb;
1862
+ return make_observable_with_cb(ctx, cb);
1863
+ }
1864
+ default:
1865
+ break;
1866
+ }
1867
+ int n = static_cast<int>(data.size());
1868
+ JSValue fn = JS_NewCFunctionData(ctx, op_apply, 1, op_magic(kind, n), n, data.data());
1869
+ JS_FreeValue(ctx, owned);
1870
+ return fn;
1871
+ }
1872
+
1873
+ /* pipe(...ops): compose operators left to right into one operator.
1874
+ * func_data = the operators; `magic` = their count. */
1875
+ static JSValue pipe_apply(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
1876
+ int magic, JSValueConst* func_data) {
1877
+ (void)this_val;
1878
+ JSValueConst source = argc > 0 ? argv[0] : JS_UNDEFINED;
1879
+ return observable_pipe(ctx, source, magic, func_data);
1880
+ }
1881
+
1882
+ static JSValue pipe_factory(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
1883
+ (void)this_val;
1884
+ for (int i = 0; i < argc; i++) {
1885
+ if (!JS_IsFunction(ctx, argv[i])) {
1886
+ return JS_ThrowTypeError(ctx, "pipe: arguments must be operator functions");
1887
+ }
1888
+ }
1889
+ return JS_NewCFunctionData(ctx, pipe_apply, 1, argc, argc, argv);
1890
+ }
1891
+
1892
+ static int operators_module_init(JSContext* ctx, JSModuleDef* m) {
1893
+ ensure_protos(ctx);
1894
+ for (int kind = OP_MAP; kind < OP_TAKE_WHILE; kind++) {
1895
+ const char* name = op_names[kind];
1896
+ JS_SetModuleExport(
1897
+ ctx, m, name,
1898
+ JS_NewCFunctionMagic(ctx, op_factory, name, 1, JS_CFUNC_generic_magic, kind));
1899
+ }
1900
+ JS_SetModuleExport(ctx, m, "pipe", JS_NewCFunction(ctx, pipe_factory, "pipe", 0));
1901
+ return 0;
1902
+ }
1903
+
993
1904
  /* ── Observable prototype ─────────────────────────────────────────── */
994
1905
 
995
1906
  static const JSCFunctionListEntry observable_proto_funcs[] = {
@@ -999,9 +1910,16 @@ static const JSCFunctionListEntry observable_proto_funcs[] = {
999
1910
 
1000
1911
  /* ── Module init ──────────────────────────────────────────────────── */
1001
1912
 
1002
- static int observable_module_init(JSContext* ctx, JSModuleDef* m) {
1003
- /* Build prototype objects and bind to class IDs. The classes themselves
1004
- * are registered by mik__observable_init below. */
1913
+ /* Build the prototypes and the constructor once per context, on the first
1914
+ * import of either module: operators create Observables without the
1915
+ * observable module ever being imported. The classes themselves are
1916
+ * registered by mik__observable_init below. */
1917
+ static void ensure_protos(JSContext* ctx) {
1918
+ JSValue existing = JS_GetClassProto(ctx, observable_class_id);
1919
+ bool done = JS_IsObject(existing);
1920
+ JS_FreeValue(ctx, existing);
1921
+ if (done) return;
1922
+
1005
1923
  JSValue subscriber_proto = JS_NewObject(ctx);
1006
1924
  JS_SetPropertyFunctionList(ctx, subscriber_proto, subscriber_proto_funcs,
1007
1925
  sizeof(subscriber_proto_funcs) /
@@ -1024,19 +1942,45 @@ static int observable_module_init(JSContext* ctx, JSModuleDef* m) {
1024
1942
  JS_CFUNC_constructor, 0);
1025
1943
  JS_SetConstructor(ctx, obs_ctor, obs_proto);
1026
1944
 
1027
- JS_DefinePropertyValueStr(
1028
- ctx, obs_ctor, "from",
1029
- JS_NewCFunction(ctx, observable_from, "from", 1), JS_PROP_C_W_E);
1030
1945
  JS_DefinePropertyValueStr(
1031
1946
  ctx, obs_ctor, "withEmitters",
1032
1947
  JS_NewCFunction(ctx, observable_with_emitters, "withEmitters", 0), JS_PROP_C_W_E);
1948
+ /* The prototype keeps the constructor reachable (proto.constructor). */
1949
+ JS_FreeValue(ctx, obs_ctor);
1950
+ }
1033
1951
 
1952
+ static int observable_module_init(JSContext* ctx, JSModuleDef* m) {
1953
+ ensure_protos(ctx);
1954
+ JSValue obs_proto = JS_GetClassProto(ctx, observable_class_id);
1955
+ JSValue obs_ctor = JS_GetPropertyStr(ctx, obs_proto, "constructor");
1956
+ JS_FreeValue(ctx, obs_proto);
1034
1957
  JS_SetModuleExport(ctx, m, "Observable", obs_ctor);
1958
+ JS_SetModuleExport(ctx, m, "from", JS_NewCFunction(ctx, observable_from, "from", 1));
1959
+ JS_SetModuleExport(ctx, m, "of", JS_NewCFunction(ctx, observable_of, "of", 0));
1035
1960
  return 0;
1036
1961
  }
1037
1962
 
1038
1963
  } // namespace
1039
1964
 
1965
+ /* mikro/observable/operators, resolved through the C-module table in
1966
+ * modules.cpp on first import. */
1967
+ JSModuleDef* mik__observable_operators_load(JSContext* ctx) {
1968
+ JSModuleDef* m = JS_NewCModule(ctx, "mikro/observable/operators", operators_module_init);
1969
+ if (!m) return nullptr;
1970
+ for (int kind = OP_MAP; kind < OP_TAKE_WHILE; kind++) {
1971
+ JS_AddModuleExport(ctx, m, op_names[kind]);
1972
+ }
1973
+ JS_AddModuleExport(ctx, m, "pipe");
1974
+ return m;
1975
+ }
1976
+
1977
+ int mik__observable_multicast_new(JSContext* ctx, JSValue* observable, JSValue* next,
1978
+ JSValue* complete) {
1979
+ /* A C module can hand out an Observable before any JS imports mikro/observable. */
1980
+ ensure_protos(ctx);
1981
+ return multicast_new(ctx, observable, next, complete);
1982
+ }
1983
+
1040
1984
  void mik__observable_dispatch_free(MIKRuntime* mik_rt) {
1041
1985
  delete mik_rt->observable_dispatch;
1042
1986
  mik_rt->observable_dispatch = nullptr;
@@ -1063,9 +2007,13 @@ JSModuleDef* mik__observable_init(JSContext* ctx) {
1063
2007
  JS_NewClass(rt, from_promise_class_id, &from_promise_class_def);
1064
2008
  JS_NewClassID(rt, &multicast_class_id);
1065
2009
  JS_NewClass(rt, multicast_class_id, &multicast_class_def);
2010
+ JS_NewClassID(rt, &op_state_class_id);
2011
+ JS_NewClass(rt, op_state_class_id, &op_state_class_def);
1066
2012
 
1067
2013
  JSModuleDef* m = JS_NewCModule(ctx, "native:mikro/observable", observable_module_init);
1068
2014
  if (!m) return nullptr;
1069
2015
  JS_AddModuleExport(ctx, m, "Observable");
2016
+ JS_AddModuleExport(ctx, m, "from");
2017
+ JS_AddModuleExport(ctx, m, "of");
1070
2018
  return m;
1071
2019
  }