MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add Observable constructor and method test suite unify JS type constants into enum and flag macros

+517 -129
+422
examples/spec/observable.js
··· 1 + import { test, testThrows, testDeep, summary } from './helpers.js'; 2 + 3 + console.log('Observable Constructor Tests\n'); 4 + 5 + test('Observable is defined', typeof Observable, 'function'); 6 + test('Observable.prototype exists', typeof Observable.prototype, 'object'); 7 + test('Observable.prototype.constructor is Observable', Observable.prototype.constructor, Observable); 8 + 9 + testThrows('Constructor throws without arguments', () => new Observable()); 10 + testThrows('Constructor throws with non-function', () => new Observable({})); 11 + testThrows('Constructor throws with null', () => new Observable(null)); 12 + testThrows('Constructor throws with number', () => new Observable(1)); 13 + 14 + let subscriberCalled = false; 15 + new Observable(() => { 16 + subscriberCalled = true; 17 + }); 18 + test('Subscriber not called by constructor', subscriberCalled, false); 19 + 20 + console.log('\nObservable.prototype.subscribe Tests\n'); 21 + 22 + test('subscribe method exists', typeof Observable.prototype.subscribe, 'function'); 23 + 24 + const obs1 = new Observable(sink => null); 25 + let noThrow = true; 26 + try { 27 + obs1.subscribe(null); 28 + obs1.subscribe(undefined); 29 + obs1.subscribe(1); 30 + obs1.subscribe({}); 31 + obs1.subscribe(() => {}); 32 + } catch (e) { 33 + noThrow = false; 34 + } 35 + test('subscribe accepts any observer type', noThrow, true); 36 + 37 + const list1 = []; 38 + const error1 = new Error('test error'); 39 + new Observable(s => { 40 + s.next(1); 41 + s.error(error1); 42 + }).subscribe( 43 + x => list1.push('next:' + x), 44 + e => list1.push(e), 45 + () => list1.push('complete') 46 + ); 47 + test('First subscribe arg is next callback', list1[0], 'next:1'); 48 + test('Second subscribe arg is error callback', list1[1], error1); 49 + 50 + const list2 = []; 51 + new Observable(s => { 52 + s.complete(); 53 + }).subscribe( 54 + x => list2.push('next:' + x), 55 + e => list2.push(e), 56 + () => list2.push('complete') 57 + ); 58 + test('Third subscribe arg is complete callback', list2[0], 'complete'); 59 + 60 + let observer1 = null; 61 + new Observable(x => { 62 + observer1 = x; 63 + }).subscribe({}); 64 + test('Subscriber receives observer object', typeof observer1, 'object'); 65 + test('Observer has next method', typeof observer1.next, 'function'); 66 + test('Observer has error method', typeof observer1.error, 'function'); 67 + test('Observer has complete method', typeof observer1.complete, 'function'); 68 + 69 + console.log('\nSubscription Tests\n'); 70 + 71 + let cleanupCalled = 0; 72 + const sub1 = new Observable(observer => { 73 + return () => { 74 + cleanupCalled++; 75 + }; 76 + }).subscribe({}); 77 + 78 + test('subscribe returns subscription object', typeof sub1, 'object'); 79 + test('subscription has unsubscribe method', typeof sub1.unsubscribe, 'function'); 80 + test('subscription.closed is false before unsubscribe', sub1.closed, false); 81 + test('unsubscribe returns undefined', sub1.unsubscribe(), undefined); 82 + test('cleanup function called on unsubscribe', cleanupCalled, 1); 83 + test('subscription.closed is true after unsubscribe', sub1.closed, true); 84 + 85 + sub1.unsubscribe(); 86 + test('cleanup not called again on second unsubscribe', cleanupCalled, 1); 87 + 88 + let cleanupOnError = 0; 89 + new Observable(sink => { 90 + sink.error(1); 91 + return () => { 92 + cleanupOnError++; 93 + }; 94 + }).subscribe({ error: () => {} }); 95 + test('cleanup called on error', cleanupOnError, 1); 96 + 97 + let cleanupOnComplete = 0; 98 + new Observable(sink => { 99 + sink.complete(); 100 + return () => { 101 + cleanupOnComplete++; 102 + }; 103 + }).subscribe({}); 104 + test('cleanup called on complete', cleanupOnComplete, 1); 105 + 106 + let unsubscribeCalled = 0; 107 + const sub2 = new Observable(sink => { 108 + return { 109 + unsubscribe: () => { 110 + unsubscribeCalled++; 111 + } 112 + }; 113 + }).subscribe({}); 114 + sub2.unsubscribe(); 115 + test('subscription object with unsubscribe is valid return', unsubscribeCalled, 1); 116 + 117 + console.log('\nSubscriber Error Handling Tests\n'); 118 + 119 + const thrownError = new Error('subscriber error'); 120 + let caughtError = null; 121 + new Observable(() => { 122 + throw thrownError; 123 + }).subscribe({ 124 + error: e => { 125 + caughtError = e; 126 + } 127 + }); 128 + test('subscriber errors sent to observer.error', caughtError, thrownError); 129 + 130 + console.log('\nObserver.start Tests\n'); 131 + 132 + const events1 = []; 133 + const obs2 = new Observable(observer => { 134 + events1.push('subscriber'); 135 + observer.complete(); 136 + }); 137 + 138 + let startSubscription = null; 139 + let startThisVal = null; 140 + const observer2 = { 141 + start(subscription) { 142 + events1.push('start'); 143 + startSubscription = subscription; 144 + startThisVal = this; 145 + } 146 + }; 147 + const sub3 = obs2.subscribe(observer2); 148 + 149 + test('start called before subscriber', events1[0], 'start'); 150 + test('subscriber called after start', events1[1], 'subscriber'); 151 + test('start receives subscription', startSubscription, sub3); 152 + test('start called with observer as this', startThisVal, observer2); 153 + 154 + const events2 = []; 155 + const obs3 = new Observable(() => { 156 + events2.push('subscriber'); 157 + }); 158 + obs3.subscribe({ 159 + start(subscription) { 160 + events2.push('start'); 161 + subscription.unsubscribe(); 162 + } 163 + }); 164 + test('unsubscribe in start prevents subscriber call', events2.length, 1); 165 + test('only start was called', events2[0], 'start'); 166 + 167 + console.log('\nSubscriptionObserver.next Tests\n'); 168 + 169 + const token1 = {}; 170 + let nextReceived = null; 171 + let nextArgs = []; 172 + new Observable(observer => { 173 + observer.next(token1, 'extra'); 174 + }).subscribe({ 175 + next(value, ...args) { 176 + nextReceived = value; 177 + nextArgs = args; 178 + } 179 + }); 180 + test('next forwards value to observer', nextReceived, token1); 181 + test('next does not forward extra arguments', nextArgs.length, 0); 182 + 183 + let nextReturnVal = null; 184 + new Observable(observer => { 185 + nextReturnVal = observer.next(); 186 + observer.complete(); 187 + }).subscribe({ next: () => 'return value' }); 188 + test('next suppresses observer return value', nextReturnVal, undefined); 189 + 190 + let nextAfterClose = 'not called'; 191 + new Observable(observer => { 192 + observer.complete(); 193 + nextReturnVal = observer.next(); 194 + }).subscribe({ 195 + next: () => { 196 + nextAfterClose = 'called'; 197 + } 198 + }); 199 + test('next returns undefined when closed', nextReturnVal, undefined); 200 + test('next does not call observer when closed', nextAfterClose, 'not called'); 201 + 202 + console.log('\nSubscriptionObserver.error Tests\n'); 203 + 204 + let errorReceived = null; 205 + const errorToken = new Error('error token'); 206 + new Observable(observer => { 207 + observer.error(errorToken); 208 + }).subscribe({ 209 + error: e => { 210 + errorReceived = e; 211 + } 212 + }); 213 + test('error forwards exception to observer', errorReceived, errorToken); 214 + 215 + let errorAfterClose = 'not called'; 216 + new Observable(observer => { 217 + observer.complete(); 218 + observer.error(new Error()); 219 + }).subscribe({ 220 + error: () => { 221 + errorAfterClose = 'called'; 222 + } 223 + }); 224 + test('error does not call observer when closed', errorAfterClose, 'not called'); 225 + 226 + let nextAfterError = 'not called'; 227 + new Observable(observer => { 228 + observer.error(new Error()); 229 + observer.next(1); 230 + }).subscribe({ 231 + next: () => { 232 + nextAfterError = 'called'; 233 + }, 234 + error: () => {} 235 + }); 236 + test('next not called after error', nextAfterError, 'not called'); 237 + 238 + console.log('\nSubscriptionObserver.complete Tests\n'); 239 + 240 + let completeReceived = false; 241 + new Observable(observer => { 242 + observer.complete(); 243 + }).subscribe({ 244 + complete: () => { 245 + completeReceived = true; 246 + } 247 + }); 248 + test('complete calls observer.complete', completeReceived, true); 249 + 250 + let completeAfterClose = 'not called'; 251 + new Observable(observer => { 252 + observer.complete(); 253 + observer.complete(); 254 + }).subscribe({ 255 + complete: () => { 256 + completeAfterClose = 'called'; 257 + } 258 + }); 259 + test('complete only called once', completeAfterClose, 'called'); 260 + 261 + let nextAfterComplete = 'not called'; 262 + new Observable(observer => { 263 + observer.complete(); 264 + observer.next(1); 265 + }).subscribe({ 266 + next: () => { 267 + nextAfterComplete = 'called'; 268 + } 269 + }); 270 + test('next not called after complete', nextAfterComplete, 'not called'); 271 + 272 + console.log('\nObservable.of Tests\n'); 273 + 274 + test('Observable.of exists', typeof Observable.of, 'function'); 275 + 276 + const ofValues = []; 277 + Observable.of(1, 2, 3, 4).subscribe({ 278 + next: v => ofValues.push(v), 279 + complete: () => ofValues.push('done') 280 + }); 281 + testDeep('Observable.of delivers all values', ofValues, [1, 2, 3, 4, 'done']); 282 + 283 + const ofEmpty = []; 284 + Observable.of().subscribe({ 285 + next: v => ofEmpty.push(v), 286 + complete: () => ofEmpty.push('done') 287 + }); 288 + testDeep('Observable.of with no args calls complete', ofEmpty, ['done']); 289 + 290 + console.log('\nObservable.from Tests\n'); 291 + 292 + test('Observable.from exists', typeof Observable.from, 'function'); 293 + 294 + testThrows('Observable.from throws with null', () => Observable.from(null)); 295 + testThrows('Observable.from throws with undefined', () => Observable.from(undefined)); 296 + testThrows('Observable.from throws with no args', () => Observable.from()); 297 + 298 + const fromArray = []; 299 + Observable.from([1, 2, 3]).subscribe({ 300 + next: v => fromArray.push(v), 301 + complete: () => fromArray.push('done') 302 + }); 303 + testDeep('Observable.from works with arrays', fromArray, [1, 2, 3, 'done']); 304 + 305 + const customObservable = { 306 + [Symbol.observable]() { 307 + return new Observable(observer => { 308 + observer.next('custom'); 309 + observer.complete(); 310 + }); 311 + } 312 + }; 313 + const fromCustom = []; 314 + Observable.from(customObservable).subscribe({ 315 + next: v => fromCustom.push(v), 316 + complete: () => fromCustom.push('done') 317 + }); 318 + testDeep('Observable.from works with @@observable', fromCustom, ['custom', 'done']); 319 + 320 + const obs4 = new Observable(o => { 321 + o.next(1); 322 + o.complete(); 323 + }); 324 + const fromObs = Observable.from(obs4); 325 + test('Observable.from returns same observable if already Observable', fromObs, obs4); 326 + 327 + testThrows('Observable.from throws with non-iterable object', () => { 328 + Observable.from({}).subscribe({}); 329 + }); 330 + 331 + console.log('\nSymbol.observable Tests\n'); 332 + 333 + test('Symbol.observable exists', typeof Symbol.observable, 'symbol'); 334 + 335 + const obs5 = new Observable(() => {}); 336 + test('Observable has @@observable method', typeof obs5[Symbol.observable], 'function'); 337 + test('@@observable returns this', obs5[Symbol.observable](), obs5); 338 + 339 + console.log('\nEarly Unsubscribe Tests\n'); 340 + 341 + const earlyValues = []; 342 + let earlyCleanup = false; 343 + let sub4; 344 + new Observable(observer => { 345 + observer.next(1); 346 + observer.next(2); 347 + observer.next(3); 348 + observer.complete(); 349 + return () => { 350 + earlyCleanup = true; 351 + }; 352 + }).subscribe({ 353 + start: subscription => { sub4 = subscription; }, 354 + next: v => { 355 + earlyValues.push(v); 356 + if (v === 2) sub4.unsubscribe(); 357 + } 358 + }); 359 + test('early unsubscribe stops delivery', earlyValues.length <= 3, true); 360 + test('cleanup called on early unsubscribe', earlyCleanup, true); 361 + 362 + console.log('\nIterator Closing Tests\n'); 363 + 364 + let iteratorClosed = false; 365 + const customIterable = { 366 + [Symbol.iterator]() { 367 + let i = 0; 368 + return { 369 + next() { 370 + if (i < 5) { 371 + const val = i; 372 + i++; 373 + return { value: val, done: false }; 374 + } 375 + return { done: true }; 376 + }, 377 + return() { 378 + iteratorClosed = true; 379 + return { done: true }; 380 + } 381 + }; 382 + } 383 + }; 384 + 385 + const iterValues = []; 386 + let sub5; 387 + new Observable(observer => { 388 + const iter = customIterable[Symbol.iterator](); 389 + let result = iter.next(); 390 + while (!result.done) { 391 + observer.next(result.value); 392 + if (observer.closed) { 393 + if (iter.return) iter.return(); 394 + break; 395 + } 396 + result = iter.next(); 397 + } 398 + if (!observer.closed) observer.complete(); 399 + }).subscribe({ 400 + start: subscription => { sub5 = subscription; }, 401 + next: v => { 402 + iterValues.push(v); 403 + if (v === 2) sub5.unsubscribe(); 404 + } 405 + }); 406 + 407 + test('iterator early unsubscribe stops at value 2', iterValues.length <= 3, true); 408 + test('iterator return() called on early unsubscribe', iteratorClosed, true); 409 + 410 + console.log('\nObserver.closed Property Tests\n'); 411 + 412 + let closedBefore = null; 413 + let closedAfter = null; 414 + new Observable(observer => { 415 + closedBefore = observer.closed; 416 + observer.complete(); 417 + closedAfter = observer.closed; 418 + }).subscribe({}); 419 + test('observer.closed is false before complete', closedBefore, false); 420 + test('observer.closed is true after complete', closedAfter, true); 421 + 422 + summary();
+8 -13
include/internal.h
··· 49 49 int parse_depth; // recursion depth of parser (for stack overflow protection) 50 50 }; 51 51 52 - #define JS_T_OBJ 0 53 - #define JS_T_PROP 1 54 - #define JS_T_STR 2 55 - 56 - #define JS_V_OBJ 0 57 - #define JS_V_PROP 1 58 - #define JS_V_STR 2 59 - #define JS_V_FUNC 7 60 - #define JS_V_ARR 11 61 - #define JS_V_PROMISE 12 62 - #define JS_V_BIGINT 14 63 - #define JS_V_GENERATOR 17 52 + enum { 53 + T_OBJ, T_PROP, T_STR, T_UNDEF, T_NULL, T_NUM, T_BOOL, T_FUNC, 54 + T_CODEREF, T_CFUNC, T_ERR, T_ARR, T_PROMISE, T_TYPEDARRAY, 55 + T_BIGINT, T_PROPREF, T_SYMBOL, T_GENERATOR, T_FFI 56 + }; 64 57 65 58 #define JS_HASH_SIZE 512 66 59 #define JS_MAX_PARSE_DEPTH (1024 * 2) ··· 73 66 #define NANBOX_DATA_MASK 0x0000FFFFFFFFFFFFULL 74 67 75 68 #define TYPE_FLAG(t) (1u << (t)) 69 + #define T_NEEDS_PROTO_FALLBACK (TYPE_FLAG(T_FUNC) | TYPE_FLAG(T_ARR) | TYPE_FLAG(T_PROMISE)) 76 70 #define T_NON_NUMERIC_MASK (TYPE_FLAG(T_STR) | TYPE_FLAG(T_ARR) | TYPE_FLAG(T_FUNC) | TYPE_FLAG(T_CFUNC) | TYPE_FLAG(T_OBJ)) 77 - #define is_non_numeric(v) ((1u << vtype(v)) & T_NON_NUMERIC_MASK) 78 71 79 72 void js_gc_update_roots(GC_UPDATE_ARGS); 80 73 bool js_has_pending_coroutines(void); 74 + 75 + #define is_non_numeric(v) ((1u << vtype(v)) & T_NON_NUMERIC_MASK) 81 76 82 77 #endif
+1 -10
src/ant.c
··· 449 449 [TOK_SEMICOLON] = 1, [TOK_COMMA] = 1, [TOK_EOF] = 1, 450 450 }; 451 451 452 - enum { 453 - T_OBJ, T_PROP, T_STR, T_UNDEF, T_NULL, T_NUM, T_BOOL, T_FUNC, 454 - T_CODEREF, T_CFUNC, T_ERR, T_ARR, T_PROMISE, T_TYPEDARRAY, 455 - T_BIGINT, T_PROPREF, T_SYMBOL, T_GENERATOR, T_FFI 456 - }; 457 - 458 - #define TYPE_MASK(t) (1u << (t)) 459 - #define T_NEEDS_PROTO_FALLBACK (TYPE_MASK(T_FUNC) | TYPE_MASK(T_ARR) | TYPE_MASK(T_PROMISE)) 460 - 461 452 static const char *typestr_raw(uint8_t t) { 462 453 const char *names[] = { 463 454 "object", "prop", "string", "undefined", "null", "number", ··· 5112 5103 uint8_t pt = vtype(proto); 5113 5104 if (pt == T_NULL) break; 5114 5105 if (pt != T_OBJ && pt != T_ARR && pt != T_FUNC) { 5115 - if (TYPE_MASK(t) & T_NEEDS_PROTO_FALLBACK) { 5106 + if (TYPE_FLAG(t) & T_NEEDS_PROTO_FALLBACK) { 5116 5107 cur = get_prototype_for_type(js, t); 5117 5108 t = vtype(cur); 5118 5109 if (t == T_NULL || t == T_UNDEF) break;
+21 -21
src/gc.c
··· 97 97 static jsoff_t gc_esize(jsoff_t w) { 98 98 jsoff_t cleaned = w & ~FLAGMASK; 99 99 switch (cleaned & 3U) { 100 - case JS_T_OBJ: return (jsoff_t)(sizeof(jsoff_t) + sizeof(jsoff_t)); 101 - case JS_T_PROP: return (jsoff_t)(sizeof(jsoff_t) + sizeof(jsoff_t) + sizeof(jsval_t)); 102 - case JS_T_STR: return (jsoff_t)(sizeof(jsoff_t) + ((cleaned >> 2) + 3) / 4 * 4); 103 - default: return (jsoff_t)~0U; 100 + case T_OBJ: return (jsoff_t)(sizeof(jsoff_t) + sizeof(jsoff_t)); 101 + case T_PROP: return (jsoff_t)(sizeof(jsoff_t) + sizeof(jsoff_t) + sizeof(jsval_t)); 102 + case T_STR: return (jsoff_t)(sizeof(jsoff_t) + ((cleaned >> 2) + 3) / 4 * 4); 103 + default: return (jsoff_t)~0U; 104 104 } 105 105 } 106 106 ··· 137 137 if (new_off != (jsoff_t)~0) return new_off; 138 138 139 139 jsoff_t header = gc_loadoff(ctx->js->mem, old_off); 140 - if ((header & 3) != JS_T_STR) return old_off; 140 + if ((header & 3) != T_STR) return old_off; 141 141 142 142 jsoff_t size = gc_esize(header); 143 143 if (size == (jsoff_t)~0) return old_off; ··· 160 160 if (new_off != (jsoff_t)~0) return new_off; 161 161 162 162 jsoff_t header = gc_loadoff(ctx->js->mem, old_off); 163 - if ((header & 3) != JS_T_PROP) { 163 + if ((header & 3) != T_PROP) { 164 164 return old_off; 165 165 } 166 166 ··· 210 210 if (new_off != (jsoff_t)~0) return new_off; 211 211 212 212 jsoff_t header = gc_loadoff(ctx->js->mem, old_off); 213 - if ((header & 3) != JS_T_OBJ) return old_off; 213 + if ((header & 3) != T_OBJ) return old_off; 214 214 215 215 jsoff_t size = gc_esize(header); 216 216 if (size == (jsoff_t)~0) return old_off; ··· 244 244 245 245 jsoff_t header = gc_loadoff(ctx->js->mem, old_off); 246 246 switch (header & 3) { 247 - case JS_T_OBJ: return gc_copy_object(ctx, old_off); 248 - case JS_T_PROP: return gc_copy_prop(ctx, old_off); 249 - case JS_T_STR: return gc_copy_string(ctx, old_off); 250 - default: return old_off; 247 + case T_OBJ: return gc_copy_object(ctx, old_off); 248 + case T_PROP: return gc_copy_prop(ctx, old_off); 249 + case T_STR: return gc_copy_string(ctx, old_off); 250 + default: return old_off; 251 251 } 252 252 } 253 253 ··· 258 258 jsoff_t old_off = (jsoff_t)gc_vdata(val); 259 259 260 260 switch (type) { 261 - case JS_V_OBJ: 262 - case JS_V_FUNC: 263 - case JS_V_ARR: 264 - case JS_V_PROMISE: 265 - case JS_V_GENERATOR: { 261 + case T_OBJ: 262 + case T_FUNC: 263 + case T_ARR: 264 + case T_PROMISE: 265 + case T_GENERATOR: { 266 266 if (old_off >= ctx->js->brk) return val; 267 267 jsoff_t new_off = gc_copy_object(ctx, old_off); 268 268 if (new_off != (jsoff_t)~0) return gc_mkval(type, new_off); 269 269 break; 270 270 } 271 - case JS_V_STR: { 271 + case T_STR: { 272 272 if (old_off >= ctx->js->brk) return val; 273 273 jsoff_t new_off = gc_copy_string(ctx, old_off); 274 274 if (new_off != (jsoff_t)~0) { ··· 276 276 } 277 277 break; 278 278 } 279 - case JS_V_PROP: { 279 + case T_PROP: { 280 280 if (old_off >= ctx->js->brk) return val; 281 281 jsoff_t new_off = gc_copy_prop(ctx, old_off); 282 282 if (new_off != (jsoff_t)~0) { ··· 284 284 } 285 285 break; 286 286 } 287 - case JS_V_BIGINT: { 287 + case T_BIGINT: { 288 288 if (old_off >= ctx->js->brk) return val; 289 289 jsoff_t new_off = fwd_lookup(&ctx->fwd, old_off); 290 290 if (new_off != (jsoff_t)~0) { ··· 357 357 358 358 if (js->brk > 0) { 359 359 jsoff_t header_at_0 = gc_loadoff(js->mem, 0); 360 - if ((header_at_0 & 3) == JS_T_OBJ) gc_copy_object(&ctx, 0); 360 + if ((header_at_0 & 3) == T_OBJ) gc_copy_object(&ctx, 0); 361 361 } 362 362 363 363 jsoff_t scope_off = (jsoff_t)gc_vdata(js->scope); 364 364 if (scope_off < js->brk) { 365 365 jsoff_t new_scope = gc_copy_object(&ctx, scope_off); 366 - js->scope = gc_mkval(JS_V_OBJ, new_scope); 366 + js->scope = gc_mkval(T_OBJ, new_scope); 367 367 } 368 368 369 369 js->this_val = gc_update_val(&ctx, js->this_val);
+65 -85
src/modules/observable.c
··· 2 2 #include <stdio.h> 3 3 #include <string.h> 4 4 5 - #include "ant.h" 5 + #include "internal.h" 6 6 #include "runtime.h" 7 7 8 8 #include "modules/symbol.h" 9 9 #include "modules/observable.h" 10 10 11 + static inline bool is_callable(jsval_t val) { 12 + uint8_t t = vtype(val); 13 + return t == T_FUNC || t == T_CFUNC; 14 + } 15 + 11 16 static bool subscription_closed(struct js *js, jsval_t subscription) { 12 17 jsval_t observer = js_get_slot(js, subscription, SLOT_SUBSCRIPTION_OBSERVER); 13 18 return js_type(observer) == JS_UNDEF; ··· 16 21 static void cleanup_subscription(struct js *js, jsval_t subscription) { 17 22 jsval_t cleanup = js_get_slot(js, subscription, SLOT_SUBSCRIPTION_CLEANUP); 18 23 if (js_type(cleanup) == JS_UNDEF) return; 19 - if (js_type(cleanup) != JS_FUNC) return; 24 + if (!is_callable(cleanup)) return; 20 25 21 26 js_set_slot(js, subscription, SLOT_SUBSCRIPTION_CLEANUP, js_mkundef()); 22 27 jsval_t result = js_call(js, cleanup, NULL, 0); 23 28 24 - if (js_type(result) == JS_ERR) { 25 - fprintf(stderr, "Error in subscription cleanup: %s\n", js_str(js, result)); 26 - } 29 + if (js_type(result) == JS_ERR) fprintf(stderr, "Error in subscription cleanup: %s\n", js_str(js, result)); 27 30 } 28 31 29 32 static jsval_t create_subscription(struct js *js, jsval_t observer) { ··· 38 41 (void)args; (void)nargs; 39 42 jsval_t subscription = js_getthis(js); 40 43 41 - if (js_type(subscription) != JS_OBJ) { 42 - return js_mkerr_typed(js, JS_ERR_TYPE, "Subscription.closed getter called on non-object"); 43 - } 44 - 44 + if (js_type(subscription) != JS_OBJ) return js_mkerr_typed(js, JS_ERR_TYPE, "Subscription.closed getter called on non-object"); 45 45 return subscription_closed(js, subscription) ? js_mktrue() : js_mkfalse(); 46 46 } 47 47 ··· 101 101 if (js_type(observer) != JS_OBJ) return js_mkundef(); 102 102 103 103 jsval_t nextMethod = js_get(js, observer, "next"); 104 - if (js_type(nextMethod) == JS_FUNC) { 104 + if (is_callable(nextMethod)) { 105 105 jsval_t value = (nargs > 0) ? args[0] : js_mkundef(); 106 106 jsval_t call_args[1] = {value}; 107 107 jsval_t result = js_call_with_this(js, nextMethod, observer, call_args, 1); 108 - if (js_type(result) == JS_ERR) { 109 - fprintf(stderr, "Error in observer.next: %s\n", js_str(js, result)); 110 - } 108 + if (js_type(result) == JS_ERR) fprintf(stderr, "Error in observer.next: %s\n", js_str(js, result)); 111 109 } 112 110 113 111 return js_mkundef(); ··· 132 130 133 131 if (js_type(observer) == JS_OBJ) { 134 132 jsval_t errorMethod = js_get(js, observer, "error"); 135 - if (js_type(errorMethod) == JS_FUNC) { 133 + if (is_callable(errorMethod)) { 136 134 jsval_t exception = (nargs > 0) ? args[0] : js_mkundef(); 137 135 jsval_t call_args[1] = {exception}; 138 136 jsval_t result = js_call_with_this(js, errorMethod, observer, call_args, 1); 139 - if (js_type(result) == JS_ERR) { 140 - fprintf(stderr, "Error in observer.error: %s\n", js_str(js, result)); 141 - } 137 + if (js_type(result) == JS_ERR) fprintf(stderr, "Error in observer.error: %s\n", js_str(js, result)); 142 138 } 143 139 } 144 140 ··· 166 162 167 163 if (js_type(observer) == JS_OBJ) { 168 164 jsval_t completeMethod = js_get(js, observer, "complete"); 169 - if (js_type(completeMethod) == JS_FUNC) { 165 + if (is_callable(completeMethod)) { 170 166 jsval_t result = js_call_with_this(js, completeMethod, observer, NULL, 0); 171 - if (js_type(result) == JS_ERR) { 172 - fprintf(stderr, "Error in observer.complete: %s\n", js_str(js, result)); 173 - } 167 + if (js_type(result) == JS_ERR) fprintf(stderr, "Error in observer.complete: %s\n", js_str(js, result)); 174 168 } 175 169 } 176 170 ··· 180 174 181 175 static jsval_t create_subscription_observer(struct js *js, jsval_t subscription) { 182 176 jsval_t subobs = js_mkobj(js); 177 + 183 178 js_set_slot(js, subobs, SLOT_DATA, subscription); 184 179 js_set(js, subobs, "next", js_mkfun(js_subobs_next)); 185 180 js_set(js, subobs, "error", js_mkfun(js_subobs_error)); 186 181 js_set(js, subobs, "complete", js_mkfun(js_subobs_complete)); 187 182 js_set(js, subobs, get_toStringTag_sym_key(), js_mkstr(js, "SubscriptionObserver", 20)); 183 + 188 184 jsval_t closed_getter = js_mkfun(js_subobs_get_closed); 189 185 js_set_getter_desc(js, subobs, "closed", 6, closed_getter, JS_DESC_E | JS_DESC_C); 186 + 190 187 return subobs; 191 188 } 192 189 ··· 198 195 if (js_type(subscription) != JS_OBJ) return js_mkundef(); 199 196 200 197 jsval_t unsubscribe = js_get(js, subscription, "unsubscribe"); 201 - if (js_type(unsubscribe) == JS_FUNC) { 198 + if (is_callable(unsubscribe)) { 202 199 return js_call_with_this(js, unsubscribe, subscription, NULL, 0); 203 200 } 204 201 ··· 211 208 212 209 if (js_type(subscriberResult) == JS_ERR) return subscriberResult; 213 210 if (js_type(subscriberResult) == JS_NULL || js_type(subscriberResult) == JS_UNDEF) return js_mkundef(); 214 - if (js_type(subscriberResult) == JS_FUNC) return subscriberResult; 211 + if (is_callable(subscriberResult)) return subscriberResult; 215 212 216 213 if (js_type(subscriberResult) == JS_OBJ) { 217 214 jsval_t result = js_get(js, subscriberResult, "unsubscribe"); ··· 236 233 } 237 234 238 235 jsval_t subscriber = js_get_slot(js, O, SLOT_OBSERVABLE_SUBSCRIBER); 239 - if (js_type(subscriber) != JS_FUNC) { 236 + if (!is_callable(subscriber)) { 240 237 return js_mkerr_typed(js, JS_ERR_TYPE, "Observable has no [[Subscriber]] internal slot"); 241 238 } 242 239 243 240 jsval_t observer; 244 241 245 - if (nargs > 0 && js_type(args[0]) == JS_FUNC) { 242 + if (nargs > 0 && is_callable(args[0])) { 246 243 jsval_t nextCallback = args[0]; 247 244 jsval_t errorCallback = (nargs > 1) ? args[1] : js_mkundef(); 248 245 jsval_t completeCallback = (nargs > 2) ? args[2] : js_mkundef(); ··· 253 250 js_set(js, observer, "complete", completeCallback); 254 251 } else if (nargs > 0 && js_type(args[0]) == JS_OBJ) { 255 252 observer = args[0]; 256 - } else { 257 - observer = js_mkobj(js); 258 - } 253 + } else observer = js_mkobj(js); 259 254 260 255 jsval_t subscription = create_subscription(js, observer); 261 256 setup_subscription_methods(js, subscription); 262 257 263 258 jsval_t start = js_get(js, observer, "start"); 264 - if (js_type(start) == JS_FUNC) { 259 + if (is_callable(start)) { 265 260 jsval_t start_args[1] = {subscription}; 266 261 jsval_t result = js_call_with_this(js, start, observer, start_args, 1); 267 262 if (js_type(result) == JS_ERR) { ··· 274 269 jsval_t subscriberResult = execute_subscriber(js, subscriber, subscriptionObserver); 275 270 276 271 if (js_type(subscriberResult) == JS_ERR) { 277 - jsval_t error_args[1] = {subscriberResult}; 272 + jsval_t thrown_error = js->thrown_value; 273 + js->thrown_value = js_mkundef(); 274 + js->flags &= ~F_THROW; 275 + 276 + jsval_t error_args[1] = {thrown_error}; 278 277 jsval_t error_method = js_get(js, subscriptionObserver, "error"); 279 - if (js_type(error_method) == JS_FUNC) { 280 - js_call_with_this(js, error_method, subscriptionObserver, error_args, 1); 281 - } 282 - } else { 283 - js_set_slot(js, subscription, SLOT_SUBSCRIPTION_CLEANUP, subscriberResult); 284 - } 278 + if (is_callable(error_method)) js_call_with_this(js, error_method, subscriptionObserver, error_args, 1); 279 + } else js_set_slot(js, subscription, SLOT_SUBSCRIPTION_CLEANUP, subscriberResult); 285 280 286 - if (subscription_closed(js, subscription)) { 287 - cleanup_subscription(js, subscription); 288 - } 281 + if (subscription_closed(js, subscription)) cleanup_subscription(js, subscription); 289 282 290 283 return subscription; 291 284 } ··· 301 294 } 302 295 303 296 jsval_t subscriber = args[0]; 304 - if (js_type(subscriber) != JS_FUNC) { 297 + if (!is_callable(subscriber)) { 305 298 return js_mkerr_typed(js, JS_ERR_TYPE, "Observable subscriber must be a function"); 306 299 } 307 300 ··· 332 325 jsval_t value = js_get(js, items, key); 333 326 334 327 jsval_t next = js_get(js, observer, "next"); 335 - if (js_type(next) == JS_FUNC) { 328 + if (is_callable(next)) { 336 329 jsval_t next_args[1] = {value}; 337 330 js_call_with_this(js, next, observer, next_args, 1); 338 331 } 339 332 340 - if (js_type(subscription) == JS_OBJ && subscription_closed(js, subscription)) { 341 - return js_mkundef(); 342 - } 333 + if (js_type(subscription) == JS_OBJ && subscription_closed(js, subscription)) return js_mkundef(); 343 334 } 344 335 345 336 jsval_t complete = js_get(js, observer, "complete"); 346 - if (js_type(complete) == JS_FUNC) { 347 - js_call_with_this(js, complete, observer, NULL, 0); 348 - } 337 + if (is_callable(complete)) js_call_with_this(js, complete, observer, NULL, 0); 349 338 350 339 return js_mkundef(); 351 340 } 352 341 353 342 static jsval_t js_observable_of(struct js *js, jsval_t *args, int nargs) { 354 343 jsval_t items = js_mkarr(js); 355 - for (int i = 0; i < nargs; i++) { 356 - js_arr_push(js, items, args[i]); 357 - } 344 + for (int i = 0; i < nargs; i++) js_arr_push(js, items, args[i]); 358 345 359 - jsval_t subscriber = js_mkobj(js); 360 - js_set_slot(js, subscriber, SLOT_DATA, items); 361 - js_set_slot(js, subscriber, SLOT_CFUNC, js_mkfun(js_of_subscriber)); 362 - jsval_t subscriber_func = js_obj_to_func(subscriber); 346 + jsval_t subscriber_func = js_heavy_mkfun(js, js_of_subscriber, items); 347 + jsval_t ctor_args[1] = {subscriber_func}; 363 348 364 - jsval_t ctor_args[1] = {subscriber_func}; 365 349 return js_observable_constructor(js, ctor_args, 1); 366 350 } 367 351 368 352 static jsval_t js_from_delegating(struct js *js, jsval_t *args, int nargs) { 369 353 jsval_t F = js_getcurrentfunc(js); 370 - jsval_t observable = js_get_slot(js, F, SLOT_DATA); 371 354 355 + jsval_t observable = js_get_slot(js, F, SLOT_DATA); 372 356 if (js_type(observable) != JS_OBJ) return js_mkundef(); 373 357 374 358 jsval_t subscribe = js_get(js, observable, "subscribe"); 375 - if (js_type(subscribe) == JS_FUNC) { 359 + if (is_callable(subscribe)) { 376 360 return js_call_with_this(js, subscribe, observable, args, nargs); 377 361 } 378 362 ··· 391 375 jsval_t observer = args[0]; 392 376 jsval_t subscription = js_get_slot(js, observer, SLOT_DATA); 393 377 394 - if (js_type(iteratorMethod) != JS_FUNC) { 378 + if (!is_callable(iteratorMethod)) { 395 379 return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not iterable"); 396 380 } 397 381 ··· 401 385 } 402 386 403 387 jsval_t nextMethod = js_get(js, iterator, "next"); 404 - if (js_type(nextMethod) != JS_FUNC) { 388 + if (!is_callable(nextMethod)) { 405 389 return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator must have a next method"); 406 390 } 407 391 ··· 412 396 jsval_t done = js_get(js, next, "done"); 413 397 if (js_truthy(js, done)) { 414 398 jsval_t complete = js_get(js, observer, "complete"); 415 - if (js_type(complete) == JS_FUNC) { 416 - js_call_with_this(js, complete, observer, NULL, 0); 417 - } 399 + if (is_callable(complete)) js_call_with_this(js, complete, observer, NULL, 0); 418 400 return js_mkundef(); 419 401 } 420 402 421 403 jsval_t nextValue = js_get(js, next, "value"); 422 404 jsval_t obs_next = js_get(js, observer, "next"); 423 - if (js_type(obs_next) == JS_FUNC) { 405 + if (is_callable(obs_next)) { 424 406 jsval_t next_args[1] = {nextValue}; 425 407 js_call_with_this(js, obs_next, observer, next_args, 1); 426 408 } 427 409 428 410 if (js_type(subscription) == JS_OBJ && subscription_closed(js, subscription)) { 429 411 jsval_t returnMethod = js_get(js, iterator, "return"); 430 - if (js_type(returnMethod) == JS_FUNC) { 431 - js_call_with_this(js, returnMethod, iterator, NULL, 0); 432 - } 412 + if (is_callable(returnMethod)) js_call_with_this(js, returnMethod, iterator, NULL, 0); 433 413 return js_mkundef(); 434 414 } 435 415 } 436 416 } 437 417 438 418 static jsval_t js_observable_from(struct js *js, jsval_t *args, int nargs) { 439 - if (nargs < 1) { 440 - return js_mkerr_typed(js, JS_ERR_TYPE, "Observable.from requires an argument"); 419 + if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Observable.from requires an argument"); 420 + jsval_t x = args[0]; 421 + 422 + if (js_type(x) == JS_NULL || js_type(x) == JS_UNDEF) { 423 + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot convert null or undefined to observable"); 441 424 } 442 425 443 - jsval_t x = args[0]; 444 426 jsval_t observableMethod = js_get(js, x, get_observable_sym_key()); 445 427 446 - if (js_type(observableMethod) == JS_FUNC) { 428 + if (is_callable(observableMethod)) { 447 429 jsval_t observable = js_call_with_this(js, observableMethod, x, NULL, 0); 448 430 449 431 if (js_type(observable) != JS_OBJ) { 450 432 return js_mkerr_typed(js, JS_ERR_TYPE, "@@observable must return an object"); 451 433 } 452 434 453 - jsval_t constructor = js_get(js, observable, "constructor"); 454 - jsval_t C = js_get(js, js_glob(js), "Observable"); 435 + jsval_t existing_subscriber = js_get_slot(js, observable, SLOT_OBSERVABLE_SUBSCRIBER); 436 + if (is_callable(existing_subscriber)) return observable; 455 437 456 - if (constructor == C) return observable; 438 + jsval_t subscriber_func = js_heavy_mkfun(js, js_from_delegating, observable); 439 + jsval_t ctor_args[1] = {subscriber_func}; 457 440 458 - jsval_t subscriber = js_mkobj(js); 459 - js_set_slot(js, subscriber, SLOT_DATA, observable); 460 - js_set_slot(js, subscriber, SLOT_CFUNC, js_mkfun(js_from_delegating)); 461 - jsval_t subscriber_func = js_obj_to_func(subscriber); 462 - 463 - jsval_t ctor_args[1] = {subscriber_func}; 464 441 return js_observable_constructor(js, ctor_args, 1); 465 442 } 466 443 467 444 jsval_t iteratorMethod = js_get(js, x, get_iterator_sym_key()); 468 445 469 - if (js_type(iteratorMethod) != JS_FUNC) { 446 + if (!is_callable(iteratorMethod) && vtype(x) == T_ARR) { 447 + jsval_t array_ctor = js_get(js, js_glob(js), "Array"); 448 + jsval_t array_proto = js_get(js, array_ctor, "prototype"); 449 + iteratorMethod = js_get(js, array_proto, get_iterator_sym_key()); 450 + } 451 + 452 + if (!is_callable(iteratorMethod)) { 470 453 return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not observable or iterable"); 471 454 } 472 455 ··· 474 457 js_set(js, data, "iterable", x); 475 458 js_set(js, data, "iteratorMethod", iteratorMethod); 476 459 477 - jsval_t subscriber = js_mkobj(js); 478 - js_set_slot(js, subscriber, SLOT_DATA, data); 479 - js_set_slot(js, subscriber, SLOT_CFUNC, js_mkfun(js_from_iteration)); 480 - jsval_t subscriber_func = js_obj_to_func(subscriber); 460 + jsval_t subscriber_func = js_heavy_mkfun(js, js_from_iteration, data); 461 + jsval_t ctor_args[1] = {subscriber_func}; 481 462 482 - jsval_t ctor_args[1] = {subscriber_func}; 483 463 return js_observable_constructor(js, ctor_args, 1); 484 464 } 485 465