npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

teth

v1.0.39

Published

Functional, reactive, pattern matching based, centralized state tree, open source JS library.

Downloads

62

Readme

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