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

@microstates/observable

v0.4.0

Published

Observable wrapper for Microstates

Downloads

7

Readme

Observable Microstates

A Microstate presents data and operations that can be performed on that data. A microstate can only ever have one value. Every operations returns a new microstate that's independent from the object that generated it. When you need a stream of values, you can create an observable from a microstate. The created obserable will stream microstate's state, actions and value to the observer.

Here is what that looks like,

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

let ms = microstate(MS.Number, 42)
let observable = from(ms)

observable.subscribe(v => console.log(v))
/**
 * => {
 *  state: 42,
 *  actions: { increment, decrement, set, add, substract }
 *  valueOf: function
 * }
 **/

When you subscribe to the observable, the observable will immidately push an object to the observer. This object gives you the initial state and actions that can be invoked to next value to the observer.

The value object has the following structure:

{
  /**
   * State generated from the microstate. Same as `ms.state`
   */
  state: ,
  /**
   * These are transitions of the microstate that are wrapped in closure.
   * Invoking an action will push the result of the operation through the stream.
   */
  actions: {
    ...
  },
  /**
   * Return the value that was used to create the microstate
   */
  valueOf: Function
}

Value and valueOf

Value is a plain JavaScript object used to construct a microstate. From this value, microstates library builds state and actions. The value is treated safely, meaning that the original object that's passed into a microstate constructor will never be modified. Any operations performed on the value will create a copy of the original value.

Value can be any privitive type that can be serialized with JSON.stringify and parsed with JSON.parse.

You can use valueOf function that's included in the stream value to retrieve the value bound to state and actions.

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

class Address {
  street = MS.String
  city = MS.String
  state = MS.String
  country = MS.String
}

class Person {
  name = MS.String
  address = Address
}

let ms = microstate(Person, { name: 'Homer Simpson', { city: 'Springfield' } })

let observable = from(ms)

observable.subscribe(v => console.log(v.valueOf()))
// => { name: 'Homer Simpson', { city: 'Springfield' } }

State

State is deserialized value of a microstate. State is a tree structure of instantiated objects, primitive values and computed properties. It's designed to make it easy to work with values that are derived from microstate's value.

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

class BookReview {
  reviewer = Person
  get hasReviewer() {
    return this.reviewer.name !== ''
  }
}

let ms = microstate(BookReview, {})
let observable = from(ms)

observable.subscribe(v => console.log(v.state))
/**
 * BookReview {
 *   reviewer: Person {
 *      name: '',
 *      address: Address {
 *        street: '',
 *        city: '',
 *        state: '',
 *        country: ''
 *     }
 *   },
 *   hasReviewer: false
 * }

Actions

Actions are microstate transitions that are wrapped in closures that push the result of the transition to the observer. Actions are bound to value of a microstate which means that for every value there is a new set of actions.

We include actions in the value object to ensure that calling an action will always give you the next value. If you invoke actions from a previous value, then your stream will receive value generated from an operation performed on an old value.

Like state, actions are a tree structure. They mirror the structure of the type that was used to build the microstate. This tree structure is evaluated lazily, therefore it does not encure evaluation cost until you read the action.

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

class Book {
  name = MS.String
  pagesCount = MS.Number
  reading = MS.Number
  turnPage(current) {
    return this().pagesCount.increment()
  }
  readAgain() {
    return this().reading.set(1)
  }
}

class BookReview {
  book = Book
  reviewer = Person
  get hasReviewer() {
    return this.reviewer.name !== ''
  }
  get isFinished() {
    return this.book.pagesCount === this.book.reading
  }
}

let ms = microstate(BookReview, {
  book: { name: 'War and Peace', pagesCount: 1000, reading: 999 },
  reviewer: { name: 'Taras Mankovski' }
})

let observable = from(ms)
let last

observable.subscribe(v => {
  console.log(v)
  last = v
})
/**
 * {
 *    state: BookReview {
 *      book: {
 *        name: 'War and Peace'
 *        pagesCount: 1000
 *        reading: 999
 *      },
 *      reviewer: {
 *        name: 'Taras Mankovski',
 *        address: Address {
 *          street: '',
 *          city: '',
 *          state: '',
 *          country: ''
 *        }
 *      },
 *      hasReviewer: true,
 *      isFinished: false
 *    },
 *    actions: {
 *      set,
 *      merge,
 *      book: {
 *        merge,
 *        name: {
 *          set,
 *          concat
 *        },
 *        pagesCount: {
 *          set,
 *          increment,
 *          decrement,
 *          add,
 *          subtract
 *        },
 *        reading: {
 *          set,
 *          increment,
 *          decrement,
 *          add,
 *          subtract
 *        }
 *      },
 *      reviewer: {
 *        set,
 *        merge,
 *        name: {
 *          set,
 *          concat
 *        },
 *        address: {
 *          set,
 *          merge,
 *          street: {
 *            set,
 *            concat
 *          },
 *          city: {
 *            set,
 *            concat
 *          },
 *          state: {
 *            set,
 *            concat
 *          },
 *          country: {
 *            set,
 *            concat
 *          }
 *        }
 *      }
 *    }
 * }
 **/

Batched & Chained Actions

You can perform several operations in one by creating batch actions or by chaining actions.

Batched Actions

Batched transitions allow you to give your action a name. When you call a batched transition, it will push one/final value to the stream.

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

class CancellableBookReview extends BookReview {
  cancelReview() {
    return this()
      .reviwer.set(null)
      .book.set(null)
  }
}

let ms = microstate(CancellableBookReview, {
  book: { name: 'War and Peace', pagesCount: 1000, reading: 999 },
  reviewer: { name: 'Taras Mankovski' }
})

let last

observable.subscribe(v => {
  last = v
  console.log(v.valueOf())
})
/**
 *  {
 *    book: { name: 'War and Peace', pagesCount: 1000, reading: 999 },
 *    reviewer: { name: 'Taras Mankovski' }
 *  }
 **/

last.actions.cancelReview()
/**
 *  {
 *    book: null,
 *    reviewer: null
 *  }
 **/

Chained actions

Chained actions allow adhoc operations to be performed on same value. Each action will send a new value to the stream.

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

let ms = microstate(BookReview, {
  book: { name: 'War and Peace', pagesCount: 1000, reading: 999 },
  reviewer: { name: 'Taras Mankovski' }
})

let last

observable.subscribe(v => {
  last = v
  console.log(v.valueOf())
})
/**
 *  {
 *    book: { name: 'War and Peace', pagesCount: 1000, reading: 999 },
 *    reviewer: { name: 'Taras Mankovski' }
 *  }
 **/

last.actions.book.turnPage().book.readAgain()
/**
 *  {
 *    book: { name: 'War and Peace', pagesCount: 1000, reading: 1000 },
 *    reviewer: { name: 'Taras Mankovski' }
 *  }
 *  {
 *    book: { name: 'War and Peace', pagesCount: 1000, reading: 1 },
 *    reviewer: { name: 'Taras Mankovski' }
 *  }
 **/

History

In most cases, you will want the last value that came through the stream because it'll give you the latest version of state and actions. However, since microstates are immutable and the stream gives you a value for every transition, it is possible to restore to a previous state by invoking the set action with value of a previous value.

For example,

import microstate, * as MS from 'microstates'
import { from } from '@microstates/observable'

let ms = microstate(MS.Object, { a: 'a' })
let observable = from(ms)
let values = []

observable.subscribe(v => values.push(v))

values[0].valueOf()
// => { a: 'a' }

values[0].actions.assign({ b: 'b' })

values[1].valueOf()
// => { a: 'a', b: 'b' }

values[1].actions.assign({ c: 'c' })

values[2].valueOf()
// => { a: 'a', b: 'b', c: 'c' }

values[3].set(values[1].valueOf())

values[4].valueOf()
// => { a: 'a', b: 'b' }

This makes it fairly trivial to implement interfaces where you need to restore to a previous value.