MCPcopy Index your code
hub / github.com/AlexGalays/kaiju

github.com/AlexGalays/kaiju @0.28.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release 0.28.1 ↗ · + Follow
770 symbols 1,585 edges 71 files 113 documented · 15% updated 7y ago0.28.1 · 2017-12-12★ 88
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Kaiju

logo

kaiju is a view layer used to build an efficient tree of stateless/stateful components and help you manage that tree data.

  • Data management (local/global/inter-component/intra-component) is unified via stores (FSM)
  • Fast, thanks to snabbdom, aggressive component rendering isolation (a key stroke in one input component should not re-evaluate the whole app) and async RAF rendering
  • Changes can easily be animated (also thanks to snabbdom)
  • Global and local state can optionally use Observables for greater composition
  • No JS class / this nonsense
  • Tiny size in KB
  • Comes with useful logs
  • First class support for typescript with a particular attention paid to type safety

Content

Components: step by step guide

kaiju adds the concept of encapsulated components to snabbdom's pure functional virtual dom. Standard Virtual nodes and components are composed to build a VNode tree that can scale in size and complexity. A VNode is what you get when calling snabbdom's h function for instance.

A component is simply a function that takes an option object as an argument and returns a VNode ready to be used inside its parent children, i.e, this is a valid array of VNodes:

[
  h('div'),
  myComponent({ someProp: 33 }),
  h('p', 'hello')
]

Note: typescript will be used in the examples, however the library also works just fine with javascript.

0) We start with a stateless "component"

function button() {
  return h('button')
}

1) For comparison sake, here is the simplest stateful component definition one can write:

import { Component, h } from 'kaiju'

export default function button() {
  return Component({ name: 'button', initState, connect, render })
}

function initState() { return {} }

function connect() {}

function render() {
  return h('button')
}

Now, that isn't terribly useful because we really want our component to be stateful, else we would just use a regular VNode object.

2) Let's add some state, and make it change over time:

import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'


export default function() {
  return Component<{}, State>({ name: 'button', initState, connect, render })
}

interface State {
  text: string
}

function initState() {
  return { text: '' }
}


const click = Message('click')


function connect({ on }: ConnectParams<{}, State>) {
  on(click, () => ({ text: 'clicked' }))
}


function render({ state }: RenderParams<{}, State>) {
  return h('button', { events: { click } }, state.text)
}

Now we created a Message named click that is locally sent to our component whenever the user clicks on the button. We handle that message in connect and return the new state of our component. The component will then redraw with that new state.

Using explicit Messages instead of callbacks to update our state brings consistency with other kinds of (external) state management and makes state debugging easier since messages can be traced and logged (see logging).

In the above code, on(click) is in fact a shortcut for on(msg.listen(click)).

Here's the longer form:

function connect({ on, msg }: ConnectParams<{}, State>) {
  on(msg.listen(click), () => ({ text: 'clicked' }))
}

What msg.listen(click) returns is an Observable that emits new values (the payload of each message) every time the message is sent. This is very useful because observables can easily be composed:


function connect({ on, msg }: ConnectParams<{}, State>) {
  const clicks = msg.listen(click).debounce(1000)

  on(clicks, () => ({ text: 'clicked' }))
}

Now, the state is only updated if we stopped clicking for 1 second.

We could also decide to just perform a side effect, instead of updating the component's state. When performing side effects (void/undefined is returned) the component is not redrawn:


function connect({ on, msg }: ConnectParams<{}, State>) {
  const clicks = msg.listen(click).debounce(1000)

  on(clicks, _ => console.log('clicked!'))
}

Our component now has an internal state and we know how to update it. But it's also completely opaque from the outside! In a tree of VNodes, parents must often be able to influence the rendering of their children.

3) For that purpose, we introduce props:

import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'


export default function(props: Props) {
  return Component<Props, State>({ name: 'button', props, initState, connect, render })
}

interface Props {
  defaultText: string
  paragraph: string
}

interface State {
  text: string
}

function initState(initProps: Props) {
  return { text: initProps.defaultText }
}


const click = Message('click')


function connect({ on }: ConnectParams<Props, State>) {
  on(click, () => ({ text: 'clicked' }))
}


function render({ props, state }: RenderParams<Props, State>) {
  return (
    h('div', [
      h('button', { events: { click } }, state.text),
      h('p', props.paragraph)
    ])
  )
}

Now our parent can render the component with more control: It can set the default text that should be displayed initially, but also directly sets the paragraph text of the p tag.

When composing components, you must choose which component should own which piece of state. Disregarding global state for now, local state can reside in a component or any of its parent hierarchy.

4) Let's see how we can move the previous button text state one level up, so that the component parent can directly change that state:

import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'


export default function(props: Props) {
  return Component<Props, {}>({ name: 'button', props, initState, connect, render })
}

interface Props {
  text: string
  paragraph: string
  onClick: Message.OnePayload<Event>
}


function initState() {
  return {}
}


const click = Message('click')


function connect({ on, props, msg }: ConnectParams<Props, State>) {
  on(click, event => msg.sendToParent(props().onClick(event)))
}


function render({ props, state }: RenderParams<Props, {}>) {
  return (
    h('div', [
      h('button', { events: { click } }, props.text),
      h('p', props.paragraph)
    ])
  )
}

We now delegate and send a message to our direct parent component so that it can, in turn, listen to that message from its connect function and update its own state. Note: The child component could send the same Message to its parent (delegation) but we choose to go with an explicit onClick property to increase semantics, cohesion and typesafety.

At this point, the component is no longer stateful and providing it didn't have any other state, should probably be refactored back to a simple function returning a VNode:


interface Props {
  text: string
  paragraph: string
  onClick: Message.OnePayload<Event>
}

function button(props: Props) {
  const { text, paragraph, onClick } = props

  return (
    h('div', [
      h('button', { events: { click: onClick } }, text),
      h('p', paragraph)
    ])
  )
}

Finally, if we wanted a generic component we could declare it like so:


export default function<T>(props: Props<T>) {
  return select(props)
}

type Props<T> = {
  items: T[]
  selectedItem: T
  onChange: Message.OnePayload<T>
}

type State = {
  focusedIndex: number | undefined
}

const select = (function<T>() {

  function initState() {
    return { focusedIndex: undefined }
  }

  function connect({}: ConnectParams<Props<T>, State>) {}

  function render({}: RenderParams<Props<T>, State>) {
    return h('ul')
  }

  return function(props: Props<T>) {
    return Component<Props<T>, State>({ name: 'select', props, initState, connect, render })
  }

})()

Observables

kaiju comes with an implementation of observables (also known as streams) so that components can more easily declare how their state should change based on user input and any other observable changes in the application.

Observables are completely optional: If you are more confident with just sending messages around every time the state should update, you can do that too.

The characteristics of this observable implementation are:

  • Tiny abstraction, fast
  • OO style chaining
  • Multicast: All observables are aware that multiple subscribers may be present
  • The last value of an observable can be read by invoking the observable as a function
  • Synchronous: Easier to reason about and friendlier stack traces
  • No error handling/swallowing: No need for it since this observable implementation is synchronous
  • No notion of an observable's end/completion for simplicity sake and since we really have two kinds of observables: never ending ones (global state), and the ones that are tied to a particular component's lifecycle
  • Lazy resource management: An observable only activates if there is at least one subscriber
  • If the observable already holds a value, any subscribe function will be called immediately upon registration

To see observables in action, check the example's ajax abstraction and its usage

import { Observable } from 'kaiju'

const obs = Observable.pure(100).map(x => x * 2).delay(200)

The Observable OO API:

interface Observable<T> {
  /**
   * Subscribes to this observable values and returns a function that may be used to unsubscribe.
   */
  subscribe: (onValue: (val: T) => void) => () => void | void
  /**
   * Gives a debug name to the observable.
   * names are inherited by observable transforms unless a new name is defined downstream.
   */
  named: (name: string) => this

  /**
   * Reads the current value of the observable or returns undefined if no value was ever pushed in the observable.
   */
  (): T | undefined

  /**
   * Delays values until a certain amount of silence has passed.
   * Values in between silence periods are discarded.
   */
  debounce(wait: number): Observable<T>

  /**
   * Delays all values by a fixed time offset
   */
  delay(delay: number): Observable<T>

  /**
   * Creates a new Observable with adjacent repeated values removed.
   * A compare function can optionally be passed to implement user-defined equality instead of strict reference equality.
   * The functions should return true if the two values are equal.
   */
  distinct(compareFunction?: (previousValue: T, currentValue: T) => boolean): Observable<T>

  /**
   * Drops 'count' initial values.
   * Note: This can also be used to drop the initial value when subscribing to an Observable, if it had seen a value previously.
   */
  drop(count: number): Observable<T>

  /**
   * Filters the values of this observable.
   */
  filter<T>(predicate: (t: T) => boolean): Observable<T>

  /*
   * Maps and flattens this observable then only publish values from the observable mapped last.
   */
  flatMapLatest<B>(mapFn: (t: T) => Observable<B>): Observable<B>

  /**
   * Maps the values of this observable.
   */
  map<B>(mapFn: (t: T) => B): Observable<B>

  /**
   * Partitions this observable into two observables based on a predicate
   */
  partition<T>(predicate: (value: T) => boolean): [Observable<T>, Observable<T>]

  /**
   * Groups values in fixed size blocks (of size 2) by passing a "sliding window" over them.
   * An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
   * then a size of 2 for subsequent values.
   * The newest value is always found at the index 0 of the Array for convenience and type safety.
   */
  sliding2(): Observable<[T, T | undefined]>

  /**
   * Groups values in fixed size blocks by passing a "sliding window" over them.
   * An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
   * then an increasing size of max (maxWindowSize parameter) for subsequent values.
   * The newest value is always found at the index 0 of the Array for convenience (...rest parameters).
   */
  sliding(maxWindowSize: number): Observable<Array<T>>

  /**
   * Delays values so that values are produced at most once per every 'time' milliseconds
   */
  throttle(time: number): Observable<T>
}

The Observable static API:

```ts

interface ObservableObject { /* Creates a new Observable / (activate?: (ad

Extension points exported contracts — how you extend this code

Props (Interface)
* @deprecated. This was used to allow clients to pass `ref` and `key` * to `createElement`, which is no longer nece
benchmark/react/react.d.ts
IRunnable (Interface)
Partial interface for Mocha's `Runnable` class.
test/typings/mocha.d.ts
Assigned (Interface)
An Assigned VNode is a node that went through the patching process and got assigned a DOM Element
src/main.d.ts
ClassNames (Interface)
(no doc)
example/typing/global.d.ts
InitData (Interface)
(no doc)
example/src/common/model/initData.ts
Props (Interface)
(no doc)
benchmark/kaiju/index.ts
RegisterMessages (Interface)
(no doc)
src/store/index.d.ts
Observable (Interface)
(no doc)
src/observable/index.d.ts

Core symbols most depended-on inside this repo

n
called by 541
benchmark/react/index.js
o
called by 204
benchmark/react/index.js
r
called by 102
benchmark/react/index.js
toBe
called by 100
test/typings/expect.d.ts
call
called by 73
example/src/common/util/ajax.ts
i
called by 72
benchmark/react/index.js
a
called by 48
benchmark/react/index.js
forEach
called by 48
benchmark/react/react.d.ts

Shape

Function 500
Interface 116
Method 114
Class 40

Languages

TypeScript100%

Modules by API surface

example/public/app.js315 symbols
benchmark/react/react.d.ts94 symbols
test/typings/mocha.d.ts65 symbols
test/typings/expect.d.ts50 symbols
benchmark/react/index.js23 symbols
src/observable/index.d.ts20 symbols
src/lib/render.js19 symbols
benchmark/kaiju/index.js19 symbols
src/main.d.ts15 symbols
test/component.ts12 symbols
example/src/common/util/ajax.ts11 symbols
example/src/common/util/router.ts10 symbols

For agents

$ claude mcp add kaiju \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page