Skip to content

Commit 84b9e51

Browse files
committed
channel fromPromise with (promise, transform { startWith })
1 parent 72e91d5 commit 84b9e51

File tree

7 files changed

+1755
-0
lines changed

7 files changed

+1755
-0
lines changed

packages/channels/channels.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { subject } from './generators.js';
2+
import 'test-utils/with-resolvers-polyfill';
3+
4+
function throwMoreArgumentsNeeded() {
5+
throw new TypeError(`\
6+
"Channel.from(promise)" requires more arguments, \
7+
expected a transform option or channel options. \
8+
Use "promise.then(transform)" for creating a channel from a single promise`);
9+
}
10+
11+
function throwAsyncSourceTypeError(type) {
12+
throw new TypeError(`\
13+
Unexpected async source type "${type}". Expected an asynchronous data provider type, or \
14+
a function that returns an asynchronous data provider type."`);
15+
}
16+
17+
export class Channel {
18+
static from = from;
19+
}
20+
21+
export function from(asyncSource, transform, options) {
22+
if(!options && typeof transform === 'object') {
23+
options = transform;
24+
transform = null;
25+
}
26+
27+
const type = typeof asyncSource;
28+
29+
switch(true) {
30+
case asyncSource instanceof Promise:
31+
return fromPromise(asyncSource, transform, options);
32+
default:
33+
throwAsyncSourceTypeError(type);
34+
35+
}
36+
}
37+
38+
function fromPromise(promise, transform, options) {
39+
const startWith = options?.startWith;
40+
if(startWith) {
41+
return [fromPromiseStartWith(promise, transform, startWith)];
42+
}
43+
return [transform ? promise.then(transform) : promise];
44+
}
45+
46+
async function* fromPromiseStartWith(promise, transform, startWith) {
47+
yield startWith;
48+
yield transform ? promise.then(transform) : promise;
49+
}

packages/channels/channels.test.js

Whitespace-only changes.

packages/channels/channels.test.jsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { test } from 'vitest';
2+
import { findByText } from '@testing-library/dom';
3+
import { Channel } from './channels.js';
4+
import { beforeEach } from 'vitest';
5+
6+
beforeEach(async context => {
7+
document.body.innerHTML = '';
8+
context.fixture = document.body;
9+
context.find = filter => findByText(context.fixture, filter, { exact: false });
10+
});
11+
12+
const Cat = ({ name }) => <p>{name}</p>;
13+
const Loading = () => <p>loading...</p>;
14+
15+
test('Channel.from(promise, transform)', async ({ fixture, find, expect }) => {
16+
const promise = Promise.resolve({ name: 'felix' });
17+
const [LayoutChannel] = Channel.from(promise, Cat);
18+
fixture.append(<LayoutChannel />);
19+
const dom = await find('felix');
20+
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>felix<!--1--></p>"`);
21+
});
22+
23+
test('Channel.from(promise, transform, { startWith })', async ({ fixture, find, expect }) => {
24+
const { promise, resolve } = Promise.withResolvers();
25+
const [LayoutChannel] = Channel.from(promise, Cat, {
26+
startWith: <Loading />
27+
});
28+
fixture.append(<>{LayoutChannel}</>);
29+
let dom = null;
30+
31+
dom = await find('loading...');
32+
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>loading...</p>"`);
33+
34+
// we delay promise resolution and trigger here to not miss the
35+
// intermediate "loading...", async testing-library "find"
36+
// pushes it out too far and it picks up the the "felix".
37+
resolve({ name: 'felix ' });
38+
39+
dom = await find('felix');
40+
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>felix <!--1--></p>"`);
41+
});

packages/channels/generators.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
export function subject(transform, options) {
3+
if(!options && typeof transform === 'object') {
4+
options = transform;
5+
transform = null;
6+
}
7+
8+
let initialValue, startWith;
9+
if(options) {
10+
initialValue = options.initialValue;
11+
startWith = options.startWith;
12+
if(initialValue !== undefined) {
13+
if(startWith !== undefined) {
14+
throw new Error('Cannot specify both initialValue and startWith option');
15+
}
16+
if(!transform) {
17+
throw new Error('Cannot specify initialValue without a transform function');
18+
}
19+
}
20+
}
21+
22+
const relay = { resolve: null };
23+
24+
let unsentEarlyDispatch = null;
25+
26+
function dispatch(payload) {
27+
if(transform) payload = transform(payload);
28+
if(relay.resolve) relay.resolve(payload);
29+
else {
30+
// eslint-disable-next-line eqeqeq
31+
if(payload != null) unsentEarlyDispatch = payload;
32+
}
33+
}
34+
35+
async function* generator() {
36+
let promise = null;
37+
let resolve = null;
38+
39+
if(initialValue !== undefined) {
40+
yield transform(initialValue);
41+
}
42+
if(startWith !== undefined) {
43+
yield startWith;
44+
}
45+
// this handles dispatch that happens between
46+
// initial/start yields and main loop:
47+
// eslint-disable-next-line eqeqeq
48+
while(unsentEarlyDispatch != null) {
49+
const toYield = unsentEarlyDispatch;
50+
unsentEarlyDispatch = null;
51+
yield toYield;
52+
}
53+
54+
while(true) {
55+
({ promise, resolve } = Promise.withResolvers());
56+
relay.resolve = resolve;
57+
yield await promise;
58+
}
59+
}
60+
61+
const asyncIterator = generator();
62+
return [asyncIterator, dispatch];
63+
}
64+
65+
66+
export function multicast(iterator) {
67+
return new Multicast(iterator);
68+
}
69+
70+
class Multicast {
71+
consumers = [];
72+
constructor(subject) {
73+
this.subject = subject;
74+
this.#start();
75+
}
76+
77+
async #start() {
78+
for await(let value of this.subject) {
79+
for(let consumer of this.consumers) {
80+
consumer(value);
81+
}
82+
}
83+
}
84+
85+
subscriber(transform, options) {
86+
const [dispatch, iterator] = subject(transform, options);
87+
this.consumers.push(dispatch);
88+
return iterator;
89+
}
90+
}

packages/channels/generators.test.jsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { beforeEach, test } from 'vitest';
2+
import { subject } from './generators.js';
3+
import 'test-utils/with-resolvers-polyfill';
4+
import { screen } from '@testing-library/dom';
5+
6+
beforeEach(async context => {
7+
document.body.innerHTML = '';
8+
context.fixture = document.body;
9+
});
10+
11+
test.skip('subject', async ({ fixture, expect }) => {
12+
13+
const [asyncIterator, dispatch] = subject();
14+
const dom = <div>{asyncIterator}!</div>;
15+
fixture.append(dom);
16+
17+
await screen.findByText('!', { exact: false });
18+
19+
expect(dom).toMatchInlineSnapshot(`
20+
<div>
21+
<!--0-->
22+
!
23+
</div>
24+
`);
25+
26+
dispatch('Hello World');
27+
await screen.findByText('Hello World', { exact: false });
28+
29+
expect(dom).toMatchInlineSnapshot(`
30+
<div>
31+
Hello World
32+
<!--1-->
33+
!
34+
</div>
35+
`);
36+
});
37+
38+
// test.skip('multicast', async ({ expect }) => {
39+
// const [asyncIterator, dispatch] = subject({ startWith: 'hello' });
40+
41+
// const mc = multicast(asyncIterator);
42+
// const s1 = mc.subscriber();
43+
// const s2 = mc.subscriber();
44+
// const s3 = mc.subscriber();
45+
// const dom = [
46+
// runCompose(s1, elementWithAnchor),
47+
// runCompose(s2, elementWithAnchor),
48+
// runCompose(s3, elementWithAnchor),
49+
// ];
50+
// dispatch('wat');
51+
52+
// await null;
53+
// await null;
54+
// await null;
55+
56+
57+
58+
// // function getNextPromises(list = [s1, s2, s3]) {
59+
// // return Promise.all(list.map(s => s.next()));
60+
// // }
61+
// // function getPromise(s) {
62+
// // return s => s.next();
63+
// // }
64+
65+
66+
// // let values = toValues(await getNextPromises());
67+
68+
// // // eslint-disable-next-line no-sparse-arrays
69+
// // expect(values).toEqual([, , ,]);
70+
71+
// // let promises = getNextPromises();
72+
// // dispatch(1);
73+
// // values = toValues(await promises);
74+
// // expect(values).toEqual([1, 1, 1,]);
75+
76+
// // promises = getNextPromises([s1, s3]);
77+
// // dispatch(22);
78+
// // values = toValues(await promises);
79+
// // expect(values).toEqual([22, 22]);
80+
81+
82+
// // dispatch(10);
83+
// // const p1 = getPromise([s1])[0];
84+
// // dispatch(20);
85+
// // const p2 = getPromise([s2])[0];
86+
// // dispatch(30);
87+
// // const p3 = getPromise([s3])[0];
88+
// // dispatch(40);
89+
90+
// // values = toValues(await Promise.all([p1, p2, p3]));
91+
// // expect(values).toEqual([10, 10]);
92+
93+
94+
// });
95+

packages/channels/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"vitest": "^1.3.1"
3232
},
3333
"devDependencies": {
34+
"@testing-library/dom": "^9.3.4",
35+
"azoth": "workspace:^",
3436
"happy-dom": "^13.6.2",
3537
"test-utils": "workspace:*"
3638
}

0 commit comments

Comments
 (0)