Never Panic Much

    teth

    1.0.39 • Public • Published

    TETH

    teth

    Library for application development: minimalist, functional, reactive, pattern matching, single immutable state tree. Teth-based applications are written in T, a JavaScript-DSL.

    Learn about the reasons behind Teth.

    example- and starter-project teth-todo

    Todo app implemented in teth and T. Provides a best practice example of how to structure an app with teth.

    Todo in teth and T, structured using actions and reducers. It's quite simple to use Teth like you would do with React and Redux.

    TOC

    T

    A minimal functional set of enhancements to JavaScript forming a domain specific language: T is functional, messaging provides robustness, entry conditions minimise need for conditionals and raise expressiveness.

    compatible

    Any JavaScript library can be used from T. Calling into T from JavaScript is the same as sending messages inside pure T.

    object-oriented-design and functional decomposition

    T and teth achieve object-oriented-design and functional decomposition through alternative means, for significant reasons. The following enumeration represent a translation table:

    1. classes

    1. To achieve separation of concerns use computational contexts.
    2. Model your "internal state" as part of the central state tree.
    3. The notion of "types" is redundant in T.

    2. inheritance

    1. T is purely functional.
    2. Logic is expressed as functions with entry conditions.
    3. Logic is grouped by computational contexts and not scattered throughout inheritance trees.

    3. calling a method/function

    1. In T the basic means of communicating is "sending messages."
    2. Messages are JavaScript object literals that express intent to act.
    3. T functions are expressed as entry condition (pattern) and handler function.
    4. Entry conditions explicitly check for a specific message-state that has to be fulfilled for the function to be invoked.

    4. mutating instance-state

    1. All state in teth is modelled via the central state tree.
    2. The intent to retrieve & mutate state variables is expressed explicitly as T middleware.

    All those points taken together have profound positive effects on speed of development, accuracy of abstraction and robustness of resulting application.

    define(...)

    import { define } from 'teth/T'
    define({ key: 'Enter' }, event => {
      // Process the user input on enter
    })
    define('key: Escape', event => {
      // Dismiss the user input on escape
    })

    define(<pattern>, [<middleware>], <function>) defines T functions that can be invoked by sending messages via the computation context they are defined in.

    • <pattern> is an object literal (or string representation thereof) and serves as function description and entry-condition at the same time. <pattern> is always a subset or equivalent of the message received.
    • [<middleware>] is an optional middleware. See cestre to get state and perform state mutations.
    • <function> is the handler function you provide. It's called with the <message> and any additional argument provides my the middleware.
    • multiple define(...) with the same <pattern> are used as application-wide event hub, together with circular(...) to send messages.

    conceptual discussion

    T function definitions describe entry-conditions. Messaging further helps with behavioral decomposition of intent. When refactoring T code, and patterns as well as messages are adjusted, function description automatically is on par with implementation.

    Using cestre (see below) further strengthens expressiveness in intent.

    send(...) | invoke(...)

    import { define, send } from 'teth/T'
    define('key: Enter', event => {
      // Process the user input on enter
    })
    define({ key: 'Escape' }, event => {
      // Dismiss the user input on escape
    })
    send({ key: event.key, value: event.target.value})
      .then(result => {
        // process result
      })
      .catch(error => {
        // handle error
      })

    send|invoke(<message>) -> <pipe> sends T messages via the computation context it is used from. The first T-function (defined by define(...)) that matches the properties of <message> will be invoked. The return value is resolved as pipe if not provided as a thenable (pipe, Promise, Q, etc.).

    • <message> is an object literal. A message carries properties representing means of association, as well as properties representing values and data models.

    • <pipe> is the return value of sending a message. Even if the handler function is not explicitly returning a <pipe>, T will resolve the return value with a <pipe>.

    send.sync(...)

    import { h1 } from 'teth/HTML'
    import { define, send } from 'teth/T'
    define('render: header', msg => {
      return h1('.header-1').content('TETH')
    })
    send.sync('render: header')

    send.sync(<message>) -> <pipe> sends synchronous messages via the computation context. T will not resolve the handler function's return value with a <pipe>.

    • Used for T-functions that must return immediately, like during rendering teth/HTML.
    • Note: use this way of calling T-functions for exceptional cases only. Most of your code should be handled in asynchronous manner in order to reduce possible future refactoring costs.

    message and pattern conventions

    Because functions are defined with patterns (entry-conditions), messages play a leading role in T. It's important to choose a reasonable convention and stick to it throughout the application. Splitting your code-base into module-components by using computational contexts helps with keeping messages simple and local.

    circular(...)

    import { define, circular } from 'teth/T'
    // in component-alpha.js
    define('type: route, cmd: change', msg => { /* ... */ })
    // in component-beta.js
    define('type: route, cmd: change', msg => { /* ... */ })
    // in component-gamma.js
    define('type: route, cmd: change', msg => { /* ... */ })
    // in main.js & on route change:
      circular({type: 'route', cmd: 'change', route: routeIdentifier})
        .then(results => {
          // process results
        })
        .catch(error => {
          // handle error
        })

    circular(<message>) -> <pipe> sends circular T messages via the computation context it is used from. All T-functions (defined by define(...)) that match the properties of <message> will be invoked. The return values will be collected in an array and returned as pipe.

    • Behaves like send(...), except it invokes all matching function definitions in the given context. Return values are resolves with an array.

    string representation of messages and patterns

    The functions define(...), send(...), circular(...), route(...) (and others) accept string representations (jsonic) of messages and patterns (object literals), e.g.

    'role: login-event, cmd: authenticate'
    // is equivalent to:
    { role: 'login-event', cmd: 'authenticate' }

    context(...)

    // backbone send and context functions:
    import { send, circular, context } from 'teth/T'
    // create component-discrete versions of define, send and circular:
    const ctx = context('my-component')
    // backbone communication with other components:
    send(/* ... */)
    circular(/* ... */)
    // invocations within this computational context only:
    ctx.define(/* ... */)
    ctx.send(/* ... */)
    ctx.circular(/* ... */)

    context([<name>]) -> <discrete-context> gets, if necessary creates, discrete named computation contexts to insulate components/services.

    • If the <name> attribute is omitted an unnamed context is created.

    • If a context is supposed to be reused in several source-code files, using a named context is recommended.

    • The 3 main functions of T define(...), send(...), circular(...) imported directly from 'teth/T' belong to the backbone computation context.

      • The backbone context forms a communication channel between components and services.
    • Discrete computation contexts provide separation of concern and encapsulation; they represent means to isolate components and services from each other.

    • context(<name>) invoked several times from completely disconnected parts of the application always returns the same context. Thus providing great testability.

    init(...)

    import remote from 'teth/init'
     
    init({
      renderPattern: 'render: app',
      state: {
        activeRoute: 'all',
        newItemText: '',
        itemEdited: null,
        todoItems: [
          {
            text: 'buy bananas',
            isCompleted: false,
            id: auid()
          },
          // ...
        ]
      },
      selector: '.todoapp'
    })

    init(<options>) initialises a Teth-app.

    • <options> is an object literal that must contain the following properties:
      • renderPattern: String|Object, is used to generate the message that is sent on state tree changes.
      • state: Object, is the initial state tree.
      • selector; String, the CSS selector of the element at which Teth is patching the app into the DOM.

    For a full initialisation example see latest version of Teth-Todo (frontend/src/main.js).

    remote(...)

    Client-side RPC invocation for Teth.

    import remote from 'teth/remote'
    // ...
    remote.init('/api') // backend API route
    // ...
    define('init: app', state.mutate('todoItems'), msg => {
      // RPC invocation to retrieve all todo items and update the state tree
      return remote('retrieve: all-todo-items').then(items => [items])
    })

    remote.init(<backend-api-route>) initialises a the remote backend endpoint.

    • <backend-api-route> a string representing the backend API route. I.e. /api.

    remote(<message>) -> <pipe> sends the <message> to the remote backend endpoint and returns a pipe resolving with the result.

    • <message> is a object literal representing the message to be send. In this respect behaves much like send(...).

    Alternative Invocation:

    remote(<context-name>, <message>) -> <pipe> sends the <message> to the remote backend endpoint and there to the context specified by <context-name> and returns a pipe resolving with the result.

    • <context-name> is the name of the named context addressed by the remote message.
    • <message> see above.

    valet(...)

    Server-side RPC adapter for Teth. Connects T with server side endpoint. Makes transparent RPC calls from client side T possible.

    const http = require('http')
    const valet = require('teth/valet')
    const { define } = require('teth/T')
     
    http.createServer(valet('/api')).listen(3030)
     
    define('retrieve: all-todo-items', msg => {
      return /* allTodoItems */
    })

    valet([<route>]) initialises and returns a request/response handler function compatible with NodeJS and Express.

    • <route> if provided filters incoming request. Otherwise returns a 404 on NodeJS or invokes next(..) on Express.

    cestre

    Centralised state tree expressed as T middleware. Inspired by the concept of single immutable state trees.

    initialise state tree

    // main.js
     
    // Init centralised state tree via cestre itself
    // NOT RECOMMENDED
    import cestre from 'teth/cestre'
     
    cestre.init({
      bicycles: {
        muscle: [13, 21, 35],
        electric: [39, 43, 97]
      }
    })
     
    // Or via teth/init
    // RECOMMENDED
    import init from 'teth/init'
     
    init({
      ...
      state: {
        bicycles: {
          muscle: [13, 21, 35],
          electric: [39, 43, 97]
        }
      },
      ...
    })

    retrieve and mutate state models

    // component.fcd.js
    const state = cestre()
    // Define interest in specific state in T function definition
    define('render: one, from: bicycles.muscle',
      state('bicycles.muscle'), // interest for state at keypath "bicycles.muscle"
      (msg, muscle) => {
        // ...
      })
    send('render: one, from: bicycles.muscle')
     
    // component.ctx.js
    const state = cestre()
    // Define intent to mutate state in T function definition
    define('add: one, to: bicycles.muscle',
      state.mutate('bicycles.muscle', 'bicycles.electric'), // intent to mutate states at specified keypaths
      (msg, muscle, electric) => {
        // ... perform mutations
        // return values must be array containing the mutated states in exact the same order as received
        return [muscle, electric] // patched if not instance-equal with received
      })
    send('add: one, to: bicycles.muscle')

    cestre.init(<initial-state>) -> <state-fn> initialize the single immutable state tree function.

    • <initial-state> is an object literal representing the full initial state of the complete application.

    cestre() -> <stateFn> get the state function of the centralised state tree.

    • <stateFn> The state function allows to express interest to retrieve or mutate a state model ...

    stateFn(<key-path-a>, <key-path-b>, ...) create T middleware that hands over the state specified by the provided keypaths.

    • <key-path-a>, <key-path-b>, ... one or many key paths, which resolve to models inside the state tree. The handler of the function definition will be called with:
      1. the original <message> as the first argument
      2. all state models as following arguments

    state mutations

    In a T-function defined with a state.mutate(...) middleware state changes must always be returned as arrays of the state models in exactly the same order and amount as received, message argument omitted. E.g. if the function handler was called with msg, muscle, electric, the return value must be [muscle, electric]. The return values contained in the array must be instance-inequal in order to trigger a redraw event and instance-equal not to.

    So it's wrong to mutate in place, for instance by using push(item). Instead a new instance must be returned:

    // ...
    (msg, muscle, electric) => {
      const item = // ...
      // in case muscle is an array:
      const newMustlePoweredBikesArray = [...muscle, item]
      return [newMustlePoweredBikesArray, electric]
    })

    In the example above only newMustlePoweredBikesArray will be patched into the state tree, because it's instance-inequal to muscle. Additionally a change event is emitted.

    // ...
    (msg, muscle, electric) => {
      const item = // ...
      // in case muscle is an object literal:
      const newMustlePoweredBikesLiteral = Object.assign({}, muscle, { item })
      return [newMustlePoweredBikesLiteral, electric]
    })

    In the example above only newMustlePoweredBikesLiteral will be patched into the state tree, because it's instance-inequal to muscle. Additionally a change event is emitted.

    A middleware can be reused throughout several T function definitions:

    const muscleModels = state.mutate('bicycles.muscle')
    const electricModels = state.mutate('bicycles.electric')
     
    define('add: one, to: muscle-bicycles', muscleModels,
      (msg, musclePoweredBikes) => { /* ... */ return [musclePoweredBikes] })
     
    define('remove: one, from: muscle-bicycles', muscleModels,
      (msg, musclePoweredBikes) => { /* ... */ return [musclePoweredBikes] })
     
    define('add: one, to: electric-bicycles', electricModels,
      (msg, electricPoweredBikes) => { /* ... */ return [electricPoweredBikes] })
     
    define('remove: one, from: electric-bicycles', electricModels,
      (msg, electricPoweredBikes) => { /* ... */ return [electricPoweredBikes] })

    conceptual discussion

    Expressing the intent of state change is enforced and stated clearly at a dominant position in function definition.

    This forms optimal conditions for high maintainability, test-driven design and development, continuous refactoring, as well as separation of concern. In crass contrast to the mainstream approach towards object orientation, where misguided handling of instance variables often leads to an incomprehensible chaos of side-effects and in turn is the main cause for those kind of errors that are devastating on maintainability and extendability.

    pipe

    Merging promises with functional reactive map/reduce (+ debounce and throttle). Pipes run on backpressure and can be used in backends as well.

    // ...
    const readFile = pipe.wrap(fs.readFile)
    const writeFile = pipe.wrap(fs.writeFile)
    // ...
    readFile('./package.json', 'utf8')
      .then(packString => JSON.parse(packString))
      .then(pack => pipe((resolve, reject) => {
        const keys = Object.keys(allScripts)
        return next => {
          if (keys.length) next(keys.splice(0, 1)[0])
          else resolve()
        }
      }))
      .filter(lit => !lit.pack.scripts[lit.key])
      .map(lit => {
        lit.pack.scripts[lit.key] = lit.value
        return lit.pack
      })
      .reduce((r, i) => i)
      .then(pack => JSON.stringify(pack, null, 2))
      .then(packString => writeFile('./package.json', packString))
      .then(() => { /* ... */ })
      .catch(console.error)

    creating pipes with deferrer and generator

    pipe(<deferrerFn>) -> <generatorFn> creates a pipe.

    • <deferrerFn> is a callback that will be called with 2 arguments: <resolveFn> and <rejectFn>.
    • Behaves like it's Promise counterpart.

    <generatorFn> can be returned from the <deferrerFn>.

    • Will be called repeatedly with a <nextFn> until <deferrerFn> resolved or rejected.

    • Every <nextFn> must be called only once with a value each time <generatorFn> is called.

    • So that values are emitted as fast as subsequent consumption is performed.

    • Example of a generator emitting keys of an object literal as fast as subsequent consumers can process:

      pipe((resolve, reject) => {
        const keys = Object.keys(anObjectLiteral)
        return next => {
          if (keys.length) next(keys.splice(0, 1)[0])
          else resolve()
        }
      })

    operators

    .map(<operate-fn>) -> <pipe> .filter(<operate-fn>) -> <pipe> .forEach(<operate-fn>) -> <pipe> .reduce(<operate-fn>) -> <pipe> behave like their array counterparts.

    .reduce(<operate-fn>) -> <pipe> the reduce result is retrieved by chaining a then().

    .then(<operate-fn>) -> <pipe> .catch(<fn>) behave like their Promise counterparts.

    .debounce(<delay>) -> <pipe> continues the stream of operations only after a firing silence of the previous operation of at least <delay> milliseconds.

    .throttle(<delay>) -> <pipe> limits the events coming from the previous operation to firing in the interval of the given <delay>.

    constructor functions

    pipe.resolve(<value>) -> <pipe> returns a pipe that will resolve with the given value.

    pipe.reject(<error>) -> <pipe> returns a pipe that will reject with the given error.

    pipe.all(<Array[Thenable]>) -> <pipe> resolves after all thenables (Promise-compatible asynchronous computations) in the given array did resolve.

    • Passes on an array of results.

    pipe.race(<Array[Thenable]>) -> <pipe> resolves as soon as the first of all the thenables (Promise-compatible asynchronous computations) resolved.

    • Passes on the respective result.

    pipe.from(<Array>) -> <pipe> creates an iterable pipe on which .map(<fn>) .filter(<fn>) .forEach(<fn>) .reduce(<fn>) can be used, from an array of values.

    pipe.wrap(<NodeJS-style-callback>) -> <pipe> wraps a NodeJS style callback function (1st argument error, others results) into a pipe.

    • Will resolve with the given arguments in an array (if more than one), with the result value otherwise.
    • Will reject on error.

    NOT RECOMMENDED: pipe.buffer(<size>) -> <buffer> creates a buffer that keeps maximum the <size> amount of emitted values before the consuming operation is retrieving them. If the consumer is too slow and a <size> is given, values might be omitted. Without a <size> given and a slow consumer the buffer might overflow and crash your application. It's advisable to structure your code so that a buffer is not needed.

    • <buffer>.emit(<value>) emits a value onto the buffer. The value is stored until a pipe consumer retrieves it or it gets pushed from the buffer by reaching the <size> limit.
    • <buffer>.resolve(<value>) resolves the pipe underneath the buffer.
    • <buffer>.reject(<error>) rejects the pipe underneath the buffer.
    • <buffer>.pipe the pipe underneath the buffer.

    route

    teth router is build on T and integrated into cestre. Based on Route-Parser. There are 2 ways of usage which can be intermixed:

    routing by state change (recommended)

    In the example below 4 routes are defined:

    • /# – the base route
    • /#/active – active route based upon base route
    • /#/completed – completed route based upon base route
    • /#/show/:itemId – show item route based upon base route
    // Defining routes
    const mutateRoute = state.mutate('activeRoute') // activeRoute must exist in state tree
    const base = route('/#', mutateRoute, () => [{ show: 'all' }])
    base.route('/active', mutateRoute, () => [{ show: 'active' }])
    base.route('/completed', mutateRoute, () => [{ show: 'completed' }])
    base.route('/show/:itemId', mutateRoute, msg => [{ showItem: msg.params.itemId }])
     
    // Using route-state
    define('render: something',
      state('activeRoute'),
      (msg, activeRoute) => {
        // do something with activeRoute ...
      })

    Each call to route(...) returns a route-function that can be used to define sub-routes extending the one defined.

    route(<description>, <mutation-middleware>, <mutation-handler-fn>) -> <sub-route-fn> is defining a route much alike a state-mutating T-function is defined.

    • <description> describes the URL of the route. The syntax:
    Expression Description
    :name a parameter to capture from the route up to /, ?, or end of string
    *splat a splat to capture from the route up to ? or end of string
    () Optional group that doesn't have to be part of the query. Can contain nested optional groups, params, and splats
    anything else free form literals
    Examples:
     
    /some/(optional/):thing
    /users/:id/comments/:comment/rating/:rating
    /*a/foo/*b
    /books/*section/:title
    /books?author=:author&subject=:subject
    • <mutation-middleware> see cestre state mutations.
    • <mutation-handler-fn> see cestre state mutations. Path parameters are accessible by the property params from the message the handler is called with.
    • <sub-route-fn> a function that can be used to define sub-routes. Behaves the same as route(...).

    routing by messaging

    route(<description>, <pattern>) -> <sub-route-fn> is defining a route for messaging.

    • <description> see above section.
    • <pattern> the pattern literal that will be extended to send route change messages. Path parameters are accessible by the property params from the message the receiving T-function-handler is called with.
    • <sub-route-fn> see above section.

    HTML

    HTML-Tags expressed as JS continuation. Based on Snabbdom.

    For every HTML element a function is exported to create nested virtual DOM elements that are patched into the actual DOM by Snabbdom. The process is triggered by state change events emitted by cestre.

    1. To create an element call a corresponding constructor-function:

      import { div } from 'teth/HTML'
      const virtualDivElem = div('#element-a1.selected')

    A constructor-function can take a CSS selector.

    2. Classes, attributes, styles, event-listeners and content can be added by chaining calls:

      import { div } from 'teth/HTML'
      div('#element-a1')
        .class({'selected', isSelected})
        .attrib({alt: 'The first element'})
        .on({click: ev => processClickOn('element-a1')})
        .content('Here comes the content')

    3. Calls to attrib(), class(), style(), on() and hook() must be called with object literals:

      /* ... */.class({selected: isSelected})
               .class({hidden: isHidden, dark: isDark})

    Note: All values from calls to the same method will be merged: The example above assigns all 3 classes (selected, hidden, dark) to the virtual DOM element.

    4. Continuation methods:

    • attrib({key: value [, ...]}) Set attributes of the DOM element.
    • class({key: value [, ...]}) Set classes of the DOM element. Keys are names of classes, values must correspond to boolean values. Evaluation to true will cause the class name to be added.
    • content(<string> * N | <Virtual-DOM-Element> * N | <Array<Virtual-DOM-Elements>) This function is the only one that doesn't support key-value-pairs. Attributes can be strings, virtual DOM elements or an array of virtual DOM elements.
    • on({key: value [, ...]}) Set event callbacks on the DOM element. Keys are names of events, values are callback functions. Every virtual DOM element needs it's own callback function for a given event type. Sharing a directly assign callback between DOM elements is not supported. Do call shared callbacks indirectly from within in directly attached callback function.
    • style({key: value [, ...]}) Set styles of the DOM element. Keys are style-names in camel-case (not in hyphen-notation as found in CSS, and not in Pascal-case) as usual when setting styles on DOM elements from JavaScript.
    • hook({key: value [, ...]}) Set callbacks for rendering hooks. The following hooks exist:
    Name Triggered when Arguments to callback
    pre the rendering process none
    begins
    init a vnode has been added vnode
    create a DOM element has been emptyVnode, vnode
    created based on a vnode
    insert an element has been vnode
    inserted into the DOM
    prepatch an element is about to be oldVnode, vnode
    rendered
    update an element is being oldVnode, vnode
    updated
    postpatch an element has been oldVnode, vnode
    rendered
    destroy an element is directly or vnode
    indirectly being removed
    remove an element is directly vnode, removeCallback
    being removed from the
    DOM
    post the render process is none
    done

    match(...)

    The pattern matching facility on top of which define(...), send(...), circular(...) and context(...) are built. It can be used as an alternative for conditionals.

    import { match } from 'teth/T'
    // ...
    match(event)
      .define({ key: 'Enter' }, event => {
        // Process the user input on enter
      })
      .define({ key: 'Escape' }, event => {
        // Dismiss the user input on escape
      })
      .do()

    A matcher instance can be used to store and reuse conditional computation trees.

    const matcher = match()
      .define({ key: 'Enter' }, event => /* Process the user input on enter */)
      .define({ key: 'Escape' }, event => /* Dismiss the user input on escape */)
    // ...
    matcher.do(event)

    If a matcher is triggered with a literal it does not know, it trows an error. Until ...

    const matcher = match()
      .define({ key: 'Enter' }, event => /* Process the user input on enter */)
      .define({ key: 'Escape' }, event => /* Dismiss the user input on escape */)
      .unknown(msg => /* Handle unknown { role: 'provoker', cmd: 'should-throw' } */)
    // ...
    matcher.do('role: provoker, cmd: should-throw')

    ... error handling is done by attaching an unknown-handler.

    Only possible with JavaScript™ ;)

    Or as Douglas put it:

    JavaScript: The World's Most Misunderstood Programming Language

    Install

    npm i teth

    DownloadsWeekly Downloads

    55

    Version

    1.0.39

    License

    MIT

    Last publish

    Collaborators

    • jaqmol