xinjs.net | docs | github | npm | cdn
In particular, this means that you can do your state management anywhere, including on either side of the "browser" divide in Electron / nwjs and similar applications. You can also efficiently implement "stateful-stateless" servers by sending mutations to complex state to the server and use GraphQL-like queries to "shape" the response from a service.
xinjs
doesxinjs
tracks the state of objects you assign to it using paths
allowing economical
and direct updates to application state.
import {xin, observe} from 'xinjs'
xin.app = {
prefs: {
darkmode: false
},
docs: [ { id: 1234, title: 'title', body: 'markdown goes here' } ]
}
observe('app.prefs.darkmode', () => {
document.body.classList.toggle('dark-mode', xin.app.prefs.darkmode)
})
observe('app.docs', () => {
// render docs
})
xinjs
does not modify the stuff you hand over to it… it just wraps objects
with a Proxy
and then if you use xin
to make changes to those objects,
xinjs
will notify any interested observers.
import {xin, observe} from 'xinjs'
xin.foo = {bar: 17}
observe('foo.bar', v => {
console.log('foo.bar was changed to', xin.foo.bar)
})
xin.foo.bar = 17 // does not trigger the observer
xin.foo.bar = Math.PI // triggers the observer
xin
is designed to behave just like a JavaScript Object
. What you put
into it is what you get out of it:
import {xin} from 'xinjs'
const foo = {bar: 'baz'}
xin.foo = foo
// xin.foo returns a Proxy wrapped around foo (without touching foo)
xin.foo._xinValue === foo
// really, it's just the original object
xin.foo.bar = 'lurman'
foo.bar === 'lurman' // true
// seriously, it's just the original object
foo.bar = 'luhrman'
xin.foo.bar === 'luhrman' // true
It's very common to deal with arrays of objects that have unique id values, so xin supports the idea of id-paths
const app = {
list: [ { id: '1234abcd', text: 'hello world' }, { id: '5678efgh', text: 'so long, redux' ]
}
xin.app = app
console.log(xin.app.list[0].text) // hello world
console.log(xin.app.list['id=5678efgh'] // so long, redux
console.log(xin['app.list[id=1234abcd]') // hello world
xin
about changes using touch()
Sometimes you will modify an object behind xin
's back (e.g. for efficiency).
When you want to trigger updates, simply touch the path.
import {xin, observe, touch} from 'xinjs'
const foo = {bar: 17}
xin.foo = foo
observe('foo.bar', path => console.log(path, '->', xin[path])
xin.foo.bar = -2 // console will show: foo.bar -> -2
foo.bar = 100 // nothing happens
touch('foo.bar') // console will show: foo.bar -> 100
xinjs
provides a type system that allows you to efficiently check types
at runtime. Types are just javascript and can be serialized as JSON, used
as mock data, and so on. The matchType
function tells you exactly what's
wrong with a given value:
const userType = {
name: 'name',
age: 17,
address: {
street: 'somewhere',
city: 'city',
zipcode: '12345'
}
}
matchType(userType, {
name: 'Juanita Citizen',
age: '17',
address: {
street: '123 Sesame',
zipcode: 10001
}
}) // returns a list of problems...
The ultimate goal of
xinjs
's type system is to allow for a single source of truth for types, so that one simple JavaScript declaration gives you:
- auto-completion when writing code
- mock data — E.g.
mock("# int(0,10]")
would produce a whole number >0 and <= 10 (This may require extra syntax to provide possible values for some types.)- run-time type-checking
- filtering of objects (see Filter below) based on types
Because all types are serializable JavaScript, this also allows for self-documenting services, service requests cab easuky specify a "shape filter" for responses, and service versioning.
The exact response from matchType
in this example is:
[
'.age was "17", expected number',
'.address.city was undefined, expected string',
'.address.zipcode was 10001, expected string'
]
matchType
supports very specific types, including optional values, and despite this
all types are "just javascript" and serializable as JSON.
E.g. object properties can be specified as optional:
const positionType = {
lat: 0,
long: 0,
'altitude?': 0
}
And numeric values can be restricted to whole numbers or ranges:
const positionType = {
lat: '#number [-90,90]',
long: '#number [-180,180]',
'altitude?': 0
}
const arrayIndexType = '#int [0,∞)'
Enumerations can be specified:
const method = '#enum "HEAD"|"INFO"|"GET"|"POST"|"PUT"|"DELETE"'
And strings can be restricted to regular expressions:
const zipcodeType = '#regexp ^\\d{5,5}(-\\d{4,4})?$'
You can build hardened functions that will throw an error if they receive the wrong input or would produce the wrong output.
import {typeSafe} from 'xinjs'
const unsafeAdd(a, b) => a + b
const safeAdd = typeSafe(unsafeAdd, [0, 0], 0)
If you want to pare down an object to a specific shape, e.g. to optimize bandwidth-usage,
the filter
function lets you use a type
object to extract exactly what you want, and
the way you specify what you want is with the same type objects used by matchType:
filter(1, 17) // 17
filter(1, 'hello') // undefined
filter({x: 0, y: 0}, {x: 1, y: 2, z: 17}) // {x: 1, y: 2}
filter({x: 0, y: 0}, {y: 1, z: 2}) // undefined
Note that you can filter heterogeneous arrays to only include the specified elements.
filter([1], ['this', 4, 'that', 17]) // [4, 17]
filter([], ['this', true, 17]) // ['this', true, 17]
filter(['', 0], ['this', true, 17]) // ['this', 17]
filter([{x: 0, y: 0}], [{x: 1, y: 2}, {lat: 10, long: -30}])
// [{x: 1, y: 2}]
One of the nice things about working with the React toolchain is hot reloading.
xinjs
supports hot reloading (and not just in development!) via the hotReload()
function:
import {xin, hotReload} from 'xinjs'
xin.app = {
...
}
hotReload()
hotReload
stores serializable state managed by xin
in localStorage and restores
it (by overlay) on reload. Because any functions (for example) won't be persisted,
simply call hotReload
after initializing your app state and you're good to go.
hotReload
accepts a test function (path => boolean) as a parameter.
Only top-level properties in xin
that pass the test will be persisted.
To completely reset the app, run localStorage.clear()
in the console.
You'll need to install bun and probably nodejs).
To work interactively on the demo code, use bun dev
.
To build you will need to chmod +x build.command
before running bun pack
.
Caution
xinjs currently builds against ES2022 because its use of private class properties causes rollup to produce bad code. Super annoying and unnecessary. I don't want to waste time coding around bad intermediate code although there's a simple workaround (WeakMaps are the dual of private symbols and don't cause the problem). Let me know if this is a pain point because, as I said, it's an easy if ugly fix.
xinjs
is in essence a highly incompatible update to b8rjs
with the goal
of removing cruft, supporting more use-cases, and eliminating functionality
that has been made redundant by improvements to the JavaScript language and
DOM APIs.
xinjs
is being developed using bun.
bun
is crazy fast (based on Webkit's JS engine, vs. V8), does a lot of stuff
natively, and runs TypeScript (with import and require) directly.
Version | Tag | Published |
---|---|---|
0.1.9 | latest | 2d ago |