@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.
- package/CMakeLists.txt +3 -1
- package/cmake/mikrojs_bytecode.cmake +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/result/native-result.node-shim.js.map +1 -1
- package/dist/runtime/result/types.d.ts +5 -5
- package/dist/runtime/result/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/mikrojs.h +26 -2
- package/include/mikrojs/ota_env.h +8 -6
- package/include/mikrojs/ota_policy.h +16 -0
- package/include/mikrojs/private.h +13 -0
- package/include/mikrojs/utils.h +24 -0
- package/package.json +10 -10
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/gpio/types.ts +116 -0
- package/runtime/i2c/types.ts +25 -31
- package/runtime/i2s/types.ts +23 -12
- package/runtime/internal.d.ts +1 -174
- package/runtime/kv/shared.ts +13 -11
- package/runtime/neopixel/types.ts +24 -6
- package/runtime/observable/native-observable.node-shim.ts +73 -37
- package/runtime/observable/native-operators.node-shim.ts +417 -0
- package/runtime/observable/observable.ts +76 -1
- package/runtime/observable/operators.ts +156 -89
- package/runtime/observable/types.ts +61 -6
- package/runtime/pwm/types.ts +31 -11
- package/runtime/result/native-result.node-shim.ts +1 -1
- package/runtime/result/types.ts +5 -5
- package/runtime/sleep/types.ts +10 -8
- package/runtime/spi/types.ts +16 -14
- package/runtime/uart/types.ts +27 -15
- package/src/gpio_claim.cpp +63 -0
- package/src/mik_abort.cpp +1 -2
- package/src/mik_console.cpp +192 -115
- package/src/mik_http_client.cpp +3 -14
- package/src/mik_inspect.cpp +86 -9
- package/src/mik_observable.cpp +1028 -80
- package/src/mik_ota_client.cpp +5 -2
- package/src/mik_ota_policy.cpp +65 -18
- package/src/mik_result.cpp +10 -9
- package/src/mikrojs.cpp +26 -13
- package/src/modules.cpp +17 -6
- package/src/timers.cpp +6 -2
- package/src/utils.cpp +106 -0
- package/runtime/i2c/i2c.ts +0 -35
- package/runtime/i2s/i2s.ts +0 -46
- package/runtime/neopixel/neopixel.ts +0 -38
- package/runtime/pin/pin.ts +0 -51
- package/runtime/pin/types.ts +0 -49
- package/runtime/pwm/pwm.ts +0 -32
- package/runtime/spi/spi.ts +0 -31
- package/runtime/uart/uart.ts +0 -52
package/src/mik_observable.cpp
CHANGED
|
@@ -4,11 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Classes registered:
|
|
6
6
|
* - Observable: constructor(cb), subscribe(observer), pipe(...ops),
|
|
7
|
-
* static
|
|
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
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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->
|
|
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->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
/* ──
|
|
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:
|
|
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, "
|
|
835
|
+
return JS_ThrowTypeError(ctx, "from: requires a source argument");
|
|
753
836
|
}
|
|
754
837
|
JSValueConst src = argv[0];
|
|
755
838
|
|
|
756
|
-
/* (1)
|
|
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
|
-
/* (
|
|
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
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
|
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
|
|
977
|
-
if (JS_IsException(
|
|
1065
|
+
JSValue obs = make_observable_with_cb(ctx, subscribe_cb);
|
|
1066
|
+
if (JS_IsException(obs)) {
|
|
978
1067
|
JS_FreeValue(ctx, mc_obj);
|
|
979
|
-
return
|
|
1068
|
+
return -1;
|
|
980
1069
|
}
|
|
981
1070
|
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
}
|