Skip to content

Latest commit

 

History

History
283 lines (189 loc) · 13.1 KB

CHANGELOG.md

File metadata and controls

283 lines (189 loc) · 13.1 KB

Releases

1.3.0 Filters can remove or mutate events

Solves the common use cases of filters which remove events from downstream listeners', or which replace one event with another. If a Filter function returns null, no further filters or listeners will see it. If a non-nullish object, that object replaces the event (make sure it duck-types an FSA or EventWithAnyFields)

1.2.9 Better tree-shakability via sideEffects: false

Simple. Viewable in the stats on Bundlephobia

1.2.7 Better Typescript typings, docs.

Won't show type errors when triggering typescript-fsa actions (they weren't runtime issues anyway). 4 fewer ts-ignores due to Type Guards as, and generally more type awareness.

1.2.6 After can defer an Observable.

It'd be handy to have after(100, ob$) defer the Observable-now it does. Important to note: this is not the same as obs.pipe(delay(N)), which delays the notifications, but still eagerly subscribes. after defers the subscription itself, so if canceled early enough, the side effect does not have to occur. Perfect for debouncing, with mode: replace.

usually (which is why you can call toPromise on it, or await it)

1.2.5 Allow listener-returned bare values or generators

Listeners ought to return Observables, but when they return an iterable, which could be a generator, how should the values be provided? They generally become next notifications individually, to preserve cases where, like Websockets, many notifications come in incrementally. However, a String is iterable, and it seemed a bug to next each letter of the string.

1.2.4 Can declaratively fire a 'start' event upon Observable subscription

For feature-parity with conventions like for Redux Query, and those that emit an event at the beginning of an async operation, a TriggerConfig may now admit a start event, which will be triggered.

Also fixed an issue where trigger: true (the source of so many Typescript errors) wasn't actually triggering.

1.2.3 Important bug fix

1.2 Smaller Bundle, More Robust

  • Bundle size 2.23Kb (Down from 2.37Kb)
  • after is type-safe!
  • channel.listen({ takeUntil: matcher }): Now adds a takeUntil(query(matcher)) to each Observable returned from the listener.

Possibly breaking:

  • channel.trigger: The 3rd argument resultSpec was removed
  • channel.listen({ trigger: { error }}): Now rescues the error and keeps the listener alive.

Includes the combineWithConcurrency export to allow ConcurrencyMode/string declarative style Observable-combination (without using the less-mnemonic operators).

1.1.6 takeUntil

channel.listen({ takeUntil: pattern }): Now adds a takeUntil(query(pattern)) to each Observable returned from the listener to allow for declarative cancelation.

1.1.3 await query(), and succint tests

Similar to after, there is a then method exposed on the return value from query(), so it is await-able without explicitly calling toPromise on it. Also, found a really nice testing pattern that will work as well in a this-less test framework like Jest, as it does in mocha, and also has fewer moving parts overall.

1.1.2 Support generators as listeners

For Redux Saga and generator fans, a listener can be a generator function— Observable-wrapping of generators is easily done.

1.1.1 Add optional TypeScript typings

The primary functions you use to trigger, filter, listen, and query the event bus, as well as the after utility, all at least somewhat support adding Typescript for addtional editor awareness.

1.1.0 Remove React dependencies

The convenience hooks have been moved to the polyrhythm-react, so as not to import React in Node environments, or express a framework preference.

1.0.12 Trigger whole event objects

Inspired by JQuery, the polyrhythm API trigger took the name of the event and the payload separately.

const result = trigger('event/type', { id: 'foo' });

A Flux Standard Action was created for you with type, and payload fields. This meant that in Listeners, the event object you'd get would have id nested under the payload field of the event.

listen('event/type', ({ payload: { id } }) => fetch(/* use the id */));

But what if you have Action Creator functions returning objects, must you split them apart? And what if you dont' want to nest under payload for compatibility with some other parts of your system? Now, you can just trigger objects:

const result = trigger({ type: 'event/type', id: 'foo' });
listen('event/type', ({ id }) => fetch(/* use the id */));

Remember to keep the type field populated with a string, all of polyrhtyhm keys off of that, but shape the rest of the event how you like it!


1.0.11 query.toPromise() returns the next matching event

Commit: (cb5a859)

A common thing to do is to trigger an event and await a promise for a response, for example with events api/search and api/results.

The way to do this before was to set up a promise for the response event type, then trigger the event that does the query. With proper cleanup, it looked like this:

const result = new Promise(resolve => {
  const sub = query('api/results').subscribe(event => {
      sub.unsubscribe()
      resolve(event)
  })
}
trigger('api/search', { q: 'query' })
result.then(/* do something with the response */)

To simplify this pattern, now you can do:

const result = query('api/results').toPromise();

trigger('api/search', { q: 'query' });
result.then(/* do something with the response */);

To do this polyrhythm redefines toPromise() on the Observable returned by query to be a Promise that resolves as of the first event. As noted by Georgi Parlakov here, toPromise() waits for your Observable to complete, so will never resolve if over a stream that doesn't complete, and polyrhythms event bus and queries over it do not complete by design!

A couple of tips:

  • The call to toPromise() must be done prior to calling trigger, or your result event may be missed.
  • Attaching the then handler to the Promise can be done before or after calling trigger - Promises are flexible like that.

Keep in mind that using a listener is still supported, and is often preferred, since it allows you to limit the concern of some components to being trigger-ers of events, and allowing other components to respond by updating spinners, and displaying results.

listen('api/results', ({ type, payload }) => {
  /* do something with the response */
});

trigger('api/search', { q: 'query' });

If there may be many different sources of api/results from api/query events, you can include an ID in the event. This code shows how to append a query identifier in each event type:

const queryId = 123;
const result = query(`api/results/${queryId}`).toPromise();

trigger(`api/search/${queryId}`, { q: 'query' });
result.then(/* do something with the response */);

On a humorous note, it was funny because I'd published the package without building it twice, making builds 1.0.9 and 1.0.10 useless. At least I discovered the npm prepublishOnly hook to save me from that in the future.


1.0.8 microq and macroq functions

Commit: (bc583de)

Here's a fun quiz: In what order are the functions fn1, fn2, fn3, fn4 called?

/* A */ Promise.resolve(1).then(() => fn1());

/* B */ await 2;
fn2();

/* C */ setTimeout(() => fn3(), 0);

/* D */ fn4();

Obviously the synchronous function fn4 is called before async ones - but in B, is fn3 delayed or sync, when awaiting a constant? And which completes first, Promise.resolve(fn1), or setTimeout(fn3, 0)? I found this stuff hard to remember, different in Node vs Browsers, and the complicated explanations left me wanting simply more readable API calls. So polyrhythm now exports microq and macroq functions.

In summary, you can use macroq to replace setTimeout(fn, 0) code with macroq(fn). This provides equivalent behavior which does not block the event loop. And you can use microq(fn) for async behavior that is equivalent to resolved-Promise deferring, for cases like layout that must complete before the next turn of the event loop.

Quiz Explanation: B is essentially converted to A— a deferred call— despite the value 2 being synchronously available, because that's what await does. And promise resolutions are processed before setTimeout(0) calls. This is because JS has basically two places asynchronous code can go, the microtask queue and the macrotask queue. They are detailed on MDN here, but to simplify, the quiz example code basically boils down to this.

/* A */ microq(fn1);

/* B */ microq(fn2);

/* C */ macroq(fn3);

/* D */ fn4();

And thus the answer is fn4, fn1, fn2, and fn3, or D,A,B,C.


1.0.7 TypeScript typings corrected for after

Commit: (defeeeb)

Aside from having an awesome SHA, 1.0.7 is a TypeScript-only enhancement to the after function. Remember after is the setTimeout you always wanted - a lazy, composable, subscribable, awaitable object:

async function ignitionSequence() {
  await after(1000, () => console.log('3'));
  await after(1000, () => console.log('2'));
  await after(1000, () => console.log('1'));
  await after(1000, () => console.log('blastoff!'));
}
ignitionSequence();

So basically, after is a deferred, Observable value or a function call. I won't call it a Monadic lift, because I'm not sure, but I think that's what it is :)

Anyway, now TypeScript/VSCode won't yell at you if you omit a 2nd argument.

async function ignitionSequence() {
  await after(1000, () => console.log('3'));
  await after(1000, () => console.log('2'));
  await after(1000, () => console.log('1'));
  await after(1000); // dramatic pause!
  await after(1000, () => console.log('blastoff!'));
}
ignitionSequence();

And just to refresh your memory for the DRY-er, more readable way to do such a sequence:

const ignitionSequence = () =>
  concat(
    after(1000, '3'),
    after(1000, '2'),
    after(1000, '1'),
    after(1000, 'blastoff!')
  );

ignitionSequence().subscribe(count => console.log(count));

You can get imports after and concat directly from polyrhythm, as of

after is so handy, there needs to be a full blog post devoted to it.


1.0.6 Handy RxJS exports

Commit: (ee38e6a)

If you use polyrhythm, you already have certain components of RxJS in your app. If you need to use only those components, you shouldn't need to have an explicit dependency on RxJS as well. For the fundamental operators map, tap, and scan that polyrhythm relies upon, you can import these directly. Same with the concat function of RxJS.

- import { tap } from 'rxjs/operators'
- import { concat } from 'rxjs'
+ import { tap, concat } from 'polyrhythm'

Unfortunately it looks like I introduced a conflict where filter is exported both as an RxJS operator and as the channel.filter function - it might be a source of error for some situations, but I'll address it in a patch later.