remesh

English 中文

npm version Documentation Maintenance License: MIT Twitter: guyingjie129

A CQRS-based DDD framework for large and complex TypeScript/JavaScript applications

Features

Why Remesh?

So, why Remesh? What benefits can it bring to my application?

It depends on whether you will be attracted to the following.

Concepts

A domain is like a component of your application. But not for the UIs, it’s for your business logic.

All related things are encapsuled in the domain.

A domain can have as many resources listed in below as you want.

For any domain, only domain-query, domain-command, domain-event can be exposed to the outside.

domain-state will not be exposed to the outside and can’t be touched directly out of the domain.

For the consumers of any domain.

Installation

# Install remesh and rxjs via npm
npm install --save remesh rxjs
# Install remesh and rxjs via yarn
yarn add remesh rxjs

Usage

You can edit it in stackblitz

Define your domain

// domain.ts
import { Remesh } from 'remesh'

import { interval } from 'rxjs'
import { map, switchMap, takeUntil } from 'rxjs/operators'

type ChangeMode = 'increment' | 'decrement'

/**
 * Define your domain model
 */
export const CountDomain = Remesh.domain({
  name: 'CountDomain',
  impl: (domain) => {
    /**
     * Define your domain's related states
     */
    const CountState = domain.state({
      name: 'CountState',
      default: 0,
    })

    /**
     * Define your domain's related events
     */
    const CountChangedEvent = domain.event<number>({
      name: 'CountChangedEvent',
    })

    /**
     * Define your domain's related commands
     */
    const SetCountCommand = domain.command({
      name: 'SetCountCommand',
      impl: ({}, count: number) => {
        /**
         * Update the domain's state and emit the related event
         */
        return [CountState().new(count), CountChangedEvent(count)]
      },
    })

    /**
     * Define your domain's related queries
     */
    const CountQuery = domain.query({
      name: 'CountQuery',
      impl: ({ get }) => {
        /**
         * Get the domain's state
         */
        return get(CountState())
      },
    })

    /**
     * You can use a command in another command
     */
    const IncreaseCountCommand = domain.command({
      name: 'IncreaseCountCommand',
      impl: ({ get }, count: number = 1) => {
        return SetCountCommand(get(CountState()) + count)
      },
    })

    /**
     * You can use a command in another command
     */
    const DecreaseCountCommand = domain.command({
      name: 'DecreaseCountCommand',
      impl: ({ get }, count: number = 1) => {
        return SetCountCommand(get(CountState()) - count)
      },
    })

    const ChangeCountByModeCommand = domain.command({
      name: 'ChangeCountByModeCommand',
      impl: ({}, mode: ChangeMode) => {
        if (mode === 'increment') return IncreaseCountCommand()
        if (mode === 'decrement') return DecreaseCountCommand()
        return null
      },
    })

    /**
     * Define an event for starting increment or decrement periodically
     */
    const StartEvent = domain.event<ChangeMode>({
      name: 'StartEvent',
    })

    /**
     * Define a command to send event since event can't be sended outside of domain
     */
    const StartCommand = domain.command({
      name: 'StartCommand',
      impl: ({}, mode: ChangeMode) => {
        return StartEvent(mode)
      },
    })

    /**
     * Define an event for stopping signal
     */
    const StopEvent = domain.event({
      name: 'StopEvent',
    })

    /**
     * Define a command to send event since event can't be sended outside of domain
     */
    const StopCommand = domain.command({
      name: 'StopCommand',
      impl: () => {
        return StopEvent()
      },
    })

    /**
     * Define your domain's related effects
     */

    domain.effect({
      name: 'ChangeCountEffect',
      impl: ({ fromEvent }) => {
        return fromEvent(StartEvent).pipe(
          switchMap((mode) => {
            return interval(100).pipe(
              map(() => ChangeCountByModeCommand(mode)),
              // finished when received stop event
              takeUntil(fromEvent(StopEvent)),
            )
          }),
        )
      },
    })

    /**
     * Expose domain resources
     */
    return {
      query: {
        CountQuery,
      },
      command: {
        SetCountCommand,
        IncreaseCountCommand,
        DecreaseCountCommand,
        StartCommand,
        StopCommand,
      },
      event: {
        StartEvent,
        StopEvent,
        CountChangedEvent,
      },
    }
  },
})

Using your domain in react component

// index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

import * as React from 'react'

import { RemeshRoot, useRemeshDomain, useRemeshQuery, useRemeshSend, useRemeshEvent } from 'remesh-react'

import { CountDomain } from './domain'

export const Counter = () => {
  /**
   * use remesh send for sending commands
   */
  const send = useRemeshSend()

  /**
   * read domain via useRemeshDomain
   */
  const countDomain = useRemeshDomain(CountDomain())

  /**
   * read domain query via useRemeshQuery
   */
  const count = useRemeshQuery(countDomain.query.CountQuery())

  const handleIncrement = () => {
    /**
     * send command to domain
     */
    send(countDomain.command.IncreaseCountCommand())
  }

  const handleDecrement = () => {
    /**
     * send command to domain
     */
    send(countDomain.command.DecreaseCountCommand())
  }

  const handleStartIncrease = () => {
    /**
     * send command to domain
     */
    send(countDomain.command.StartCommand('increment'))
  }

  const handleStartDecrease = () => {
    /**
     * send command to domain
     */
    send(countDomain.command.StartCommand('decrement'))
  }

  const handleStop = () => {
    /**
     * send command to domain
     */
    send(countDomain.command.StopCommand())
  }

  /**
   * listen to the domain event via useRemeshEvent
   */
  useRemeshEvent(countDomain.event.CountChangedEvent, (count) => {
    console.log(count)
  })

  return (
    <div id="container" style=>
      <h1 id="count">{count}</h1>
      <button style= onClick={handleStartIncrease}>
        start increase
      </button> <button style= onClick={handleIncrement}>
        +1
      </button> <button style= onClick={handleStop}>
        stop
      </button> <button style= onClick={handleDecrement}>
        -1
      </button> <button style= onClick={handleStartDecrease}>
        start decrease
      </button>{' '}
    </div>
  )
}

const rootElement = document.getElementById('root')
const root = createRoot(rootElement)

root.render(
  <StrictMode>
    <RemeshRoot>
      <Counter />
    </RemeshRoot>
  </StrictMode>,
)

Packages

Inspiration

FAQ

How do I disable the esm module output of remesh?

note: from remesh v4.2, esm is not the default, cjs-first now. You still can import remesh/esm to access esm

remesh v4.0 starts to support esm module output, which may cause errors in your project due to esm/cjs dependencies for now. The solution is to disable the esm module or map it to the corresponding cjs module via bundler configuration.

For example, in webpack, you can configure aliases via resolve.alias to

A similar configuration is available in vite resolve.alias

Pull requests are welcome