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
DestroyRefSignal-based value tracking for reactive access
Optional memory for late subscribers (
{ replay },{ initial })Read-only variants for encapsulation
Import#
import { craftComputed, source$ } from '@craft-ts/core';
Signature#
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 withcraftComputed/craftEffect. Thecraft-ts/craft-source-name-matchESLint 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*:
// 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 signalsubscribe(callback: (value: T) => void)- Subscribes to emissions with a callbackvalue: Signal<T | undefined>- A read-only signal containing the last emitted value (orundefinedif no value has been emitted)asReadonly()- Returns a read-only version of the source (onlysubscribeandvalue)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$#
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$#
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:
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:
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:
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:
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:
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#
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#
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#
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$#
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#
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#
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