From 7d81c837434e704d37d0e349f2f893b2a86c2f74 Mon Sep 17 00:00:00 2001 From: Michal Mocny Date: Wed, 10 Apr 2024 10:34:22 -0400 Subject: [PATCH] Adding more demos --- .../index.html | 1 + .../example-extension-marks-measures/index.js | 70 +++++++ sandbox/force-mutation-observers/index.html | 1 + sandbox/force-mutation-observers/index.js | 77 +++++++ .../index.html | 34 +++ .../apps/test/debugOperators.js | 6 + sandbox/web-mightals.js/apps/test/index.js | 194 +++++++++++------- .../web-mightals.js/apps/test/operators.js | 0 sandbox/web-mightals.js/apps/test/pipeMap.js | 52 +++++ sandbox/web-mightals.js/package.json | 2 +- sandbox/web-mightals.js/pnpm-lock.yaml | 14 +- 11 files changed, 365 insertions(+), 86 deletions(-) create mode 100644 sandbox/example-extension-marks-measures/index.html create mode 100644 sandbox/example-extension-marks-measures/index.js create mode 100644 sandbox/force-mutation-observers/index.html create mode 100644 sandbox/force-mutation-observers/index.js create mode 100644 sandbox/presentation-delay-marks-examples/index.html create mode 100644 sandbox/web-mightals.js/apps/test/debugOperators.js create mode 100644 sandbox/web-mightals.js/apps/test/operators.js create mode 100644 sandbox/web-mightals.js/apps/test/pipeMap.js diff --git a/sandbox/example-extension-marks-measures/index.html b/sandbox/example-extension-marks-measures/index.html new file mode 100644 index 0000000..9552630 --- /dev/null +++ b/sandbox/example-extension-marks-measures/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sandbox/example-extension-marks-measures/index.js b/sandbox/example-extension-marks-measures/index.js new file mode 100644 index 0000000..083fd54 --- /dev/null +++ b/sandbox/example-extension-marks-measures/index.js @@ -0,0 +1,70 @@ +const then = performance.now(); + +// These colors are a guess based on a few examples and AI generated +const COLOR = [ + "primary", + "primary-light", + "secondary", + "secondary-light", + "tertiary", + "tertiary-light", + "success", + "error", + "info", + "warning", +]; + +function addCustomMeasure(name) { + performance.measure(name, { + start: then, + end: performance.now(), + detail: { + devtools: { + metadata: { + extensionName: "Michal's Extension", + dataType: "custom-measure", + }, + color: "tertiary-light", + track: "An Extension Track", + hintText: "This is a rendering task", + detailsPairs: [ + ["Description", "This is a child task"], + ["Tip", "Do something about it"], + ], + }, + }, + }); +} + +function addCustomMark(name) { + performance.mark("Custom mark", { + startTime: performance.now(), + detail: { + devtools: { + metadata: { + extensionName: "Michal's Extension", + dataType: "custom-mark", + }, + color: "primary", + detailsPairs: [ + [ + "Description", + "This marks the start of a task", + ], + ], + hintText: "A mark", + }, + }, + }); +} + + +setTimeout(() => { + addCustomMark("myMark"); +}, 200); + +setTimeout(() => { + addCustomMeasure("myMeasure"); +}, 100); + +console.log("loaded"); \ No newline at end of file diff --git a/sandbox/force-mutation-observers/index.html b/sandbox/force-mutation-observers/index.html new file mode 100644 index 0000000..9552630 --- /dev/null +++ b/sandbox/force-mutation-observers/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sandbox/force-mutation-observers/index.js b/sandbox/force-mutation-observers/index.js new file mode 100644 index 0000000..a94d04b --- /dev/null +++ b/sandbox/force-mutation-observers/index.js @@ -0,0 +1,77 @@ +const BLOCK = 50; + +function block(ms) { + const target = performance.now() + ms; + while (target > performance.now()); +} + +async function triggerSyncFetch(cb) { + const dataURI = "data:application/json,{}"; + const response = await fetch(dataURI); + const result = await response.json(); + cb(result); +} + +async function prefetch() { + const response = await fetch('https://pokeapi.co/api/v2/pokemon/ditto'); + const data = await response.json(); + return data; +} + + +function triggerMutationObserver(cb) { + const targetNode = document.createElement('div'); + const observer = new MutationObserver(cb); + // for (const mutation of mutationList) { + // console.log('Mutation detected:', mutation.type); + // } + observer.observe(targetNode, { childList: true }); + + const dummyNode = document.createElement('span'); + targetNode.appendChild(dummyNode); + // targetNode.removeChild(dummyNode); +} + +function a() { + block(BLOCK); + b(); +} +function b() { + block(BLOCK); + c(); +} +function c() { + block(BLOCK); + triggerMutationObserver(function afterMutation() { block(BLOCK); }); +} + +async function dontAwaitFirstDoAwaitBetween() { a(); await 0; block(BLOCK); a(); } +async function doAwaitFirstDoAwaitBetween() { a(); await 0; block(BLOCK); a(); } +async function dontAwaitFirstDontAwaitBetween() { a(); block(BLOCK); a(); } +async function doAwaitFirstDontAwaitBetween() { await 0; a(); block(BLOCK); a(); } + +scheduler.postTask(dontAwaitFirstDoAwaitBetween); +scheduler.postTask(doAwaitFirstDoAwaitBetween); +scheduler.postTask(dontAwaitFirstDontAwaitBetween); +scheduler.postTask(doAwaitFirstDontAwaitBetween); + +async function testFetch() { + block(BLOCK); + triggerSyncFetch(function afterFetch() { block(BLOCK); }); +} + +scheduler.postTask(testFetch); + + +async function testPrefetch() { + await prefetch(); + block(BLOCK); + + scheduler.postTask(async function afterPrefetch() { + block(BLOCK); + await prefetch(); + block(BLOCK); + }); +} + +scheduler.postTask(testPrefetch); \ No newline at end of file diff --git a/sandbox/presentation-delay-marks-examples/index.html b/sandbox/presentation-delay-marks-examples/index.html new file mode 100644 index 0000000..afe7601 --- /dev/null +++ b/sandbox/presentation-delay-marks-examples/index.html @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/sandbox/web-mightals.js/apps/test/debugOperators.js b/sandbox/web-mightals.js/apps/test/debugOperators.js new file mode 100644 index 0000000..b56dd7d --- /dev/null +++ b/sandbox/web-mightals.js/apps/test/debugOperators.js @@ -0,0 +1,6 @@ +function debugOperators(operatorFunction, name) { + return function (source) { + console.log(`Applying operator: ${name}`); + return operatorFunction(source); + }; +} diff --git a/sandbox/web-mightals.js/apps/test/index.js b/sandbox/web-mightals.js/apps/test/index.js index 7748eea..04d70c4 100644 --- a/sandbox/web-mightals.js/apps/test/index.js +++ b/sandbox/web-mightals.js/apps/test/index.js @@ -1,6 +1,7 @@ import { concatMap, switchMap, + mergeMap, delay, filter, first, @@ -23,6 +24,9 @@ import { asyncScheduler, Observable, operate, + takeWhile, + expand, + rx, } from "rxjs"; import pageSlicer$ from "../../lib/pageSlicer"; import webMightals$ from "../../lib/webMightals"; @@ -36,15 +40,15 @@ function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -const clicks$ = fromEvent(myButton, "click").pipe(share()); +const clicks$ = fromEvent(myButton, "click"); // Add Layout Shifts -clicks$.pipe(delay(1000)).subscribe((event) => { +rx(clicks$, delay(1000)).subscribe((event) => { const el = event.target; el.style.top = `${el.offsetTop + 100}px`; }); // Add Long Interaction -clicks$.subscribe(() => { +clicks$.subscribe((event) => { block(Math.random() * 400); }); @@ -52,19 +56,35 @@ pageSlicer$.subscribe((value) => { console.log("PageSlice:", value); }); -webMightals$.subscribe({ - next: (value) => { - console.groupCollapsed("webMightals"); - for (let [k, v] of Object.entries(value)) { - console.log(k, +v.score.toFixed(5), { entries: v.entries }); - } - console.groupEnd(); - }, - complete: () => { }, +webMightals$.subscribe((value) => { + console.groupCollapsed("webMightals"); + for (let [k, v] of Object.entries(value)) { + console.log(k, +v.score.toFixed(5), { entries: v.entries }); + } + console.groupEnd(); }); + + // *************************** +// This is a *very* imperative implementation, as baseline +function primes_sol0() { + const filterFns = []; + return rx( + range(2, Infinity), + mergeMap((n) => { + const is_prime = filterFns.every((fn) => fn(n)); + if (is_prime) { + filterFns.push((n) => n % n !== 0); + return from(n); + } + return EMPTY; + }) + ); +} + +// This is also very imperative, but uses some operators function primes_sol1() { const filterFns = []; return range(2, Infinity).pipe( @@ -73,6 +93,30 @@ function primes_sol1() { ); } +function withState(fn, state) { + return source$ => rx( + source$, + map(value => { + [state, value] = fn(state, value); + return value; + }) + ); +} + +function primes_sol2b() { + return rx( + range(2, Infinity), + withState((filterFns, pn) => { + const is_prime = filterFns.every((fn) => fn(n)); + if (is_prime) { + filterFns.push((n) => n % n !== 0); + return n; + } + return EMPTY; + }, []) + ); +} + function primes_sol2() { return range(2, Infinity).pipe( switchScan( @@ -96,6 +140,8 @@ function primes_sol2() { ); } + + function primes_sol3() { return range(2, Infinity) .pipe( @@ -105,7 +151,7 @@ function primes_sol3() { first(), concatMap((pn) => source$.pipe( - tap(n => console.log('pipeMap', 'n =', n, 'pn =', pn)), + // tap(n => console.log('pipeMap', 'n =', n, 'pn =', pn)), filter((n) => n % pn !== 0), filterPrimes, startWith(pn), @@ -116,6 +162,13 @@ function primes_sol3() { ); } +function debug(operatorFunction, name) { + return function (source) { + console.log(`Applying operator: ${name}`); + return operatorFunction(source); + }; +} + // pipeMap is a RxJS operator that is meant to help chain a series of RxJS operators together, based on the input stream. // Similar to how mergeMap() will add new observable streams to the output stream... pipeMap will add new operators to the current streams' pipe. @@ -125,79 +178,70 @@ function primes_sol3() { // - This callback will return a new RxJS operator, every time it is called with a new value from the input stream. // - That operator which is returned gets piped() on to the end of the current stream. // - The value that was used to produce this new operator should also be produced by the output stream. -// -// For example, if an input stream emits the values: 1, 2, 3, 4, 5 -// pipeMap() should call the callback 5 times, each time receiving a new a new operator, -// and the final output stream should have 5 operators attached to it. // Called one per Observable creation -// function pipeMap(cb) { -// // Started once per Observable subscription, which includes recursively calling itself -// return source$ => source$.pipe( -// first(), // Use this to stop the stream... we want to switchMap exactly once in this stream. -// switchMap(value => source$.pipe( // TODO: this re-subscribes to the source$ stream, which is not ideal if that stream is COLD. Besides forcing HOT, is there some way to avoid complete+resubscribe? How does switchMap() do it internally? -// // tap(value2 => console.log('pipeMap', 'pn=', value, 'n=', value2)), -// cb(value), -// pipeMap(cb), -// startWith(value), -// )), -// ); -// } - -// Called one per Observable creation -function pipeMap(cb) { - // Started once per Observable subscription, which includes recursively calling itself - return source$ => { +function pipeMap(fn) { + return source$ => new Observable(destination => { + function recurse(stream$) { + stream$.subscribe( + operate({ + destination, + next(value) { + // TODO: needed? + destination.next(value); + + const operator = fn(value); + stream$.pipe( + operator, + recurse + ); + } + }) + ) + } + recurse(source$); + }); +} - let piped$ = source$ - .pipe( - share(), +// Uses switchMap to switch this source stream to a new stream, which merely adds a new pipe() operator to the current stream +function pipeMap2(fn) { + return source$ => source$.pipe( + switchMap(value => { + const operator = fn(value); + return source$.pipe( + operator, + pipeMap2(fn), + startWith(value) ); - - const results$ = new Observable(destination => { - function recurse() { - piped$.subscribe( - operate({ - destination, - next(value) { - // TODO: alternative: .pipe(first()) ? - piped$.unsubscribe(); - - // TODO: needed? - destination.next(value); - - const operator = cb(value); - piped$ = piped$.pipe(operator); - - // recurse(); - } - }) - ) - } - recurse(); - }); - - return results$; - }; + }), + ); } function primes_sol4() { - return range(2, Infinity) - .pipe( - share(), // TODO: Any alternatives to this? Idea: maybe instead of switchMap we need a new Observer() which keeps listening to the source stream, but replacing the wrapped Observable??. - pipeMap( - pn => filter((n) => n % pn !== 0) - ), - ); + return rx( + range(2, Infinity), + pipeMap(pn => filter((n) => n % pn !== 0)), + ); } -// Ideas: -// - Play with Expand() which is a "recursive" version of switchMap()? -// - What about just using higher-order observerables directly? Map input to a stream() +function primes_sol5() { + return rx( + range(2, Infinity), + pipeMap2(pn => filter((n) => n % pn !== 0)), + ); +} -primes_sol4() - .pipe( + +[primes_sol0, primes_sol1, primes_sol2, primes_sol3, primes_sol4, primes_sol5].forEach((fn) => { + const primes$ = fn(); + + console.time(fn.name); + primes$.pipe( take(10), ).subscribe( console.log ); + console.timeEnd(fn.name); +}) + + diff --git a/sandbox/web-mightals.js/apps/test/operators.js b/sandbox/web-mightals.js/apps/test/operators.js new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/web-mightals.js/apps/test/pipeMap.js b/sandbox/web-mightals.js/apps/test/pipeMap.js new file mode 100644 index 0000000..1308e87 --- /dev/null +++ b/sandbox/web-mightals.js/apps/test/pipeMap.js @@ -0,0 +1,52 @@ +import { + Observable, + operate +} from "rxjs"; + +// pipeMap is a RxJS operator that is meant to help chain a series of RxJS operators together, based on the input stream. +// Similar to how mergeMap() will add new observable streams to the output stream... pipeMap will add new operators to the current streams' pipe. +// +// pipeMap() is a function that: +// - Accepts a Callback (will be called with each value in the stream, similar to what RxJS filter() or map() operator might accept) +// - This callback will return a new RxJS operator, every time it is called with a new value from the input stream. +// - That operator which is returned gets piped() on to the end of the current stream. +// - The value that was used to produce this new operator should also be produced by the output stream. +// Called one per Observable creation +export function pipeMap(fn) { + return source$ => new Observable(destination => { + function recurse(stream$) { + stream$.subscribe( + operate({ + destination, + next(value) { + destination.next(value); + + const operator = fn(value); + stream$.pipe( + operator, + recurse + ); + } + }) + ); + } + recurse(source$); + }); +} + + +import { switchMap, startWith } from "rxjs"; + +// Uses switchMap to switch this source stream to a new stream, which merely adds a new pipe() operator to the current stream +export function pipeMap2(fn) { + return source$ => source$.pipe( + switchMap(value => { + const operator = fn(value); + return source$.pipe( + operator, + pipeMap2(fn), + startWith(value) + ); + }) + ); +} diff --git a/sandbox/web-mightals.js/package.json b/sandbox/web-mightals.js/package.json index 561a966..ad88646 100644 --- a/sandbox/web-mightals.js/package.json +++ b/sandbox/web-mightals.js/package.json @@ -11,6 +11,6 @@ "author": "", "license": "ISC", "dependencies": { - "rxjs": "^7.8.1" + "rxjs": "8.0.0-alpha.14" } } diff --git a/sandbox/web-mightals.js/pnpm-lock.yaml b/sandbox/web-mightals.js/pnpm-lock.yaml index 153cdc8..f83949a 100644 --- a/sandbox/web-mightals.js/pnpm-lock.yaml +++ b/sandbox/web-mightals.js/pnpm-lock.yaml @@ -6,17 +6,11 @@ settings: dependencies: rxjs: - specifier: ^7.8.1 - version: 7.8.1 + specifier: 8.0.0-alpha.14 + version: 8.0.0-alpha.14 packages: - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - dependencies: - tslib: 2.6.2 - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /rxjs@8.0.0-alpha.14: + resolution: {integrity: sha512-oRCzFwbAMbo0+dVUeGUCCEf339mW7CESFvVDG3RTncD6yMtV2XoubFMpKEhBd2a9d04EpHl8QSz84/V/agG6bw==} dev: false