Security News
Bun 1.2 Released with 90% Node.js Compatibility and Built-in S3 Object Support
Bun 1.2 enhances its JavaScript runtime with 90% Node.js compatibility, built-in S3 and Postgres support, HTML Imports, and faster, cloud-first performance.
š£ Do not use this yet, it is not stable, it is not tested, it is completely experimental.
A mithril wrapper that adds :
Think: "Solid.js in mithril" and you're close.
npm install bacta
import m from 'mithril'
with import m from 'bacta'
All existing code will work the same but you'll now have some opt in granular reactivity.
You can try out bacta in a code playground with this single import:
import m from 'https://esm.sh/v117/bacta@latest/es2022/dist/bacta.esm.js'
See example here
Bacta treats mithril as a peer dependency, but this build goes and conveniently includes mithril in the ESM bundle as well.
š¤ bacta
is a superset of mithril, so please refer to the
excellent API docs on the mithril website
for the current stable mithril API.
The following documents the tiny superset of extensions we've layered on top of mithril.
You can directly return a view
function from a closure component instead of needing to return { view }
Legacy mithril:
function Old(){
return {
view: () => h('h1', 'Hello')
}
}
New and improved Bacta ā¢ļø
function New(){
return () => h('h1', 'Hello')
}
If you are using typescript, you may want to use the bacta.component
helper:
const Greeting = bacta.component<{ name: string }>(() => ({ attrs }) =>
m('p', `Hello ${attrs.name}`)
)
It is completely optional, but it makes for a better typed experience than manual annotation of the VNode
If you find that a bit verbose, you can alias it to c
const c = bacta.component
const Greeting = c<{ name: string }>(() => (v) =>
m('p', `Hello ${v.attrs.name}`)
)
All components and lifecycle hooks in mithril.js get access to a special object
called the vnode
(virtual dom node). Because this object is omnipresent in the
mithril API we rely on that to add new feature additions.
Creates a stream that will automatically end when the component unmounts.
Here's a simple example:
import m, { BactaComponent, Vnode } from 'bacta';
const Example = bacta.component((v) => {
// this stream won't emit after this component unmounts
// nor will any streams the depend upon it
let value = v.useStream(500);
return view: () => null;
});
A helper to encapsulate create and teardown effects.
v.useEffect({ name: 'example effect' }, function * () {
while (true) {
yield new Promise( Y => setTimeout(Y, 1000))
console.log('hello effect');
}
});
Within useEffect
you can write to the self
stream, and this can be used to
emit values to the consumer of the effect.
let countStream = v.useEffect({ name: 'count' }, function * ({self}) {
let count = 0;
while (true) {
yield new Promise(Y => setTimeout(Y, 1000))
// emit an incremented number every second
self(count++)
}
});
// no initial value
countStream()
// undefined
// logs:
// count 0
// count 1
// count 2 etc
countStream.map( x => console.log('count', x) )
By default this returned stream will have no initial value, and you may get type
errors saying your effect stream may be undefined
. You can solve this by passing
in a seed
value as the first argument:
let countStream = v.useEffect({ name: 'count', seed: 0 }, function * ({self}) {
while (true) {
yield new Promise(Y => setTimeout(Y, 1000))
self.update( x => x + 1 )
}
});
// has initial value
countStream()
// 0
// logs:
// count 0
// count 1
// count 2 etc
countStream.map( x => console.log('count', x) )
In addition to a seed, you can subscribe to streams and stores by yielding them. This will wait for a value and subscribe to that stream for all future emits.
async function checkConstraints(value){...}
let proposed = v.useStream('')
let accepted = v.useEffect<string>({ name: 'constraints' }, function * ({self}) {
try {
// extract value and react to changes
// from here on in if stream is unset
// we wait here until it emits
const p = yield proposed
// run an async function
// if we receive another trigger
// while waiting for a promise to resolve
// we'll cancel this effect and start a new one
yield checkConstraints(p)
// constraints satisfied, so write to the world:
self(p)
} catch (e) {
console.error('Constraint failed', e)
}
})
If you want your effect to update when other dependencies change, you can yield
normal mithril stream combinators like .map
, .merge
, .lift
etc. You can also
just yield in series, which has no real penalty in practice.
let seed = 0;
let duration = v.useStream(1000)
let countStream = v.useEffect({ name: 'example', seed }, function * ({self}) {
let id: number;
let ms = yield duration
let f = yield factor
let exp = yield exponent
while (true) {
yield new Promise(Y => setTimeout(Y, ms))
self.update( x => (x + 1 * f) ** exp );
}
});
But if you want to register your dependencies in one yield
you can yield an array ((Stream<any> | Store<any>)[]
)
let seed = 0;
let duration = v.useStream(1000)
let countStream = v.useEffect({ name: 'example', seed }, function * ({self}) {
let id: number;
// subscribe to all the streams/stores in one go
let [ms, f, exp] = yield [duration, factor, exponent]
while (true) {
yield new Promise(Y => setTimeout(Y, ms))
self.update( x => (x + 1 * f) ** exp );
}
});
Any time one of your dependencies emit, your effect will re-run.
If an existing effect is already running, it will be cancelled via:
iterator.return( new Cancellation() )
E.g. let's use a setInterval
with a cleanup:
v.useEffect({ name: 'example', seed }, function * () {
let id;
try {
let id: number;
id = setInterval(() => {
// ...
}, 1000)
// block forever to allow interval to run
yield new Promise(() => {})
} finally {
clearInterval(id)
// finally will run:
// - if the effect runs to completion
// - or if there was an exception
// - or if bacta cancels the effect because:
// - the component unmounted
// - one of the dependencies emitted
}
});
There are also multiple callbacks for different exit / entry states on the effect context
passed in as a parameter to the effect:
onReturn((data) => { ... })
called when your effect exits without any exceptionsonThrow((error) => { ... })
called when your effect exits via throwing an exceptiononFinally(() => { ... })
called when your effect exits via return
or via an exceptiononReplace(() => { ... })
called when a new instance of your effect is startingSo we could rewrite the above example with onFinally
like this:
v.useEffect({ name: 'example', seed }, function * (ctx) {
let id;
let id: number;
id = setInterval(() => {
// ...
}, 1000)
ctx.onFinally(() => clearInterval(id))
// block forever to allow interval to run
yield new Promise(() => {})
});
onReplace
can be handy when you want to do clean up only at the last possible moment.
const data$ = v.useEffect({ name: 'widget' }, function * (){
// restart whenever these streams change
let [some, dependencies] = yield [some$, dependencies$]
let imperativeThing = new ImperativeThing(some)
let imperativeWidget = new ImperativeWidget(dependencies)
onReplace(() => {
imperativeThing.close()
imperativeWidget.close()
})
return {
...some
,dependencies
,imperativeThing
,imperativeWidget
}
})
In the above example we want our imperativeThing
and imperativeWidget
to continue to exist long after we've returned our object. We only want to clean up these instances when we're creating a new instance.
Our onReplace
will run immediately before the next effect is initialized, or when the component is unmounting.
Because an effect returns a Stream
you can permanently end an effect by calling stream.end(true)
just like any other stream.
// end the effect
// this will trigger any logic in `finally` as well
countStream.end(true);
Here is a complete useInterval
example inspired by Dan Abramov's
blog post
import m, { BactaComponent, Vnode } from 'bacta';
function useInterval(v: Vnode) {
let duration = useStream(0);
let interval = v.useEffect({ name: 'example', seed: 0 }, function * ({self}) {
let ms = yield duration
while (true) {
yield new Promise( Y => setTimeout(Y, ms) )
self.update( x => x + 1 )
}
});
return { interval, duration }
}
const Example = bacta.component((v) => {
let { interval, duration } = useInterval(v)
return () =>
m('.example'
, m('p', 'Interval has fired')
, m('p'
m('label'
,'Interval Duration'
, m('input[type=range]', { $oninput: { value: duration }})
)
)
, m('span', () => duration.map( x => `${x}ms`))
)
});
š Note this example never requires a VDOM redraw.
Usually you want to emit multiple values without re-initializing the effect, and in this case you write to self
. But if your effect only has one result per invocation, you can just return the result.
E.g. this affect emits some DOM, any time the remaining
stream emits
const viewStream = v.useEffect({ name: 'view stream' }, function * ({self}) {
const n : number = yield remaining;
self(
m('span.todo-count'
, m('strong'
, `${n} ${n==1 ? 'item' : 'items'} left`
)
)
)
})
We can just use return
here instead
const viewStream = v.useEffect({ name: 'view stream' }, function * ({self}) {
const n : number = yield remaining;
return (
m('span.todo-count'
, m('strong'
, `${n} ${n==1 ? 'item' : 'items'} left`
)
)
)
})
bacta
has a built in css-in-js util that supports inlining streams as values.
import m, { css } from 'bacta'
const c = m.stream('pink');
const bc = m.stream('black');
const scale = m.stream(1);
const css = m;
m(
'.app',
css`
& {
color: ${c};
background-color: ${bc};
transform: scale(${scale});
}
`,
m('input[type=range]', {
value: scale,
$oninput: (e) => scale(e.target.valueAsNumber),
}),
);
Behind the scenes we're replacing every interpolation with a generated css variable.
š¤ The name of the css var is the hash of the css source, so it could forseeably be used with server-side hydration (because it is stateless).
If you want to inject a literal value into the css context that won't be replaced with a css var you can do so by wrapping the value in css(value)
E.g.
const desktop = css('1000px')
css`
@media( min-width: ${desktop} ) {
& {
grid-template-columns: 1fr 1fr 1fr;
}
}
`
Note on subsequent renders or invocations if the value of your literal changes, the sheet won't be updated. If you want dynamic values in your stylesheet use streams or store values.
Literal values are includes in the sheet's identity so if you have two components with identical sheets but different literal values they will generate separate css sheets.
Bacta hyperscript is still backwards compatible with mithril but now also supports using mithril streams directly in the view as:
You can also use thunks to run a section of view one time only.
And there is new event handler syntax for easy 2 way binding and avoiding mithril's default global redraw behaviour.
Let's walk through it bit by bit. šµļø
You can directly embed streams as attributes in the view.
const Example = bacta.component((v) => {
let value = v.useStream(500);
return () =>
m('input[type=range]'
,
{ value // <-- this is a stream
, min: 0
, max: 1000
, oninput: e => value(e.target.value)
}
)
});
When bacta encounters a stream attribute it sets up a stream listener that patches the DOM directly like so:
vnode.oncreate = (v) => {
value.map((x) => {
v.dom.value = x;
});
};
When value
changes, our input dom value patches without a redraw or vdom diff.
You can directly embed streams as attributes on the style object as well. This is helpful for manual binding to named css vars.
For anything more involved it is recommend to use the built in
css
helper wherever possible.
// in the component
const x = useStream(0);
const y = useStream(0);
const scale = useStream(1);
const rotation = useStream(45);
// in the view
m('player-character', {
style: {
'--x': x,
'--y': y,
'--scale': scale,
'--rotation': (x) => rotation.map((x) => `${x}deg`),
},
});
You can directly embed streams as children in the view.
let now = useStream();
let now = useEffect({ name: 'now' }, ({self}) => {
while (true) {
yield new Promise( Y => setTimeout(Y, 1000) )
return Date.now();
}
});
m('.app', m('p', 'The time is: ', now));
If you inject a stream child into the view, we need to call
m.render(parentElement, childVTree)
which means we need to know there's no
other child elements that may be mistakenly replaced by that operation.
So before updating we check if the parentElement has any other child elements on
first render. And if not, we claim the tree as our own. Otherwise we wrap our
node in a bacta-stream-child
element and render into that from then on.
So as a rule, if you want to avoid bacta wrapping your stream elements in a child node, ensure there's no other children siblings next to your stream element.
If anyone can think of a clever way to work around this caveat, let me know
$onevent
)Mithril has a very cool feature, whenever an event handler fires a global redraw automatically occurs. This means mithril doesn't need to have an opinion on how you manage state. You can use a simple pojo.
With Bacta however, you will often want to prevent mithril from performing a redraw. That is because the view updates without a redraw due to streams being supported directly in hyperscript.
You can prevent mithril from automatically redrawing by prefixing your event
handler with a $
.
m('button', {
$onclick() {}, // DOES NOT trigger a global redraw
});
m('button', {
onclick() {}, // DOES trigger a global redraw
});
We call this feature a "dollar function".
const Example = bacta.component((v) => {
let value = v.useStream(500);
return () => [
m('input[type=range]', {
value,
min: 0,
max: 1000,
// the $ prefix here tells bacta
// not to trigger a VDOM redraw
$oninput: (e) => value(e.target.value),
}),
// this paragraph will update
// without a global redraw because `value` is
// a stream.
m('p', 'Value is', value),
]
});
It is possible to manually create a 2 way binding
const value = useStream(false);
m('input[type=checkbox]', {
// this binds the input value
// to reactive updates from the stream
checked: value,
// this updates the same when
// the input value changes
$oninput: (e) => value(e.target.checked),
});
But a shorthand exists that does exactly the same thing:
const value = useStream(false);
m('input[type=checkbox]', {
min: 0,
max: 1000,
$oninput: { checked: value },
});
If your stream name and event.target property have the same name, you can take advantage of ES6 object shorthand syntax:
const checked = useStream(false);
m('input[type=checkbox]', {
min: 0,
max: 1000,
$oninput: { checked },
});
This works with any type of event, it simply plucks the matching key from
event.target
and writes it to the corresponding stream. On initialization it
sets the dom property to the current value of the stream.
We call this a "dollar binding".
When binding streams to the view you'll clickly find you need to ever so slightly adjust your stream values to work with your view.
It might be text formatting, or adding px
to a raw stream value.
In bacta streams created within the view are automatically ended on the next render and unmount. So you can define streams every redraw and you won't run out of memory!
m(
'.app',
css`
& {
transform: translateX(
${x.map( x => `${x}px`)}
);
}
`,
m('p', 'Hello', name.map(titleCase)),
);
If you want to be explicit though can you wrap the creation of a stream in a thunk, it will then only be created one time instead of destroyed/recreated every render:
m(
'.app',
css`
& {
/* subtle difference: () => ... */
transform: translateX(
${() => x.map( x => `${x}px`)}
);
}
`,
// subtle difference: () =>
m('p', 'Hello', () => name.map(titleCase)),
);
The thunk only executes one time. That means you aren't recreating these streams every render. This is definitely more efficient, but it is nice to know if you forget to wrap in a thunk the stream will still be cleaned up automatically.
Bacta streams are an API superset of mithril streams, but with some very slight changes in behaviour.
We've also added quite a few new methods to stream.
New accessors:
.get
.set
.update
New combinators:
.filter
.reject
.dropRepeats
.dropRepeatsWith
.debounce
.throttle
.awaitAll
.awaitLatest
New behaviour:
stream.update
One minor frustation when working with mithril streams is incrementing values requires this little dance:
let count = v.useStream(0);
let inc = () => count(count() + 1);
We would like to provide this alternative:
let count = v.useStream(0);
let inc = () => count((x) => x + 1);
But this would prevent some very useful patterns where we pass a function into a stream (often used by a community favourite meiosis)
So we instead add an explicit update
api which only accepts a visitor
function, this allows us to increment a count like so:
let count = v.useStream(0);
let inc = () => count.update((x) => x + 1);
This more explicit API also ties in well with our new .get
and .set
stream
APIs.
stream.get
/ stream.set
bacta/stream
's can be read via the traditional getter / setter API used in
mithril
and flyd
const a = v.useStream(0);
a(); // => 0
a(1);
a(); // => 1
But we've found in practice it can be beneficial to be explicit when getting or setting. Lets say we have a list of streams and we want to turn it into a list of values:
let streams = [a, b, c, d];
// works fine
let values = streams.map((x) => x.get());
// works fine
let values = streams.map((x) => x());
// uh oh, accidentally updated the stream
let values = streams.map(x);
Another scenario: you want to let a component read from a stream, but not write to it.
// can only read
m(UntrustedComponent, { getValue: value.get });
// can read and write
m(UntrustedComponent, { getValue: value });
stream.get
is also its own stream, so you can subscribe to changes without
having the ability to write back to the source.
someStream.get.map((x) =>
console.log('someStream changed', x)
);
It is also just a lot easier to grep for, you can more easily distinguish an arbitrary function call from a stream get/set.
stream.filter
interface Filter<T> {
(predicate: (value: T) => boolean): Stream<T>;
}
Only emits when the user provided predicate function returns a truthy value.
const person = useStream({ name: 'Barney', age: 15 });
setInterval(() => {
person.update((x) => ({ ...x, age: x.age + 1 }));
}, 86400 * 365 * 1000);
const isAdult = (x) => x.age > 18;
const adult = person.filter(isAdult);
stream.reject
interface Reject<T> {
(predicate: (value: T) => boolean): Stream<T>;
}
Only emits when the user provided predicate function returns a falsy value.
const child = person.reject(isAdult);
stream.dropRepeats
and stream.dropRepeatsWith
Prevents stream emission if the new value is the same as the prior value. You can specify custom equality with dropRepeatsWith
.
interface DropRepeats<T> {
(): Stream<T>;
}
interface DropRepeatsWith<T> {
(equality: (a: T, b: T) => boolean): Stream<T>;
}
// only send consecutively unique requests to the API
const results =
searchValue.dropRepeats().awaitLatest(async (q) => {
return m.request('/api/search?q=' + q);
});
You can also specify equality:
const equality = (a, b) =>
a.toLowerCase() == b.toLowerCase()
const results =
searchValue
.dropRepeats(equality)
.awaitLatest(async (q) => {
return m.request('/api/search?q=' + q);
});
stream.afterSilence
and stream.throttle
interface AfterSilence<T> {
(ms?: number): Stream<T>;
}
interface Throttle<T> {
(ms?: number): Stream<T>;
}
// hit the API at most every 300ms
const results1 =
searchValue
.throttle(300)
.awaitLatest(async (q) => {
return m.request('/api/search?q=' + q);
});
// hit the API when searchValue has not emitted any values for at least 300ms
const results2 =
searchValue
.afterSilence(300)
.awaitLatest(async (q) => {
return m.request('/api/search?q=' + q);
});
stream.awaitAll
and stream.awaitLatest
interface Await<T> {
(
visitor: (value: T) => Promise<T>,
): Stream<T>;
}
Converting promises to streams is not as trivial as it seems because a stream
doesn't completely semantically capture the domain of a Promise
.
We want to encourage you to pick the correct semantic conversion depending on your usecase, so we use longer awkward names than we'd like to force you to consider what you are doing.
Here are the options available:
stream.awaitAll
: Wait for every promise to resolve, emit unordered resultsstream.awaitLatest
: Wait for the latest emitted promise to resolve, if a new
promise is emitted while we're waiting, stop waiting to the old one.We do not support emitting all results in order as we've never needed that in practice.
You tend to either not care about order, or you only want the latest value (search APIs)
const search = useStream();
const results = search
.throttle(300)
.awaitLatest((q) => m.request(`/api/products/search?q=` + q));
In the above example, the user can type quite furiously, but we will at most send a search request every 300ms
. We then pipe the search term into awaitLatest
. Now if the API takes 500ms
to return the list of results, then the user will have already sent a 2nd request to the search endpoint. We then will ignore the response from the first request and now only emit the result of that second newer request. That way we never see a result for an old search query.
timeline
title awaitLatest
300ms : /api/search?q=ban
400ms : /api/search?q=banana
500ms : Response(q=ban)
: Response ignored
600ms : Response(q=banana)
: Emits Response(q=banana)
The downside of this is, if the user keeps fiddling with the search query you may never see any results. As each new request cancels the prior request before it can respond.
So you may instead prefer to let every response come through even if its arrives out of order. This is usually the worse choice, but it is needed as a compromise in some cases.
const search = useStream();
const results = search
.throttle(300)
.awaitAll((q) => m.request(`/api/products/search?q=` + q));
timeline
title awaitAll
300ms : /api/search?q=ban
400ms : /api/search?q=banana
500ms : Response(q=ban)
: Emits Response(q=ban)
600ms : Response(q=banana)
: Emits Response(q=banana)
Now with awaitAll
we naively emit every response as it arrives. They may even arrive from the server out of order so be careful. š²
We encourage you to handlle errors yourself within the awaitAll
/
awaitLatest
function and coerce the branching model of promises into regular
data like so:
const results = search
.throttle(300)
.awaitAll((q) =>
m.request(`/api/products/search?q=` + q)
.then(
value => ({ tag: 'resolved', value })
, value => ({ tag: 'rejected' value })
)
)
This is beneficial because it retains errors in the stream graph and can then be handled with other reactive operators.
But you can also specify an error handler as a second argument, this works well for debugging, logging or routing to a central error stream.
Simple Error Logging:
const results = search
.throttle(300)
.awaitAll(
(q) => m.request(`/api/products/search?q=` + q),
(error) => console.error(error),
);
Shared Error Bus:
const error = v.useStream<Error>()
const cats = search
.throttle(300)
.awaitAll(
(q) => m.request(`/api/cat/search?q=` + q),
error,
);
const dogs = search
.throttle(300)
.awaitAll(
(q) => m.request(`/api/dogs/search?q=` + q),
error,
);
If you create a stream via merge
, or lift
or combine
you are dealing with
a combined stream.
These streams are derived from some combination of other streams.
In traditional mithril.js, if one of the dependencies of that combined stream ends, then the combined stream does not end. If other dependencies continue to emit the derived stream continues to emit as well.
In bacta
instead we end derived streams when any of the dependencies end. This
behaviour aligns with flyd
(from which mithril streams were inspired), but
also most other applicative's and functors.
This also makes component scoped streams much safer, as you can combine a local scoped stream with a global stream and know you won't have the derived stream continuing to emit once your component unmounts.
In the past I advocated for streams and closure components to be added to mithril. Closure components seem to have been generally celebrated, but it seems the community is not so convinced that streams are actually necessary.
Even the core team will use it occasionally for small parts of their app, but not much more.
My belief is that the source of this view is due to mithril not taking full advantage of streams in its API. If a framework really embraces reactivity then the streams almost disappear into the overall architecture.
Thankfully, recently there's been a surge of interest in signals and reactivity. React now has many mainstream competitor frameworks that have streams at the core of their API including:
It makes sense the industry is moving in this direction. It is much faster to only update the exact dom property that needs to change in the DOM when that property changes. When we know what changed, we don't need to diff anything to update the DOM.
However, I think VDOM is still incredibly convenient and fast. Throwing VDOM away for a fully reactive view tree seems like a mistake to me.
VDOM is great at creating your initial layout or reconciling a major layout change. While streams in the view are good for updating the values within a layout as it remains largely static. Using both models works surprisingly well.
Think of it like progressive enhancement, we fallback to VDOM when we can't reactively update the view.
Mixing and matching these two paradigms gives you full access to granular reactivity (when it is called for) without needing to jettison the simplicity of mithril.
That's exactly what bacta
does.
The long term play for bacta is to use toto as our state management query language. But to give toto time to breathe and develop as a language, we have a hacky interim typescript API that allows us to simulate what it would be like to have toto with an LSP before we actually do.
We say "hacky", only because it internally relies upon new Function(...)
and JS expressions written as template literals. But we already use this store API in multiple projects, and it relatively stable and already very capable.
So, it is safe to use the store API, but be aware, eventually you'll need to replace all your queries with some toto expressions. The hope is though that we can automate that with some AST scrubbing when the time comes.
Bacta stores operate on reactive sets. You define the reactive set by specifying what data you are interested in via a query.
You can then read/write any data at that query. You can also subscribe to changes to the results of that query.
Let's use a Todo MVC like application as an example.
type Todo = { id: string, text: string, completed: false }
const $ = v.useStore('myStore', v.useStream({
todos: [] as Todo[],
}))
const todos$ = $.prop('todos')
todos$.get.map( x => {
// logs any time the store is changed
console.log('todos$ changed', x)
})
todos$.get()
//=> []
todos$.set([{ id: '1', text: 'Buy Milk', completed: false}])
//=> [ { id: '1', ... } ]
todos$.update(
xs => xs.concat({ id: '2', text: 'Buy Bread', completed: false })
)
//=> [{ id: '1', ... }, { id: '2', ... }]
We can further focus our queries, e.g. let's query all the incomplete todos.
const $incomplete =
todos$
.where`
x => !x.completed
`
This new filtered store as the same API as the root store, so we can .get
/ .set
/ .update
etc.
Let's mark one of the todos as complete as check that same todo is not in our $incomplete
set.
// get the first todo
const first$ = todos$.prop('0')
// check both todos are in $incomplete
$incomplete.get()
// [{ id: '1', ... }, { id: '2', ... }]
// complete the first todo
first$.prop('completed').set(true)
// now $incomplete has 1 less todo in it
$incomplete.get()
// [{ id: '2', ... }]
Let's try a bulk edit. We want to select multiple todos and mark them all complete/incomplete in unison.
To focus on each item in a list, we use .unnest()
to focus on each item instead of the list of items:
store
.prop('todos') // [{ id: '1', ... }, { id: '2', ... }]
.unnest() // { id: '1', ... } , { id: '1', ... }
By default when you call .get
we only return the first item in a set
store
.prop('todos') // [{ id: '1', ... }, { id: '2', ... }]
.unnest() // { id: '1', ... } , { id: '1', ... }
.get()
// { id: '1', ... }
But you can view all items in the set as a list via .getAll
We can now mark them all as incomplete:
store
.prop('todos') // [{ id: '1', ... }, { id: '2', ... }]
.unnest() // { id: '1', ... } , { id: '1', ... }
.prop('completed') // true, false
// mark all as completed=false
.set(false)
We could also update all the todo text in one go:
store
.prop('todos')
.unnest()
.prop('text')
.update( x => x.toUpperCase() + '!!!' )
Finally we can join stores/streams together reactively.
Let's update our store to have a property that stores ids of rows we have selected.
const $ = v.useStore('myStore', v.useStream({
selected: string[],
todos: [] as Todo[],
}))
const $selected = store.prop('selected')
const $todos = store.prop('todos')
// add another todo
$todos.update(xs => xs.concat({ id: '3', ... }))
// select both todos(id=1,2) but not todos(id=3)
$selected.set([1,2])
Now we can focus on just the todos that are selected:
const $selectedTodo =
$todos
.unnest()
.where`
x => ${$selected}.includes(x.id)
`
This new $selectedTodo
store will update if any of the todos
change, or if the list of selected
ids change.
We can now edit only the selected todos:
const toggleSelectedTodos = () =>
$selectedTodo.prop('completed').update( x => !x )
.prop
.get
.set
.update
.patch
Uses an impervious proxy to allow you to mutate an object and have your changes applied immutably to the store.
.unnest()
.where
The store works with typescript out of the box, but you may run into problems when using predicates against unions. Typescript will not know you have eliminated a case in a where
clause. In this case feel free to use as NewType
etc.
import * as T from 'sum-type'
const Auth = T.type('Auth', {
LoggedOut: (_: any) => _,
Refresh: (_: any) => _,
LoggedIn: (_: {
auth_token: string,
user_id: string,
}) => _,
})
const store = v.useStore('example', v.useStream({
auth: Auth.Refresh({}) as T.Instance<typeof Auth>,
}))
const $loggedIn =
(
store
.prop('auth')
.where`x => x.tag === 'LoggedIn`
// tell typescript this is a specific case of the union
as bacta.Store<T.Instance<typeof Auth.LoggedIn>>
)
// now we can access the auth_token without getting a type error
.prop('value')
.prop('auth_token')
We hope toto's eventual LSP will help us avoid type assertions.
where
?This is (at least) the 5th evolution of the store API, and we have tried a lot of API variations. When combining predicates with other reactive streams/stores we need a reliable way to know which dependencies are being referenced.
If we skipped the template literal and used arrow functions, we cannot know what dependencies were referenced without relying on tricks like injecting proxies or using signals/reference counting. You also run into interesting but annoying edge cases regarding referencing non reactive values in closures.
In a previous version of the store API we used proxies but gave up on it as we ran into lots of corner cases with typescript and value equality in predicates.
We also tried to use our own fork of S.js which is what Solid.js (and others) use. While very cool in theory we've found signals to be another dead end for us, which may be surprising because of how much other frameworks are moving in this direction. (We'll definitely be writing about this in depth at some point.)
Template literals allow us to avoid all of these problems. The downside is we are reverting to a pre-ESLint, stringly typed world, but the benefits outweigh the problems in our view.
We also already use template literals for other embedded languages such as css
and sql
.
Finally, when we eventually use toto in bacta, we'll be doing so via template literals anyway and it should be an easier transition because closure references were never possible.
This is technically a breaking change as this only impacts hyperscript elements (not components) and functions as children were never supported for normal HTML elements.
The only way this would impact you is if you are manually modifying the result (the VTree) of executed hyperscript.
Because we use functions as children as thunks, we wrap them in a special internal component called ThunkChild
. This prevents them from re-running every render.
Additionally, if you ever inspected vnode.children
on the resulting vtree and assumed the value was the exact function you passed to the component, then your component will break too.
We don't like that this is a breaking change. But it is so important for Bacta to be able to create thunks in the view that it is a trade off we have to make.
The only legitimate case I can imagine where this would be an issue is a web component that accepts a function as a child element. But I do not know if that is even supported.
Note, none of this affects components. We never touch vnode.children
on a component. It is completely up to the component author to manage their components children API.
bacta
extends the mithril/stream
API, unfortunately the only way to do that
is to have our own version of the stream library. To avoid having competing
stream graphs in your app it is important you replace any imports of
import Stream from 'mithril/stream'
with import { Stream } from 'bacta'
.
bacta/stream
is an API superset of the mithril/stream
API but they do differ
in behaviour ever so slightly.
If any dependency stream ends it will also end any child streams including
combinations of streams using operators such as combine
, merge
, lift
etc.
mithril-stream
will not end a combined stream if one of the dependencies ends.
If you combine a global stream with a scoped stream in bacta the derived stream will never emit after the scoped stream ends even if the global stream continues to emit.
At Harth we are only just starting to use this library instead of our internal fork of mithril. We intend to migrate all projects to this to ensure we're eating our own dogfood. We want to share our own opinionated view on mithril with the community as we think there's lots of exciting new terrain here, but we deliberately avoid certain parts of mithril's API and therefore there is a high probability that there will be unsurfaced bugs as a result.
For example, we never use this
bindings, and we do not attempt to preserve this
bindings in our hyperscript and component wrappers. We make no effort to support pojo or class variants of mithril components as we only use closure components. We do not use route resolvers, the list goes on.
While bacta is designed to be a superset of mithril, its primary mission is to be a useful library for projects at harth, so will only aim to support patterns that we will actually use on those products.
FAQs
Mithil.js but reactive
The npm package bacta receives a total of 35 weekly downloads. As such, bacta popularity was classified as not popular.
We found that bacta demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Bun 1.2 enhances its JavaScript runtime with 90% Node.js compatibility, built-in S3 and Postgres support, HTML Imports, and faster, cloud-first performance.
Security News
Biden's executive order pushes for AI-driven cybersecurity, software supply chain transparency, and stronger protections for federal and open source systems.
Security News
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.