@mikrojs/native 0.6.0-pr-72.g464c29c → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +3 -1
- package/include/mikrojs/mikrojs.h +2 -1
- package/include/mikrojs/private.h +3 -0
- package/package.json +4 -2
- 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/ble/ble.ts +6 -8
- package/runtime/ble/types.ts +7 -14
- package/runtime/internal.d.ts +8 -2
- package/runtime/observable/lazy.ts +39 -0
- package/runtime/observable/native-observable.node-shim.ts +220 -0
- package/runtime/observable/observable.ts +1 -0
- package/runtime/observable/operators.ts +144 -0
- package/runtime/observable/types.ts +80 -0
- package/runtime/udp/types.ts +15 -2
- package/runtime/udp/udp.ts +45 -9
- package/runtime/wifi/types.ts +8 -17
- package/runtime/wifi/wifi.ts +7 -28
- package/src/mik_app_config.cpp +4 -1
- package/src/mik_observable.cpp +950 -0
- package/src/mik_repl.cpp +9 -4
- package/src/mikrojs.cpp +1 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/* mik_observable.cpp — push-shaped composable event stream primitive.
|
|
2
|
+
*
|
|
3
|
+
* See .claude/plans/observable.md (worktree branch) for the locked design.
|
|
4
|
+
*
|
|
5
|
+
* Classes registered:
|
|
6
|
+
* - Observable: constructor(cb), subscribe(observer), pipe(...ops),
|
|
7
|
+
* static from(src), static withEmitters()
|
|
8
|
+
* - Subscriber (internal handle passed to subscribe callback): next, complete,
|
|
9
|
+
* addTeardown, closed
|
|
10
|
+
* - Subscription: unsubscribe
|
|
11
|
+
*
|
|
12
|
+
* Error semantics: throws inside observer or operator callbacks (and inside
|
|
13
|
+
* teardown callbacks) are caught at the dispatch boundary, isolated to the
|
|
14
|
+
* offending subscriber, and re-thrown asynchronously via setTimeout(0). The
|
|
15
|
+
* synchronous producer keeps running (sibling subscribers receive the value,
|
|
16
|
+
* remaining teardowns run); the bug eventually surfaces as an uncaught
|
|
17
|
+
* exception, which the runtime treats as fatal via the existing
|
|
18
|
+
* unhandled-rejection halt mechanism. Stream errors are panics.
|
|
19
|
+
*
|
|
20
|
+
* Producer-setup throws inside the subscribe callback bubble synchronously
|
|
21
|
+
* to the .subscribe() caller — that's a bug in the producer factory itself,
|
|
22
|
+
* not a runtime dispatch event.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
#include <cstddef>
|
|
26
|
+
#include <cstdint>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
#include "mikrojs/private.h"
|
|
30
|
+
#include "mikrojs/utils.h"
|
|
31
|
+
|
|
32
|
+
extern "C" {
|
|
33
|
+
#include "quickjs.h"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
namespace {
|
|
37
|
+
|
|
38
|
+
JSClassID observable_class_id;
|
|
39
|
+
JSClassID subscriber_class_id;
|
|
40
|
+
JSClassID subscription_class_id;
|
|
41
|
+
|
|
42
|
+
struct ObservableData {
|
|
43
|
+
JSValue subscribe_cb;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
struct SubscriberData {
|
|
47
|
+
JSContext* ctx;
|
|
48
|
+
bool closed;
|
|
49
|
+
/* observer object retained so its props can't be reclaimed mid-dispatch. */
|
|
50
|
+
JSValue observer;
|
|
51
|
+
JSValue next_fn;
|
|
52
|
+
JSValue complete_fn;
|
|
53
|
+
std::vector<JSValue> teardowns;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
struct SubscriptionData {
|
|
57
|
+
/* Owns the subscriber JSValue so unsubscribe() can reach the SubscriberData
|
|
58
|
+
* even after the producer's reference drops. */
|
|
59
|
+
JSValue subscriber_value;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
/* Re-throws the captured exception from func_data[0]. Used as the
|
|
65
|
+
* `setTimeout(callback, 0)` payload in panic_async. */
|
|
66
|
+
static JSValue throw_captured(JSContext* ctx, JSValueConst this_val, int argc,
|
|
67
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
68
|
+
(void)this_val;
|
|
69
|
+
(void)argc;
|
|
70
|
+
(void)argv;
|
|
71
|
+
(void)magic;
|
|
72
|
+
return JS_Throw(ctx, JS_DupValue(ctx, func_data[0]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Catch a thrown error and re-throw it on the next event-loop tick via the
|
|
76
|
+
* runtime's setTimeout. The synchronous caller keeps going (sibling
|
|
77
|
+
* subscribers receive the value, remaining teardowns run); the eventual
|
|
78
|
+
* uncaught throw halts the runtime via the existing unhandled-rejection
|
|
79
|
+
* path. Stream errors are panics.
|
|
80
|
+
*
|
|
81
|
+
* Takes ownership of `exception` — caller must not free after this call. */
|
|
82
|
+
static void panic_async(JSContext* ctx, JSValue exception) {
|
|
83
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
84
|
+
JSValue setTimeout = JS_GetPropertyStr(ctx, global, "setTimeout");
|
|
85
|
+
JS_FreeValue(ctx, global);
|
|
86
|
+
if (!JS_IsFunction(ctx, setTimeout)) {
|
|
87
|
+
/* setTimeout missing or not a function — should not happen in the
|
|
88
|
+
* mikrojs runtime since it's globally defined. Drop on the floor
|
|
89
|
+
* rather than disrupting the dispatch path. */
|
|
90
|
+
JS_FreeValue(ctx, setTimeout);
|
|
91
|
+
JS_FreeValue(ctx, exception);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
JSValueConst data[1] = {exception};
|
|
95
|
+
JSValue thrower = JS_NewCFunctionData(ctx, throw_captured, 0, 0, 1, data);
|
|
96
|
+
JS_FreeValue(ctx, exception);
|
|
97
|
+
if (JS_IsException(thrower)) {
|
|
98
|
+
JS_FreeValue(ctx, setTimeout);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
JSValue zero = JS_NewInt32(ctx, 0);
|
|
102
|
+
JSValueConst call_args[2] = {thrower, zero};
|
|
103
|
+
JSValue ret = JS_Call(ctx, setTimeout, JS_UNDEFINED, 2, call_args);
|
|
104
|
+
JS_FreeValue(ctx, setTimeout);
|
|
105
|
+
JS_FreeValue(ctx, thrower);
|
|
106
|
+
JS_FreeValue(ctx, zero);
|
|
107
|
+
if (JS_IsException(ret)) {
|
|
108
|
+
/* setTimeout itself threw — best effort, drop the secondary error. */
|
|
109
|
+
JSValue suppressed = JS_GetException(ctx);
|
|
110
|
+
JS_FreeValue(ctx, suppressed);
|
|
111
|
+
} else {
|
|
112
|
+
JS_FreeValue(ctx, ret);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Call `fn(argv...)` synchronously; if it throws, schedule the exception
|
|
117
|
+
* to re-throw on the next tick. Caller is not informed of the throw. */
|
|
118
|
+
static void run_safely(JSContext* ctx, JSValue fn, int argc, JSValue* argv) {
|
|
119
|
+
JSValue ret = JS_Call(ctx, fn, JS_UNDEFINED, argc, argv);
|
|
120
|
+
if (JS_IsException(ret)) {
|
|
121
|
+
JSValue exc = JS_GetException(ctx);
|
|
122
|
+
panic_async(ctx, exc);
|
|
123
|
+
} else {
|
|
124
|
+
JS_FreeValue(ctx, ret);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Same panic-on-throw semantics as run_safely, for invoke-by-method calls
|
|
129
|
+
* (used when we don't have direct access to the C-level subscriber struct
|
|
130
|
+
* — multicast dispatch, from-iterable, from-promise). */
|
|
131
|
+
static void invoke_safely(JSContext* ctx, JSValueConst this_val, JSAtom method, int argc,
|
|
132
|
+
JSValueConst* argv) {
|
|
133
|
+
JSValue ret = JS_Invoke(ctx, this_val, method, argc, argv);
|
|
134
|
+
if (JS_IsException(ret)) {
|
|
135
|
+
JSValue exc = JS_GetException(ctx);
|
|
136
|
+
panic_async(ctx, exc);
|
|
137
|
+
} else {
|
|
138
|
+
JS_FreeValue(ctx, ret);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Run all registered teardowns in reverse insertion order. Throws are
|
|
143
|
+
* scheduled to re-throw async via panic_async, so subsequent teardowns
|
|
144
|
+
* still run synchronously. */
|
|
145
|
+
static void run_teardowns(JSContext* ctx, SubscriberData* d) {
|
|
146
|
+
/* Swap into a local list. If a teardown calls addTeardown synchronously,
|
|
147
|
+
* the SubscriberData.closed flag is already true so addTeardown fires the
|
|
148
|
+
* new callback immediately (handled in subscriber_add_teardown). */
|
|
149
|
+
std::vector<JSValue> list;
|
|
150
|
+
list.swap(d->teardowns);
|
|
151
|
+
for (auto it = list.rbegin(); it != list.rend(); ++it) {
|
|
152
|
+
run_safely(ctx, *it, 0, nullptr);
|
|
153
|
+
JS_FreeValue(ctx, *it);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── Subscriber ─────────────────────────────────────────────────── */
|
|
158
|
+
|
|
159
|
+
static void subscriber_finalizer(JSRuntime* rt, JSValue val) {
|
|
160
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque(val, subscriber_class_id));
|
|
161
|
+
if (!d) return;
|
|
162
|
+
JS_FreeValueRT(rt, d->observer);
|
|
163
|
+
JS_FreeValueRT(rt, d->next_fn);
|
|
164
|
+
JS_FreeValueRT(rt, d->complete_fn);
|
|
165
|
+
for (auto& td : d->teardowns) JS_FreeValueRT(rt, td);
|
|
166
|
+
delete d;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static void subscriber_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
170
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque(val, subscriber_class_id));
|
|
171
|
+
if (!d) return;
|
|
172
|
+
JS_MarkValue(rt, d->observer, mark_func);
|
|
173
|
+
JS_MarkValue(rt, d->next_fn, mark_func);
|
|
174
|
+
JS_MarkValue(rt, d->complete_fn, mark_func);
|
|
175
|
+
for (auto& td : d->teardowns) JS_MarkValue(rt, td, mark_func);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static JSClassDef subscriber_class_def = {
|
|
179
|
+
"Subscriber",
|
|
180
|
+
subscriber_finalizer,
|
|
181
|
+
subscriber_gc_mark,
|
|
182
|
+
nullptr,
|
|
183
|
+
nullptr,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
static JSValue subscriber_next(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
187
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
188
|
+
if (!d) return JS_EXCEPTION;
|
|
189
|
+
if (d->closed) return JS_UNDEFINED;
|
|
190
|
+
if (!JS_IsUndefined(d->next_fn)) {
|
|
191
|
+
JSValue arg = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
192
|
+
run_safely(ctx, d->next_fn, 1, &arg);
|
|
193
|
+
}
|
|
194
|
+
return JS_UNDEFINED;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static JSValue subscriber_complete(JSContext* ctx, JSValueConst this_val, int argc,
|
|
198
|
+
JSValueConst* argv) {
|
|
199
|
+
(void)argc;
|
|
200
|
+
(void)argv;
|
|
201
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
202
|
+
if (!d) return JS_EXCEPTION;
|
|
203
|
+
if (d->closed) return JS_UNDEFINED;
|
|
204
|
+
d->closed = true;
|
|
205
|
+
if (!JS_IsUndefined(d->complete_fn)) {
|
|
206
|
+
run_safely(ctx, d->complete_fn, 0, nullptr);
|
|
207
|
+
}
|
|
208
|
+
run_teardowns(ctx, d);
|
|
209
|
+
return JS_UNDEFINED;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static JSValue subscriber_add_teardown(JSContext* ctx, JSValueConst this_val, int argc,
|
|
213
|
+
JSValueConst* argv) {
|
|
214
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
215
|
+
if (!d) return JS_EXCEPTION;
|
|
216
|
+
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
|
|
217
|
+
return JS_ThrowTypeError(ctx, "addTeardown: argument must be a function");
|
|
218
|
+
}
|
|
219
|
+
if (d->closed) {
|
|
220
|
+
/* Late teardown registration — fire immediately to avoid leaks. */
|
|
221
|
+
run_safely(ctx, argv[0], 0, nullptr);
|
|
222
|
+
} else {
|
|
223
|
+
d->teardowns.push_back(JS_DupValue(ctx, argv[0]));
|
|
224
|
+
}
|
|
225
|
+
return JS_UNDEFINED;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static JSValue subscriber_get_closed(JSContext* ctx, JSValueConst this_val) {
|
|
229
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
230
|
+
if (!d) return JS_EXCEPTION;
|
|
231
|
+
return JS_NewBool(ctx, d->closed);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static const JSCFunctionListEntry subscriber_proto_funcs[] = {
|
|
235
|
+
JS_CFUNC_DEF("next", 1, subscriber_next),
|
|
236
|
+
JS_CFUNC_DEF("complete", 0, subscriber_complete),
|
|
237
|
+
JS_CFUNC_DEF("addTeardown", 1, subscriber_add_teardown),
|
|
238
|
+
JS_CGETSET_DEF("closed", subscriber_get_closed, nullptr),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/* ── Subscription ─────────────────────────────────────────────────── */
|
|
242
|
+
|
|
243
|
+
static void subscription_finalizer(JSRuntime* rt, JSValue val) {
|
|
244
|
+
auto* d = static_cast<SubscriptionData*>(JS_GetOpaque(val, subscription_class_id));
|
|
245
|
+
if (!d) return;
|
|
246
|
+
JS_FreeValueRT(rt, d->subscriber_value);
|
|
247
|
+
delete d;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static void subscription_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
251
|
+
auto* d = static_cast<SubscriptionData*>(JS_GetOpaque(val, subscription_class_id));
|
|
252
|
+
if (!d) return;
|
|
253
|
+
JS_MarkValue(rt, d->subscriber_value, mark_func);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static JSClassDef subscription_class_def = {
|
|
257
|
+
"Subscription",
|
|
258
|
+
subscription_finalizer,
|
|
259
|
+
subscription_gc_mark,
|
|
260
|
+
nullptr,
|
|
261
|
+
nullptr,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
static JSValue subscription_unsubscribe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
265
|
+
JSValueConst* argv) {
|
|
266
|
+
(void)argc;
|
|
267
|
+
(void)argv;
|
|
268
|
+
auto* sd =
|
|
269
|
+
static_cast<SubscriptionData*>(JS_GetOpaque2(ctx, this_val, subscription_class_id));
|
|
270
|
+
if (!sd) return JS_EXCEPTION;
|
|
271
|
+
auto* sub = static_cast<SubscriberData*>(JS_GetOpaque(sd->subscriber_value,
|
|
272
|
+
subscriber_class_id));
|
|
273
|
+
if (!sub || sub->closed) return JS_UNDEFINED;
|
|
274
|
+
sub->closed = true;
|
|
275
|
+
/* unsubscribe() is silent — does NOT call observer.complete().
|
|
276
|
+
* Only natural producer-driven completion fires observer.complete(). */
|
|
277
|
+
run_teardowns(ctx, sub);
|
|
278
|
+
return JS_UNDEFINED;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static const JSCFunctionListEntry subscription_proto_funcs[] = {
|
|
282
|
+
JS_CFUNC_DEF("unsubscribe", 0, subscription_unsubscribe),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/* Build a Subscription wrapping the given subscriber value. Takes ownership of
|
|
286
|
+
* subscriber_val (caller must not free after this point). */
|
|
287
|
+
static JSValue make_subscription(JSContext* ctx, JSValue subscriber_val) {
|
|
288
|
+
JSValue obj = JS_NewObjectClass(ctx, subscription_class_id);
|
|
289
|
+
if (JS_IsException(obj)) {
|
|
290
|
+
JS_FreeValue(ctx, subscriber_val);
|
|
291
|
+
return obj;
|
|
292
|
+
}
|
|
293
|
+
auto* d = new SubscriptionData{subscriber_val};
|
|
294
|
+
JS_SetOpaque(obj, d);
|
|
295
|
+
return obj;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* ── Observable ───────────────────────────────────────────────────── */
|
|
299
|
+
|
|
300
|
+
static void observable_finalizer(JSRuntime* rt, JSValue val) {
|
|
301
|
+
auto* d = static_cast<ObservableData*>(JS_GetOpaque(val, observable_class_id));
|
|
302
|
+
if (!d) return;
|
|
303
|
+
JS_FreeValueRT(rt, d->subscribe_cb);
|
|
304
|
+
delete d;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static void observable_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
308
|
+
auto* d = static_cast<ObservableData*>(JS_GetOpaque(val, observable_class_id));
|
|
309
|
+
if (!d) return;
|
|
310
|
+
JS_MarkValue(rt, d->subscribe_cb, mark_func);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
static JSClassDef observable_class_def = {
|
|
314
|
+
"Observable",
|
|
315
|
+
observable_finalizer,
|
|
316
|
+
observable_gc_mark,
|
|
317
|
+
nullptr,
|
|
318
|
+
nullptr,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/* Run a subscribe callback against an observer, return a Subscription.
|
|
322
|
+
* subscribe_cb is borrowed (not freed). Observer is borrowed.
|
|
323
|
+
* Used by both Observable.prototype.subscribe and the static factories. */
|
|
324
|
+
static JSValue subscribe_with_callback(JSContext* ctx, JSValueConst subscribe_cb,
|
|
325
|
+
JSValueConst observer) {
|
|
326
|
+
JSValue next_fn = JS_UNDEFINED;
|
|
327
|
+
JSValue complete_fn = JS_UNDEFINED;
|
|
328
|
+
JSValue observer_dup = JS_UNDEFINED;
|
|
329
|
+
|
|
330
|
+
if (!JS_IsUndefined(observer) && !JS_IsNull(observer)) {
|
|
331
|
+
if (JS_IsFunction(ctx, observer)) {
|
|
332
|
+
next_fn = JS_DupValue(ctx, observer);
|
|
333
|
+
} else if (JS_IsObject(observer)) {
|
|
334
|
+
observer_dup = JS_DupValue(ctx, observer);
|
|
335
|
+
JSValue n = JS_GetPropertyStr(ctx, observer, "next");
|
|
336
|
+
JSValue c = JS_GetPropertyStr(ctx, observer, "complete");
|
|
337
|
+
if (JS_IsFunction(ctx, n)) {
|
|
338
|
+
next_fn = n;
|
|
339
|
+
} else {
|
|
340
|
+
JS_FreeValue(ctx, n);
|
|
341
|
+
}
|
|
342
|
+
if (JS_IsFunction(ctx, c)) {
|
|
343
|
+
complete_fn = c;
|
|
344
|
+
} else {
|
|
345
|
+
JS_FreeValue(ctx, c);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
return JS_ThrowTypeError(
|
|
349
|
+
ctx, "subscribe: observer must be a function, object, undefined, or null");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
JSValue subscriber_val = JS_NewObjectClass(ctx, subscriber_class_id);
|
|
354
|
+
if (JS_IsException(subscriber_val)) {
|
|
355
|
+
JS_FreeValue(ctx, observer_dup);
|
|
356
|
+
JS_FreeValue(ctx, next_fn);
|
|
357
|
+
JS_FreeValue(ctx, complete_fn);
|
|
358
|
+
return subscriber_val;
|
|
359
|
+
}
|
|
360
|
+
auto* d = new SubscriberData{
|
|
361
|
+
ctx,
|
|
362
|
+
false,
|
|
363
|
+
observer_dup,
|
|
364
|
+
next_fn,
|
|
365
|
+
complete_fn,
|
|
366
|
+
{},
|
|
367
|
+
};
|
|
368
|
+
JS_SetOpaque(subscriber_val, d);
|
|
369
|
+
|
|
370
|
+
/* Sync emission is allowed: producer may call next/complete inside the
|
|
371
|
+
* subscribe callback. The Subscription returned to the caller may already
|
|
372
|
+
* be in a closed state by the time we return it — that's fine, idempotent
|
|
373
|
+
* unsubscribe handles it. */
|
|
374
|
+
JSValue cb_result = JS_Call(ctx, subscribe_cb, JS_UNDEFINED, 1, &subscriber_val);
|
|
375
|
+
if (JS_IsException(cb_result)) {
|
|
376
|
+
/* Producer setup threw — bubble up. Mark closed so any deferred
|
|
377
|
+
* dispatch back to this subscriber is silently dropped. */
|
|
378
|
+
d->closed = true;
|
|
379
|
+
JS_FreeValue(ctx, subscriber_val);
|
|
380
|
+
return cb_result;
|
|
381
|
+
}
|
|
382
|
+
JS_FreeValue(ctx, cb_result);
|
|
383
|
+
|
|
384
|
+
return make_subscription(ctx, subscriber_val);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
static JSValue observable_subscribe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
388
|
+
JSValueConst* argv) {
|
|
389
|
+
auto* d = static_cast<ObservableData*>(JS_GetOpaque2(ctx, this_val, observable_class_id));
|
|
390
|
+
if (!d) return JS_EXCEPTION;
|
|
391
|
+
JSValue observer = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
392
|
+
return subscribe_with_callback(ctx, d->subscribe_cb, observer);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
static JSValue observable_pipe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
396
|
+
JSValueConst* argv) {
|
|
397
|
+
JSValue current = JS_DupValue(ctx, this_val);
|
|
398
|
+
for (int i = 0; i < argc; i++) {
|
|
399
|
+
if (!JS_IsFunction(ctx, argv[i])) {
|
|
400
|
+
JS_FreeValue(ctx, current);
|
|
401
|
+
return JS_ThrowTypeError(ctx, "pipe: arguments must be operator functions");
|
|
402
|
+
}
|
|
403
|
+
JSValue next = JS_Call(ctx, argv[i], JS_UNDEFINED, 1, ¤t);
|
|
404
|
+
JS_FreeValue(ctx, current);
|
|
405
|
+
if (JS_IsException(next)) return next;
|
|
406
|
+
current = next;
|
|
407
|
+
}
|
|
408
|
+
return current;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
static JSValue observable_constructor(JSContext* ctx, JSValueConst new_target, int argc,
|
|
412
|
+
JSValueConst* argv) {
|
|
413
|
+
(void)new_target;
|
|
414
|
+
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
|
|
415
|
+
return JS_ThrowTypeError(ctx, "Observable: constructor requires a function argument");
|
|
416
|
+
}
|
|
417
|
+
JSValue obj = JS_NewObjectClass(ctx, observable_class_id);
|
|
418
|
+
if (JS_IsException(obj)) return obj;
|
|
419
|
+
auto* d = new ObservableData{JS_DupValue(ctx, argv[0])};
|
|
420
|
+
JS_SetOpaque(obj, d);
|
|
421
|
+
return obj;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ── Observable.from ─────────────────────────────────────────────── */
|
|
425
|
+
|
|
426
|
+
/* Build an Observable from an arbitrary subscribe callback (C function). */
|
|
427
|
+
static JSValue make_observable_with_cb(JSContext* ctx, JSValue subscribe_cb_taking_ownership) {
|
|
428
|
+
JSValue obj = JS_NewObjectClass(ctx, observable_class_id);
|
|
429
|
+
if (JS_IsException(obj)) {
|
|
430
|
+
JS_FreeValue(ctx, subscribe_cb_taking_ownership);
|
|
431
|
+
return obj;
|
|
432
|
+
}
|
|
433
|
+
auto* d = new ObservableData{subscribe_cb_taking_ownership};
|
|
434
|
+
JS_SetOpaque(obj, d);
|
|
435
|
+
return obj;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* QuickJS-NG doesn't export JS_GetIterator/JS_IteratorNext. Implement
|
|
439
|
+
* iteration manually via Symbol.iterator + .next() / .done / .value.
|
|
440
|
+
* Returns the iterator object on success, JS_EXCEPTION on error.
|
|
441
|
+
* Caller frees the returned value. */
|
|
442
|
+
static JSValue get_iterator(JSContext* ctx, JSValueConst src) {
|
|
443
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
444
|
+
JSValue symbol_obj = JS_GetPropertyStr(ctx, global, "Symbol");
|
|
445
|
+
JS_FreeValue(ctx, global);
|
|
446
|
+
JSValue iter_sym = JS_GetPropertyStr(ctx, symbol_obj, "iterator");
|
|
447
|
+
JS_FreeValue(ctx, symbol_obj);
|
|
448
|
+
if (JS_IsException(iter_sym)) return iter_sym;
|
|
449
|
+
|
|
450
|
+
JSAtom iter_atom = JS_ValueToAtom(ctx, iter_sym);
|
|
451
|
+
JS_FreeValue(ctx, iter_sym);
|
|
452
|
+
if (iter_atom == JS_ATOM_NULL) return JS_EXCEPTION;
|
|
453
|
+
|
|
454
|
+
JSValue iter_method = JS_GetProperty(ctx, src, iter_atom);
|
|
455
|
+
JS_FreeAtom(ctx, iter_atom);
|
|
456
|
+
if (JS_IsException(iter_method)) return iter_method;
|
|
457
|
+
if (!JS_IsFunction(ctx, iter_method)) {
|
|
458
|
+
JS_FreeValue(ctx, iter_method);
|
|
459
|
+
return JS_ThrowTypeError(ctx, "value is not iterable");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
JSValue iterator = JS_Call(ctx, iter_method, src, 0, nullptr);
|
|
463
|
+
JS_FreeValue(ctx, iter_method);
|
|
464
|
+
return iterator;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Pull the next value from a manually-driven iterator.
|
|
468
|
+
* Sets *done = true if iteration is complete (and returns JS_UNDEFINED).
|
|
469
|
+
* Returns JS_EXCEPTION on protocol error. Caller frees the returned value. */
|
|
470
|
+
static JSValue iterator_next(JSContext* ctx, JSValueConst iterator, bool* done) {
|
|
471
|
+
JSAtom next_atom = JS_NewAtom(ctx, "next");
|
|
472
|
+
JSValue result = JS_Invoke(ctx, iterator, next_atom, 0, nullptr);
|
|
473
|
+
JS_FreeAtom(ctx, next_atom);
|
|
474
|
+
if (JS_IsException(result)) return result;
|
|
475
|
+
|
|
476
|
+
JSValue done_val = JS_GetPropertyStr(ctx, result, "done");
|
|
477
|
+
int done_int = JS_ToBool(ctx, done_val);
|
|
478
|
+
JS_FreeValue(ctx, done_val);
|
|
479
|
+
if (done_int < 0) {
|
|
480
|
+
JS_FreeValue(ctx, result);
|
|
481
|
+
return JS_EXCEPTION;
|
|
482
|
+
}
|
|
483
|
+
*done = done_int == 1;
|
|
484
|
+
|
|
485
|
+
if (*done) {
|
|
486
|
+
JS_FreeValue(ctx, result);
|
|
487
|
+
return JS_UNDEFINED;
|
|
488
|
+
}
|
|
489
|
+
JSValue value = JS_GetPropertyStr(ctx, result, "value");
|
|
490
|
+
JS_FreeValue(ctx, result);
|
|
491
|
+
return value;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Helper: Observable.from(iterable) — sync drain. The iterable is captured
|
|
495
|
+
* via a small wrapper object so the C function can recover it on subscribe. */
|
|
496
|
+
|
|
497
|
+
/* Closure data attached to a from-iterable subscribe callback. */
|
|
498
|
+
struct FromIterableCtx {
|
|
499
|
+
JSValue iterable;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
static JSClassID from_iter_class_id;
|
|
503
|
+
static void from_iter_finalizer(JSRuntime* rt, JSValue val) {
|
|
504
|
+
auto* c = static_cast<FromIterableCtx*>(JS_GetOpaque(val, from_iter_class_id));
|
|
505
|
+
if (!c) return;
|
|
506
|
+
JS_FreeValueRT(rt, c->iterable);
|
|
507
|
+
delete c;
|
|
508
|
+
}
|
|
509
|
+
static void from_iter_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
510
|
+
auto* c = static_cast<FromIterableCtx*>(JS_GetOpaque(val, from_iter_class_id));
|
|
511
|
+
if (!c) return;
|
|
512
|
+
JS_MarkValue(rt, c->iterable, mark_func);
|
|
513
|
+
}
|
|
514
|
+
static JSClassDef from_iter_class_def = {
|
|
515
|
+
"FromIterableCtx",
|
|
516
|
+
from_iter_finalizer,
|
|
517
|
+
from_iter_gc_mark,
|
|
518
|
+
nullptr,
|
|
519
|
+
nullptr,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
static JSValue from_iterable_subscribe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
523
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
524
|
+
(void)this_val;
|
|
525
|
+
(void)magic;
|
|
526
|
+
if (argc < 1) return JS_UNDEFINED;
|
|
527
|
+
JSValue subscriber = argv[0];
|
|
528
|
+
|
|
529
|
+
/* func_data[0] holds an opaque object carrying the iterable. */
|
|
530
|
+
auto* c = static_cast<FromIterableCtx*>(JS_GetOpaque(func_data[0], from_iter_class_id));
|
|
531
|
+
if (!c) return JS_ThrowInternalError(ctx, "from_iterable: missing context");
|
|
532
|
+
|
|
533
|
+
JSValue iterator = get_iterator(ctx, c->iterable);
|
|
534
|
+
if (JS_IsException(iterator)) return iterator;
|
|
535
|
+
|
|
536
|
+
JSAtom next_atom = JS_NewAtom(ctx, "next");
|
|
537
|
+
|
|
538
|
+
bool done = false;
|
|
539
|
+
while (!done) {
|
|
540
|
+
/* Check closed before each pull so take(N) downstream can stop us
|
|
541
|
+
* synchronously inside the loop. */
|
|
542
|
+
auto* sd = static_cast<SubscriberData*>(
|
|
543
|
+
JS_GetOpaque(subscriber, subscriber_class_id));
|
|
544
|
+
if (!sd || sd->closed) break;
|
|
545
|
+
|
|
546
|
+
JSValue value = iterator_next(ctx, iterator, &done);
|
|
547
|
+
if (JS_IsException(value)) {
|
|
548
|
+
JS_FreeAtom(ctx, next_atom);
|
|
549
|
+
JS_FreeValue(ctx, iterator);
|
|
550
|
+
return value;
|
|
551
|
+
}
|
|
552
|
+
if (done) {
|
|
553
|
+
JS_FreeValue(ctx, value);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
invoke_safely(ctx, subscriber, next_atom, 1, &value);
|
|
557
|
+
JS_FreeValue(ctx, value);
|
|
558
|
+
}
|
|
559
|
+
JS_FreeAtom(ctx, next_atom);
|
|
560
|
+
JS_FreeValue(ctx, iterator);
|
|
561
|
+
|
|
562
|
+
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(subscriber, subscriber_class_id));
|
|
563
|
+
if (sd && !sd->closed) {
|
|
564
|
+
JSAtom complete_atom = JS_NewAtom(ctx, "complete");
|
|
565
|
+
invoke_safely(ctx, subscriber, complete_atom, 0, nullptr);
|
|
566
|
+
JS_FreeAtom(ctx, complete_atom);
|
|
567
|
+
}
|
|
568
|
+
return JS_UNDEFINED;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
struct FromPromiseCtx {
|
|
572
|
+
JSValue promise;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
static JSClassID from_promise_class_id;
|
|
576
|
+
static void from_promise_finalizer(JSRuntime* rt, JSValue val) {
|
|
577
|
+
auto* c = static_cast<FromPromiseCtx*>(JS_GetOpaque(val, from_promise_class_id));
|
|
578
|
+
if (!c) return;
|
|
579
|
+
JS_FreeValueRT(rt, c->promise);
|
|
580
|
+
delete c;
|
|
581
|
+
}
|
|
582
|
+
static void from_promise_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
583
|
+
auto* c = static_cast<FromPromiseCtx*>(JS_GetOpaque(val, from_promise_class_id));
|
|
584
|
+
if (!c) return;
|
|
585
|
+
JS_MarkValue(rt, c->promise, mark_func);
|
|
586
|
+
}
|
|
587
|
+
static JSClassDef from_promise_class_def = {
|
|
588
|
+
"FromPromiseCtx",
|
|
589
|
+
from_promise_finalizer,
|
|
590
|
+
from_promise_gc_mark,
|
|
591
|
+
nullptr,
|
|
592
|
+
nullptr,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/* The .then handler: argv[0] = resolved value, func_data[0] = subscriber. */
|
|
596
|
+
static JSValue from_promise_on_resolve(JSContext* ctx, JSValueConst this_val, int argc,
|
|
597
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
598
|
+
(void)this_val;
|
|
599
|
+
(void)magic;
|
|
600
|
+
JSValue subscriber = func_data[0];
|
|
601
|
+
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(subscriber, subscriber_class_id));
|
|
602
|
+
if (!sd || sd->closed) return JS_UNDEFINED;
|
|
603
|
+
|
|
604
|
+
JSValue value = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
605
|
+
JSAtom next_atom = JS_NewAtom(ctx, "next");
|
|
606
|
+
invoke_safely(ctx, subscriber, next_atom, 1, &value);
|
|
607
|
+
JS_FreeAtom(ctx, next_atom);
|
|
608
|
+
|
|
609
|
+
/* Re-check closed: an observer's next handler may have unsubscribed. */
|
|
610
|
+
if (sd->closed) return JS_UNDEFINED;
|
|
611
|
+
JSAtom complete_atom = JS_NewAtom(ctx, "complete");
|
|
612
|
+
invoke_safely(ctx, subscriber, complete_atom, 0, nullptr);
|
|
613
|
+
JS_FreeAtom(ctx, complete_atom);
|
|
614
|
+
return JS_UNDEFINED;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
static JSValue from_promise_subscribe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
618
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
619
|
+
(void)this_val;
|
|
620
|
+
(void)magic;
|
|
621
|
+
if (argc < 1) return JS_UNDEFINED;
|
|
622
|
+
JSValueConst subscriber = argv[0];
|
|
623
|
+
|
|
624
|
+
auto* c = static_cast<FromPromiseCtx*>(JS_GetOpaque(func_data[0], from_promise_class_id));
|
|
625
|
+
if (!c) return JS_ThrowInternalError(ctx, "from_promise: missing context");
|
|
626
|
+
|
|
627
|
+
JSValue subscriber_dup = JS_DupValue(ctx, subscriber);
|
|
628
|
+
JSValue then_handler =
|
|
629
|
+
JS_NewCFunctionData(ctx, from_promise_on_resolve, 1, 0, 1, &subscriber_dup);
|
|
630
|
+
JS_FreeValue(ctx, subscriber_dup);
|
|
631
|
+
|
|
632
|
+
JSAtom then_atom = JS_NewAtom(ctx, "then");
|
|
633
|
+
JSValue ret = JS_Invoke(ctx, c->promise, then_atom, 1, &then_handler);
|
|
634
|
+
JS_FreeAtom(ctx, then_atom);
|
|
635
|
+
JS_FreeValue(ctx, then_handler);
|
|
636
|
+
if (JS_IsException(ret)) return ret;
|
|
637
|
+
JS_FreeValue(ctx, ret);
|
|
638
|
+
return JS_UNDEFINED;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
static JSValue observable_from(JSContext* ctx, JSValueConst this_val, int argc,
|
|
642
|
+
JSValueConst* argv) {
|
|
643
|
+
(void)this_val;
|
|
644
|
+
if (argc < 1) {
|
|
645
|
+
return JS_ThrowTypeError(ctx, "Observable.from: requires a source argument");
|
|
646
|
+
}
|
|
647
|
+
JSValueConst src = argv[0];
|
|
648
|
+
|
|
649
|
+
/* (1) Already an Observable — passthrough. */
|
|
650
|
+
if (JS_IsObject(src) &&
|
|
651
|
+
JS_GetOpaque(src, observable_class_id) != nullptr) {
|
|
652
|
+
return JS_DupValue(ctx, src);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* (2) PromiseLike — has a callable .then */
|
|
656
|
+
if (JS_IsObject(src)) {
|
|
657
|
+
JSValue then_method = JS_GetPropertyStr(ctx, src, "then");
|
|
658
|
+
bool is_thenable = JS_IsFunction(ctx, then_method);
|
|
659
|
+
JS_FreeValue(ctx, then_method);
|
|
660
|
+
if (is_thenable) {
|
|
661
|
+
JSValue ctx_obj = JS_NewObjectClass(ctx, from_promise_class_id);
|
|
662
|
+
if (JS_IsException(ctx_obj)) return ctx_obj;
|
|
663
|
+
auto* c = new FromPromiseCtx{JS_DupValue(ctx, src)};
|
|
664
|
+
JS_SetOpaque(ctx_obj, c);
|
|
665
|
+
JSValue subscribe_cb =
|
|
666
|
+
JS_NewCFunctionData(ctx, from_promise_subscribe, 1, 0, 1, &ctx_obj);
|
|
667
|
+
JS_FreeValue(ctx, ctx_obj);
|
|
668
|
+
return make_observable_with_cb(ctx, subscribe_cb);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* (3) Iterable — has @@iterator. */
|
|
673
|
+
if (JS_IsObject(src) || JS_IsString(src)) {
|
|
674
|
+
JSValue iter_test = get_iterator(ctx, src);
|
|
675
|
+
if (!JS_IsException(iter_test)) {
|
|
676
|
+
JS_FreeValue(ctx, iter_test);
|
|
677
|
+
JSValue ctx_obj = JS_NewObjectClass(ctx, from_iter_class_id);
|
|
678
|
+
if (JS_IsException(ctx_obj)) return ctx_obj;
|
|
679
|
+
auto* c = new FromIterableCtx{JS_DupValue(ctx, src)};
|
|
680
|
+
JS_SetOpaque(ctx_obj, c);
|
|
681
|
+
JSValue subscribe_cb =
|
|
682
|
+
JS_NewCFunctionData(ctx, from_iterable_subscribe, 1, 0, 1, &ctx_obj);
|
|
683
|
+
JS_FreeValue(ctx, ctx_obj);
|
|
684
|
+
return make_observable_with_cb(ctx, subscribe_cb);
|
|
685
|
+
}
|
|
686
|
+
/* Clear the iterability check failure so we can throw a clearer
|
|
687
|
+
* "unsupported source" error below. */
|
|
688
|
+
JS_GetException(ctx);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return JS_ThrowTypeError(
|
|
692
|
+
ctx, "Observable.from: source must be a Promise, Iterable, or Observable");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* ── Observable.withEmitters ────────────────────────────────────── */
|
|
696
|
+
|
|
697
|
+
/* The multicast source backing withEmitters().
|
|
698
|
+
*
|
|
699
|
+
* - subscribers: snapshot-on-dispatch via slice-into-local, so a subscriber
|
|
700
|
+
* unsubscribing during dispatch doesn't shift indices we're iterating.
|
|
701
|
+
* - completed: idempotent close. After complete(), late subscribers receive
|
|
702
|
+
* complete() immediately during subscribe.
|
|
703
|
+
* - Re-entrant next() dispatches recursively (matches RxJS Subject default).
|
|
704
|
+
*/
|
|
705
|
+
struct MulticastState {
|
|
706
|
+
JSContext* ctx;
|
|
707
|
+
bool completed;
|
|
708
|
+
/* JSValues for each subscriber: opaque references kept alive by the
|
|
709
|
+
* MulticastState's GC mark hook. */
|
|
710
|
+
std::vector<JSValue> subscribers;
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
static JSClassID multicast_class_id;
|
|
714
|
+
|
|
715
|
+
static void multicast_finalizer(JSRuntime* rt, JSValue val) {
|
|
716
|
+
auto* m = static_cast<MulticastState*>(JS_GetOpaque(val, multicast_class_id));
|
|
717
|
+
if (!m) return;
|
|
718
|
+
for (auto& s : m->subscribers) JS_FreeValueRT(rt, s);
|
|
719
|
+
delete m;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
static void multicast_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
723
|
+
auto* m = static_cast<MulticastState*>(JS_GetOpaque(val, multicast_class_id));
|
|
724
|
+
if (!m) return;
|
|
725
|
+
for (auto& s : m->subscribers) JS_MarkValue(rt, s, mark_func);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
static JSClassDef multicast_class_def = {
|
|
729
|
+
"MulticastState",
|
|
730
|
+
multicast_finalizer,
|
|
731
|
+
multicast_gc_mark,
|
|
732
|
+
nullptr,
|
|
733
|
+
nullptr,
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
/* The subscribe callback for the multicast Observable. */
|
|
737
|
+
static JSValue multicast_subscribe(JSContext* ctx, JSValueConst this_val, int argc,
|
|
738
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
739
|
+
(void)this_val;
|
|
740
|
+
(void)magic;
|
|
741
|
+
if (argc < 1) return JS_UNDEFINED;
|
|
742
|
+
JSValueConst subscriber = argv[0];
|
|
743
|
+
|
|
744
|
+
auto* m = static_cast<MulticastState*>(JS_GetOpaque(func_data[0], multicast_class_id));
|
|
745
|
+
if (!m) return JS_ThrowInternalError(ctx, "multicast_subscribe: missing context");
|
|
746
|
+
|
|
747
|
+
if (m->completed) {
|
|
748
|
+
/* Late subscriber: complete immediately. */
|
|
749
|
+
JSAtom complete_atom = JS_NewAtom(ctx, "complete");
|
|
750
|
+
invoke_safely(ctx, subscriber, complete_atom, 0, nullptr);
|
|
751
|
+
JS_FreeAtom(ctx, complete_atom);
|
|
752
|
+
return JS_UNDEFINED;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Add to roster. The multicast holds a reference; the teardown removes it. */
|
|
756
|
+
JSValue dup = JS_DupValue(ctx, subscriber);
|
|
757
|
+
m->subscribers.push_back(dup);
|
|
758
|
+
|
|
759
|
+
/* Register teardown that removes this subscriber from the multicast list.
|
|
760
|
+
* Capture the multicast context object via func_data so the teardown
|
|
761
|
+
* survives the subscribe call returning. */
|
|
762
|
+
JSValue mc_ref = JS_DupValue(ctx, func_data[0]);
|
|
763
|
+
JSValue teardown_data[2] = {mc_ref, dup};
|
|
764
|
+
|
|
765
|
+
auto teardown_fn = [](JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
|
|
766
|
+
int magic, JSValue* func_data) -> JSValue {
|
|
767
|
+
(void)this_val;
|
|
768
|
+
(void)argc;
|
|
769
|
+
(void)argv;
|
|
770
|
+
(void)magic;
|
|
771
|
+
auto* m = static_cast<MulticastState*>(
|
|
772
|
+
JS_GetOpaque(func_data[0], multicast_class_id));
|
|
773
|
+
if (!m) return JS_UNDEFINED;
|
|
774
|
+
for (auto it = m->subscribers.begin(); it != m->subscribers.end(); ++it) {
|
|
775
|
+
if (JS_VALUE_GET_PTR(*it) == JS_VALUE_GET_PTR(func_data[1])) {
|
|
776
|
+
JS_FreeValue(ctx, *it);
|
|
777
|
+
m->subscribers.erase(it);
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return JS_UNDEFINED;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
JSValue td = JS_NewCFunctionData(ctx, teardown_fn, 0, 0, 2, teardown_data);
|
|
785
|
+
JS_FreeValue(ctx, mc_ref);
|
|
786
|
+
/* dup is owned by m->subscribers, no free here. */
|
|
787
|
+
|
|
788
|
+
JSValue ret = JS_Invoke(ctx, subscriber, JS_NewAtom(ctx, "addTeardown"), 1, &td);
|
|
789
|
+
JS_FreeValue(ctx, td);
|
|
790
|
+
if (JS_IsException(ret)) return ret;
|
|
791
|
+
JS_FreeValue(ctx, ret);
|
|
792
|
+
|
|
793
|
+
return JS_UNDEFINED;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* withEmitters()'s `next` function. func_data[0] = multicast context. */
|
|
797
|
+
static JSValue multicast_emit_next(JSContext* ctx, JSValueConst this_val, int argc,
|
|
798
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
799
|
+
(void)this_val;
|
|
800
|
+
(void)magic;
|
|
801
|
+
auto* m = static_cast<MulticastState*>(JS_GetOpaque(func_data[0], multicast_class_id));
|
|
802
|
+
if (!m || m->completed) return JS_UNDEFINED;
|
|
803
|
+
|
|
804
|
+
/* Snapshot for safe iteration: a subscriber's next handler might
|
|
805
|
+
* unsubscribe during dispatch, modifying m->subscribers. */
|
|
806
|
+
std::vector<JSValue> snapshot;
|
|
807
|
+
snapshot.reserve(m->subscribers.size());
|
|
808
|
+
for (auto& s : m->subscribers) snapshot.push_back(JS_DupValue(ctx, s));
|
|
809
|
+
|
|
810
|
+
JSValue value = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
811
|
+
JSAtom next_atom = JS_NewAtom(ctx, "next");
|
|
812
|
+
for (auto& s : snapshot) {
|
|
813
|
+
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(s, subscriber_class_id));
|
|
814
|
+
if (!sd || sd->closed) {
|
|
815
|
+
JS_FreeValue(ctx, s);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
invoke_safely(ctx, s, next_atom, 1, &value);
|
|
819
|
+
JS_FreeValue(ctx, s);
|
|
820
|
+
}
|
|
821
|
+
JS_FreeAtom(ctx, next_atom);
|
|
822
|
+
return JS_UNDEFINED;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* withEmitters()'s `complete` function. */
|
|
826
|
+
static JSValue multicast_emit_complete(JSContext* ctx, JSValueConst this_val, int argc,
|
|
827
|
+
JSValueConst* argv, int magic, JSValue* func_data) {
|
|
828
|
+
(void)this_val;
|
|
829
|
+
(void)argc;
|
|
830
|
+
(void)argv;
|
|
831
|
+
(void)magic;
|
|
832
|
+
auto* m = static_cast<MulticastState*>(JS_GetOpaque(func_data[0], multicast_class_id));
|
|
833
|
+
if (!m || m->completed) return JS_UNDEFINED;
|
|
834
|
+
m->completed = true;
|
|
835
|
+
|
|
836
|
+
/* Drain subscribers, completing each. We've taken them out of the roster
|
|
837
|
+
* before dispatch so any reentrant emit() finds an empty list. */
|
|
838
|
+
std::vector<JSValue> snapshot;
|
|
839
|
+
snapshot.swap(m->subscribers);
|
|
840
|
+
|
|
841
|
+
JSAtom complete_atom = JS_NewAtom(ctx, "complete");
|
|
842
|
+
for (auto& s : snapshot) {
|
|
843
|
+
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(s, subscriber_class_id));
|
|
844
|
+
if (sd && !sd->closed) {
|
|
845
|
+
invoke_safely(ctx, s, complete_atom, 0, nullptr);
|
|
846
|
+
}
|
|
847
|
+
JS_FreeValue(ctx, s);
|
|
848
|
+
}
|
|
849
|
+
JS_FreeAtom(ctx, complete_atom);
|
|
850
|
+
return JS_UNDEFINED;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
static JSValue observable_with_emitters(JSContext* ctx, JSValueConst this_val, int argc,
|
|
854
|
+
JSValueConst* argv) {
|
|
855
|
+
(void)this_val;
|
|
856
|
+
(void)argc;
|
|
857
|
+
(void)argv;
|
|
858
|
+
|
|
859
|
+
JSValue mc_obj = JS_NewObjectClass(ctx, multicast_class_id);
|
|
860
|
+
if (JS_IsException(mc_obj)) return mc_obj;
|
|
861
|
+
auto* m = new MulticastState{ctx, false, {}};
|
|
862
|
+
JS_SetOpaque(mc_obj, m);
|
|
863
|
+
|
|
864
|
+
JSValue subscribe_cb = JS_NewCFunctionData(ctx, multicast_subscribe, 1, 0, 1, &mc_obj);
|
|
865
|
+
JSValue observable = make_observable_with_cb(ctx, subscribe_cb);
|
|
866
|
+
if (JS_IsException(observable)) {
|
|
867
|
+
JS_FreeValue(ctx, mc_obj);
|
|
868
|
+
return observable;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
JSValue next_fn = JS_NewCFunctionData(ctx, multicast_emit_next, 1, 0, 1, &mc_obj);
|
|
872
|
+
JSValue complete_fn = JS_NewCFunctionData(ctx, multicast_emit_complete, 0, 0, 1, &mc_obj);
|
|
873
|
+
JS_FreeValue(ctx, mc_obj);
|
|
874
|
+
|
|
875
|
+
JSValue result = JS_NewObject(ctx);
|
|
876
|
+
JS_DefinePropertyValueStr(ctx, result, "observable", observable, JS_PROP_C_W_E);
|
|
877
|
+
JS_DefinePropertyValueStr(ctx, result, "next", next_fn, JS_PROP_C_W_E);
|
|
878
|
+
JS_DefinePropertyValueStr(ctx, result, "complete", complete_fn, JS_PROP_C_W_E);
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* ── Observable prototype ─────────────────────────────────────────── */
|
|
883
|
+
|
|
884
|
+
static const JSCFunctionListEntry observable_proto_funcs[] = {
|
|
885
|
+
JS_CFUNC_DEF("subscribe", 1, observable_subscribe),
|
|
886
|
+
JS_CFUNC_DEF("pipe", 1, observable_pipe),
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
/* ── Module init ──────────────────────────────────────────────────── */
|
|
890
|
+
|
|
891
|
+
static int observable_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
892
|
+
/* Build prototype objects and bind to class IDs. The classes themselves
|
|
893
|
+
* are registered by mik__observable_init below. */
|
|
894
|
+
JSValue subscriber_proto = JS_NewObject(ctx);
|
|
895
|
+
JS_SetPropertyFunctionList(ctx, subscriber_proto, subscriber_proto_funcs,
|
|
896
|
+
sizeof(subscriber_proto_funcs) /
|
|
897
|
+
sizeof(subscriber_proto_funcs[0]));
|
|
898
|
+
JS_SetClassProto(ctx, subscriber_class_id, subscriber_proto);
|
|
899
|
+
|
|
900
|
+
JSValue subscription_proto = JS_NewObject(ctx);
|
|
901
|
+
JS_SetPropertyFunctionList(ctx, subscription_proto, subscription_proto_funcs,
|
|
902
|
+
sizeof(subscription_proto_funcs) /
|
|
903
|
+
sizeof(subscription_proto_funcs[0]));
|
|
904
|
+
JS_SetClassProto(ctx, subscription_class_id, subscription_proto);
|
|
905
|
+
|
|
906
|
+
JSValue obs_proto = JS_NewObject(ctx);
|
|
907
|
+
JS_SetPropertyFunctionList(ctx, obs_proto, observable_proto_funcs,
|
|
908
|
+
sizeof(observable_proto_funcs) /
|
|
909
|
+
sizeof(observable_proto_funcs[0]));
|
|
910
|
+
JS_SetClassProto(ctx, observable_class_id, obs_proto);
|
|
911
|
+
|
|
912
|
+
JSValue obs_ctor = JS_NewCFunction2(ctx, observable_constructor, "Observable", 1,
|
|
913
|
+
JS_CFUNC_constructor, 0);
|
|
914
|
+
JS_SetConstructor(ctx, obs_ctor, obs_proto);
|
|
915
|
+
|
|
916
|
+
JS_DefinePropertyValueStr(
|
|
917
|
+
ctx, obs_ctor, "from",
|
|
918
|
+
JS_NewCFunction(ctx, observable_from, "from", 1), JS_PROP_C_W_E);
|
|
919
|
+
JS_DefinePropertyValueStr(
|
|
920
|
+
ctx, obs_ctor, "withEmitters",
|
|
921
|
+
JS_NewCFunction(ctx, observable_with_emitters, "withEmitters", 0), JS_PROP_C_W_E);
|
|
922
|
+
|
|
923
|
+
JS_SetModuleExport(ctx, m, "Observable", obs_ctor);
|
|
924
|
+
return 0;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
} // namespace
|
|
928
|
+
|
|
929
|
+
JSModuleDef* mik__observable_init(JSContext* ctx) {
|
|
930
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
931
|
+
|
|
932
|
+
/* Class IDs are runtime-scoped; safe to register once per runtime. */
|
|
933
|
+
JS_NewClassID(rt, &observable_class_id);
|
|
934
|
+
JS_NewClass(rt, observable_class_id, &observable_class_def);
|
|
935
|
+
JS_NewClassID(rt, &subscriber_class_id);
|
|
936
|
+
JS_NewClass(rt, subscriber_class_id, &subscriber_class_def);
|
|
937
|
+
JS_NewClassID(rt, &subscription_class_id);
|
|
938
|
+
JS_NewClass(rt, subscription_class_id, &subscription_class_def);
|
|
939
|
+
JS_NewClassID(rt, &from_iter_class_id);
|
|
940
|
+
JS_NewClass(rt, from_iter_class_id, &from_iter_class_def);
|
|
941
|
+
JS_NewClassID(rt, &from_promise_class_id);
|
|
942
|
+
JS_NewClass(rt, from_promise_class_id, &from_promise_class_def);
|
|
943
|
+
JS_NewClassID(rt, &multicast_class_id);
|
|
944
|
+
JS_NewClass(rt, multicast_class_id, &multicast_class_def);
|
|
945
|
+
|
|
946
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:observable", observable_module_init);
|
|
947
|
+
if (!m) return nullptr;
|
|
948
|
+
JS_AddModuleExport(ctx, m, "Observable");
|
|
949
|
+
return m;
|
|
950
|
+
}
|