-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
channel fromPromise with (promise, transform { startWith })
- Loading branch information
Showing
7 changed files
with
1,755 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>"`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
|
||
|
||
// }); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.