Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Reactivity utilities
  2. source$

source$#

An event source: something you emit() to, that others react to — with automatic cleanup and signal-based value tracking.

Use it when several independent pieces of state must react to one event: a "reset everything" action, a refresh trigger, a cross-cutting notification. Not for a direct call from A to B — that is just a method.

Overview#

source$ provides a lightweight event streaming solution that combines:

  • Event emission and subscription capabilities

  • Automatic subscription cleanup via DestroyRef

  • Signal-based value tracking for reactive access

  • Optional memory for late subscribers ({ replay }, { initial })

  • Read-only variants for encapsulation

Import#

typescript
import { craftComputed, source$ } from '@craft-ts/core';

Signature#

typescript
function source$<T>(name: string, options?: SourceOptions<T>): Source$<T>;

type SourceOptions<T> = {
  /** A new subscriber first receives the last `replay` emitted values. */
  replay?: number;
  /** Starts the source holding this value; `value()` is then never `undefined`. */
  initial?: T;
};

Parameters#

  • name: string - Name matching the variable/property this source is assigned to. Used for host tagging and dev-tools snapshot reporting, consistent with craftComputed/craftEffect. The craft-ts/craft-source-name-match ESLint rule enforces the match and offers a quick fix.

Returns#

Source$<T> is directly usable as a source and can also be consumed with yield*:

typescript
// Direct source API
const source = source$<void>('reset$');
source.emit();

// Yieldable primitive API
const reset$ = yield* source$<void>('reset$');

The yielded value is the source instance. Its source API is unchanged:

  • emit(value: T) - Emits a value to all subscribers and updates the internal signal

  • subscribe(callback: (value: T) => void) - Subscribes to emissions with a callback

  • value: Signal<T | undefined> - A read-only signal containing the last emitted value (or undefined if no value has been emitted)

  • asReadonly() - Returns a read-only version of the source (only subscribe and value)

  • preserveLastValue() - Deprecated, use { replay: 1 }. Returns a source variant that immediately emits the last value to new subscribers (nothing when no value was emitted yet)

Types#

Source$#

typescript
type Source$<T> = SourceInstance<T> &
  NamedCraftPrimitiveGen<string, SourceInstance<T>>;

type SourceInstance<T> = {
  emit: (value: T) => void;
  subscribe: (callback: (value: T) => void) => Subscription;
  value: Signal<T | undefined>;
  asReadonly: () => ReadonlySource$<T>;
  preserveLastValue: () => {
    emit: (value: T) => void;
    subscribe: (callback: (value: T) => void) => void;
    value: Signal<T | undefined>;
    asReadonly: () => {
      subscribe: (callback: (value: T) => void) => void;
      value: Signal<T | undefined>;
    };
  };
};

The generator side yields { [name]: SourceInstance<T> }. The source side keeps the existing emit, subscribe, value, asReadonly and preserveLastValue API.

ReadonlySource$#

typescript
type ReadonlySource$<T> = {
  subscribe: (callback: (value: T) => void) => Subscription;
  value: Signal<T | undefined>;
};

Key Features#

Source services and dependency tracking#

Use a craftService as the dependency handle when a source is shared by multiple consumers. The service exposes its source under its name, and on$(Service, ...) subscribes to the one source a service exposes:

ts
import { craftService, on$, source$, state } from '@craft-ts/core';

const { Reset } = craftService(
  { name: 'Reset', providedIn: 'global' },
  function* () {
    // With an explicit value type, repeat the name as a type argument:
    // TypeScript cannot infer the one while you give the other.
    yield* source$<void, 'reset$'>('reset$');
  },
);

const { Counter } = craftService(
  { name: 'Counter', providedIn: 'global' },
  function* () {
    yield* state('counter', 0, ({ set }) => ({
      reset: on$(Reset, () => set(0)),
    }));

    yield* Reset.reset$();
  },
);
void Counter;

on$(Reset, ...) records Reset as a dependency of the primitive. Calling reset.emit() only publishes an event; it does not create or modify the dependency graph.

Automatic Cleanup#

Subscriptions are automatically cleaned up when the injection context is destroyed, preventing memory leaks:

typescript
const userAction$ = source$<string>('userAction$');

// Subscription is automatically unsubscribed on component destruction
userAction$.subscribe((action) => console.log(action));

Signal Integration#

The value property provides reactive access to the last emitted value:

typescript
const message$ = source$<string>('message$');

message$.emit('Hello');
console.log(message$.value()); // 'Hello'

// Use in templates or craftComputed
const uppercased = craftUse(craftComputed('uppercased', function* () {
  return (yield* message$.value())?.toUpperCase();
}));

Replay and initial value#

Give the source a memory with options, instead of deriving a variant:

typescript
const notifications$ = source$<string>('notifications$', { replay: 2 });
notifications$.emit('a');
notifications$.emit('b');
notifications$.emit('c');
// A late subscriber receives 'b' then 'c', then live emissions.
notifications$.subscribe((message) => console.log(message));

// `initial` seeds the source: `value()` is typed `T` (never undefined) and a
// subscriber receives the current value first.
const theme$ = source$<'light' | 'dark'>('theme$', { initial: 'light' });
theme$.value(); // 'light'

replay keeps the last n values; initial implies replay >= 1.

Last Value Preservation (deprecated)#

preserveLastValue() predates the options above and is kept for compatibility; prefer { replay: 1 }. It makes late subscribers receive the most recent value:

typescript
const counter$ = source$<number>('counter$');
counter$.emit(42);

// Standard source: late subscriber receives nothing
counter$.subscribe((v) => console.log('Standard:', v)); // Only future values

// With preserveLastValue: late subscriber gets the last value immediately
const preserved$ = counter$.preserveLastValue();
preserved$.subscribe((v) => console.log('Preserved:', v)); // Logs: Preserved: 42

Common Patterns#

Event Broadcasting#

typescript
const buttonClick$ = source$<MouseEvent>('buttonClick$');

// Multiple subscribers
buttonClick$.subscribe((event) => console.log('Logger:', event));
buttonClick$.subscribe((event) => trackEvent('button_click'));

// Emit events
button.addEventListener('click', (e) => buttonClick$.emit(e));

Read-Only Access#

typescript
class DataService {
  private dataUpdated$ = source$<Data>('dataUpdated$');

  // Expose read-only version
  readonly dataUpdated = this.dataUpdated$.asReadonly();

  updateData(data: Data) {
    this.dataUpdated$.emit(data);
  }
}

Coordination with State#

typescript
const resetTrigger$ = source$<void>('resetTrigger$');

const { counter } = state('counter', 0, ({ set, update }) => ({
  increment: () => update((v) => v + 1),
  decrement: () => update((v) => v - 1),
  // Reset when source emits
  reset: on$(resetTrigger$, () => set(0)),
}));

Examples#

Basic Usage with on$#

typescript
import { button, craftComponent, p } from '@craft-ts/component';
import { on$, source$, state } from '@craft-ts/core';

export const Counter = craftComponent('Counter', {}, function* () {
  // a source for reset events
  const reset$ = source$<void>('reset$');

  const counter = yield* state('counter', 0, ({ set, update }) => ({
    increment: () => update((v) => v + 1),
    decrement: () => update((v) => v - 1),
    // internal: listens to reset$ and sets counter to 0.
    // NOT exposed on the ref, because it is bound with on$
    reset: on$(reset$, () => set(0)),
  }));

  return [
    p(function* () {
      return `Count: ${yield* counter()}`;
    }),
    button({ click: counter.increment }, '+1'),
    button({ click: counter.decrement }, '-1'),
    button({ click: () => reset$.emit() }, 'Reset'),
  ];
});

Multi-Source Coordination#

typescript
import { source$, state, on$ } from '@craft-ts/core';

// Multiple sources for different events
const userLogin$ = source$<User>('userLogin$');
const userLogout$ = source$<void>('userLogout$');

const { authState } = state<User | null>('authState', null, ({ set }) => ({
  // Respond to multiple sources
  onLogin: on$(userLogin$, (user) => set(user)),
  onLogout: on$(userLogout$, () => set(null)),
}));

// Trigger events
userLogin$.emit({ id: 1, name: 'Alice' });
console.log(authState()); // { id: 1, name: 'Alice' }

userLogout$.emit();
console.log(authState()); // null

Late Subscriber Pattern#

typescript
import { source$ } from '@craft-ts/core';

const notifications$ = source$<string>('notifications$').preserveLastValue();

// Emit before any subscribers
notifications$.emit('Server started');
notifications$.emit('Database connected');

// Late subscriber receives the last value immediately
setTimeout(() => {
  notifications$.subscribe((msg) => {
    console.log('Late subscriber:', msg); // Logs: Late subscriber: Database connected
  });
}, 1000);

Related#

  • on$ - Subscribe to sources with automatic cleanup in state insertions

See Also#

  • on$ — reacting to a source

  • fromEventToSource$

  • sourceFromEvent

←craftMethodon$→

On this page

  • Overview
  • Import
  • Signature
  • Parameters
  • Returns
  • Types
  • Source$
  • ReadonlySource$
  • Key Features
  • Source services and dependency tracking
  • Automatic Cleanup
  • Signal Integration
  • Replay and initial value
  • Last Value Preservation (deprecated)
  • Common Patterns
  • Event Broadcasting
  • Read-Only Access
  • Coordination with State
  • Examples
  • Basic Usage with on$
  • Multi-Source Coordination
  • Late Subscriber Pattern
  • Related
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search