Skip to content

Commit

Permalink
channel fromPromise with (promise, transform { startWith })
Browse files Browse the repository at this point in the history
  • Loading branch information
martypdx committed Feb 29, 2024
1 parent 72e91d5 commit 84b9e51
Show file tree
Hide file tree
Showing 7 changed files with 1,755 additions and 0 deletions.
49 changes: 49 additions & 0 deletions packages/channels/channels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { subject } from './generators.js';
import 'test-utils/with-resolvers-polyfill';

function throwMoreArgumentsNeeded() {
throw new TypeError(`\
"Channel.from(promise)" requires more arguments, \
expected a transform option or channel options. \
Use "promise.then(transform)" for creating a channel from a single promise`);
}

function throwAsyncSourceTypeError(type) {
throw new TypeError(`\
Unexpected async source type "${type}". Expected an asynchronous data provider type, or \
a function that returns an asynchronous data provider type."`);
}

export class Channel {
static from = from;
}

export function from(asyncSource, transform, options) {
if(!options && typeof transform === 'object') {
options = transform;
transform = null;
}

const type = typeof asyncSource;

switch(true) {
case asyncSource instanceof Promise:
return fromPromise(asyncSource, transform, options);
default:
throwAsyncSourceTypeError(type);

}
}

function fromPromise(promise, transform, options) {
const startWith = options?.startWith;
if(startWith) {
return [fromPromiseStartWith(promise, transform, startWith)];
}
return [transform ? promise.then(transform) : promise];
}

async function* fromPromiseStartWith(promise, transform, startWith) {
yield startWith;
yield transform ? promise.then(transform) : promise;
}
Empty file removed packages/channels/channels.test.js
Empty file.
41 changes: 41 additions & 0 deletions packages/channels/channels.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from 'vitest';
import { findByText } from '@testing-library/dom';
import { Channel } from './channels.js';
import { beforeEach } from 'vitest';

beforeEach(async context => {
document.body.innerHTML = '';
context.fixture = document.body;
context.find = filter => findByText(context.fixture, filter, { exact: false });
});

const Cat = ({ name }) => <p>{name}</p>;
const Loading = () => <p>loading...</p>;

test('Channel.from(promise, transform)', async ({ fixture, find, expect }) => {
const promise = Promise.resolve({ name: 'felix' });
const [LayoutChannel] = Channel.from(promise, Cat);
fixture.append(<LayoutChannel />);
const dom = await find('felix');
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>felix<!--1--></p>"`);
});

test('Channel.from(promise, transform, { startWith })', async ({ fixture, find, expect }) => {
const { promise, resolve } = Promise.withResolvers();
const [LayoutChannel] = Channel.from(promise, Cat, {
startWith: <Loading />
});
fixture.append(<>{LayoutChannel}</>);
let dom = null;

dom = await find('loading...');
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>loading...</p>"`);

// we delay promise resolution and trigger here to not miss the
// intermediate "loading...", async testing-library "find"
// pushes it out too far and it picks up the the "felix".
resolve({ name: 'felix ' });

dom = await find('felix');
expect(dom.outerHTML).toMatchInlineSnapshot(`"<p>felix <!--1--></p>"`);
});
90 changes: 90 additions & 0 deletions packages/channels/generators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

export function subject(transform, options) {
if(!options && typeof transform === 'object') {
options = transform;
transform = null;
}

let initialValue, startWith;
if(options) {
initialValue = options.initialValue;
startWith = options.startWith;
if(initialValue !== undefined) {
if(startWith !== undefined) {
throw new Error('Cannot specify both initialValue and startWith option');
}
if(!transform) {
throw new Error('Cannot specify initialValue without a transform function');
}
}
}

const relay = { resolve: null };

let unsentEarlyDispatch = null;

function dispatch(payload) {
if(transform) payload = transform(payload);
if(relay.resolve) relay.resolve(payload);
else {
// eslint-disable-next-line eqeqeq
if(payload != null) unsentEarlyDispatch = payload;
}
}

async function* generator() {
let promise = null;
let resolve = null;

if(initialValue !== undefined) {
yield transform(initialValue);
}
if(startWith !== undefined) {
yield startWith;
}
// this handles dispatch that happens between
// initial/start yields and main loop:
// eslint-disable-next-line eqeqeq
while(unsentEarlyDispatch != null) {
const toYield = unsentEarlyDispatch;
unsentEarlyDispatch = null;
yield toYield;
}

while(true) {
({ promise, resolve } = Promise.withResolvers());
relay.resolve = resolve;
yield await promise;
}
}

const asyncIterator = generator();
return [asyncIterator, dispatch];
}


export function multicast(iterator) {
return new Multicast(iterator);
}

class Multicast {
consumers = [];
constructor(subject) {
this.subject = subject;
this.#start();
}

async #start() {
for await(let value of this.subject) {
for(let consumer of this.consumers) {
consumer(value);
}
}
}

subscriber(transform, options) {
const [dispatch, iterator] = subject(transform, options);
this.consumers.push(dispatch);
return iterator;
}
}
95 changes: 95 additions & 0 deletions packages/channels/generators.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { beforeEach, test } from 'vitest';
import { subject } from './generators.js';
import 'test-utils/with-resolvers-polyfill';
import { screen } from '@testing-library/dom';

beforeEach(async context => {
document.body.innerHTML = '';
context.fixture = document.body;
});

test.skip('subject', async ({ fixture, expect }) => {

const [asyncIterator, dispatch] = subject();
const dom = <div>{asyncIterator}!</div>;
fixture.append(dom);

await screen.findByText('!', { exact: false });

expect(dom).toMatchInlineSnapshot(`
<div>
<!--0-->
!
</div>
`);

dispatch('Hello World');
await screen.findByText('Hello World', { exact: false });

expect(dom).toMatchInlineSnapshot(`
<div>
Hello World
<!--1-->
!
</div>
`);
});

// test.skip('multicast', async ({ expect }) => {
// const [asyncIterator, dispatch] = subject({ startWith: 'hello' });

// const mc = multicast(asyncIterator);
// const s1 = mc.subscriber();
// const s2 = mc.subscriber();
// const s3 = mc.subscriber();
// const dom = [
// runCompose(s1, elementWithAnchor),
// runCompose(s2, elementWithAnchor),
// runCompose(s3, elementWithAnchor),
// ];
// dispatch('wat');

// await null;
// await null;
// await null;



// // function getNextPromises(list = [s1, s2, s3]) {
// // return Promise.all(list.map(s => s.next()));
// // }
// // function getPromise(s) {
// // return s => s.next();
// // }


// // let values = toValues(await getNextPromises());

// // // eslint-disable-next-line no-sparse-arrays
// // expect(values).toEqual([, , ,]);

// // let promises = getNextPromises();
// // dispatch(1);
// // values = toValues(await promises);
// // expect(values).toEqual([1, 1, 1,]);

// // promises = getNextPromises([s1, s3]);
// // dispatch(22);
// // values = toValues(await promises);
// // expect(values).toEqual([22, 22]);


// // dispatch(10);
// // const p1 = getPromise([s1])[0];
// // dispatch(20);
// // const p2 = getPromise([s2])[0];
// // dispatch(30);
// // const p3 = getPromise([s3])[0];
// // dispatch(40);

// // values = toValues(await Promise.all([p1, p2, p3]));
// // expect(values).toEqual([10, 10]);


// });

2 changes: 2 additions & 0 deletions packages/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"vitest": "^1.3.1"
},
"devDependencies": {
"@testing-library/dom": "^9.3.4",
"azoth": "workspace:^",
"happy-dom": "^13.6.2",
"test-utils": "workspace:*"
}
Expand Down
Loading

0 comments on commit 84b9e51

Please sign in to comment.