Skip to content

Typed insertion pipes

Each primitive accepts one insertion directly. When a primitive needs several insertions, use the pipe named after that primitive:

PrimitiveTyped pipe
stateinsertStatePipe
queryinsertQueryPipe
mutationinsertMutationPipe
queryParamsinsertQueryParamsPipe
asyncProcessinsertAsyncProcessPipe

The typed pipe keeps the primitive call readable and gives every member the correct contextual type. Members run from left to right, and each member can read the outputs of the members before it through insertions.

State

typescript
import { computed } from '@angular/core';
import { insertStatePipe, state } from '@craft-ng/core';

const { counter } =
  yield *
  state(
    'counter',
    0,
    insertStatePipe(
      ({ update }) => ({
        increment: () => update((value) => value + 1),
      }),
      ({ state, insertions }) => ({
        isOdd: computed(() => state() % 2 === 1),
        incrementAndReport: () => {
          insertions.increment();
          return state();
        },
      }),
    ),
  );

Query

typescript
import {
  insertStoragePersister,
  insertQueryPipe,
  query,
} from '@craft-ng/core';

const { users } =
  yield *
  query(
    'users',
    {
      params: () => ({ page: 1 }),
      loader: ({ params }) => api.getUsers(params),
    },
    insertQueryPipe(
      insertStoragePersister({ storeName: 'app', key: 'users' }),
      ({ resource }) => ({
        reloadUsers: () => resource.reload(),
      }),
    ),
  );

Mutation

typescript
import { insertMutationPipe, mutation } from '@craft-ng/core';

const { saveUser } =
  yield *
  mutation(
    'saveUser',
    {
      method: (user: User) => user,
      loader: ({ params }) => api.saveUser(params),
    },
  insertMutationPipe(
    ({ resource }) => ({
      reload: () => resource.reload(),
    }),
    ({ insertions }) => ({
      reloadTwice: () => {
        insertions.reload();
        insertions.reload();
      },
      }),
    ),
  );

URL state

typescript
import { computed } from '@angular/core';
import { insertQueryParamsPipe, queryParams } from '@craft-ng/core';

const { filters } =
  yield *
  queryParams(
    'filters',
    {
      state: {
        page: { fallbackValue: 1 },
        search: { fallbackValue: '' },
      },
    },
    insertQueryParamsPipe(
      ({ state }) => ({
        hasSearch: computed(() => state().search.length > 0),
      }),
      ({ state, patch }) => ({
        nextPage: () => patch({ page: state().page + 1 }),
      }),
    ),
  );

Async process

typescript
import { insertAsyncProcessPipe, asyncProcess } from '@craft-ng/core';

const { search } =
  yield *
  asyncProcess(
    'search',
    {
      method: (term: string) => term,
      loader: ({ params }) => api.search(params),
    },
  insertAsyncProcessPipe(
    () => ({ source: 'search-box' as const }),
    ({ insertions }) => ({
      prefixTerm: (term: string) => `${insertions.source}:${term}`,
    }),
    ),
  );

When to use craftPipe

Use a single insertion directly when there is no composition:

typescript
state('counter', 0, ({ update }) => ({
  increment: () => update((value) => value + 1),
}));

Keep craftPipe for universal compositions that need an explicit context, especially nested insertions such as insertSelect:

typescript
state('board', initialBoard, (context) =>
  craftPipe(
    context,
    insertSelect('grid', (gridContext) =>
      craftPipe(gridContext, ({ update }) => ({
        reset: () => update(() => []),
      })),
    ),
    ({ state }) => ({ rowCount: computed(() => state().grid.length) }),
  ),
);

The typed pipes delegate to craftPipe, so their runtime semantics remain the same: insertion outputs are merged left to right, generator insertions are driven, and each member keeps its own observability wrapper.