createEmitter
Create the client-safe emitter: typed track(), two delivery lanes, identify()/reset(), batching transport, optional identity provider.
Creates the emitter — the deliberately dumb side of Dotyc. It knows only the catalog and a transport: no adapters, no secrets, no wiring. track() is fully typed by inference from the catalog.
import { createEmitter, http } from 'dotyc'Signature
function createEmitter<C extends Catalog>(options: {
catalog: C
transport: Transport
identity?: IdentityProvider
}): Emitter<C>Options
| Option | Description |
|---|---|
catalog | The catalog from defineCatalog. Anchors the types of track(). |
transport | Where events go. Use http(url, settings) for the standard first-party endpoint, or direct(dispatcher) (from dotyc/ingest) for in-process server-side dispatch. |
identity | Optional identity provider (emitter facet). Plugs into your existing auth so you never call identify() manually. Without it, use manual identify() / reset(). |
The http transport
http('/api/ingest', {
batchSize: 20, // flush after N buffered events
flushInterval: 5_000, // flush every N ms
})Defaults: batching by size and interval, plus sendBeacon on page unload so trailing events survive navigation. Everything is a setting — nothing is hard-coded. One POST carries one Envelope containing N events.
Two lanes: durable and best-effort
The transport keeps two queues, one per delivery class, and an envelope is never mixed — the important never travels with the noise:
- Best-effort lane —
delivery: 'best-effort'events are batched lazily (size/interval settings above) and fire-and-forget: no ack tracking,sendBeaconon unload, a lost batch is acceptable by contract. - Durable lane —
delivery: 'durable'events are flushed immediately in their own envelopes and kept in a retry queue until the ingest response acks them (acceptedorlegacy). Not acked — network failure, 5xx — means retry. This is the emitter half of at-least-once; the dispatcher's outbox is the server half.
This split kills head-of-line blocking by construction: a burst of best-effort volume can never delay a business fact.
Return value
An emitter with:
track(name, properties)— typed by inference: unknown event names and non-conforming properties are compile errors. On anhttp-transport emitter,source: 'trusted'events are excluded from the type entirely — trying to track one is a compile error, because trusted events may only be emitted in-process viadirect(dispatcher). Fire-and-forget from the call site's perspective (no ack promise) — the durable lane's retrying happens under the hood.identify({ userId, accountId, traits })— declares the identity claim. Deduplicated: a hash of the last identity is persisted alongsideanonymousId, so repeated calls with an unchanged identity (auth SDKs notify on every render/refresh) are no-ops.reset()— logout: rotatessessionIdand purges the persisted identity.
Every tracked event is stamped at emission with an eventId (a ULID — sortable, unique) and the schemaHash of its definition. The eventId is what makes retries safe end to end: ingest dedupes replays over a TTL window, and adapters can deduplicate at-least-once deliveries.
Examples
export const dotyc = createEmitter({
catalog,
transport: http('/api/ingest', { batchSize: 20, flushInterval: 5_000 }),
})
dotyc.track('feature_used', { feature: 'gantt_view' }) // OK
dotyc.track('feature_used', {}) // compile error
dotyc.track('typo_event', { feature: 'x' }) // compile error
dotyc.track('subscription_upgraded', { plan: 'pro' }) // compile error: source 'trusted'Server-side, skip HTTP entirely and dispatch in-process through the same wiring — this is the only emitter that can carry trusted events:
import { direct } from 'dotyc/ingest'
const dotycServer = createEmitter({ catalog, transport: direct(dispatcher) })
dotycServer.track('subscription_upgraded', { plan: 'pro' }) // OK hereNotes
- Rejections are loud in development. The ingest response acks every event (
accepted | rejected | legacy); in dev, the emitter surfacesrejectedevents noisily in the console instead of letting them vanish — no more debugging blind. anonymousIdlives in a cookie (SSR-friendly, shared across subdomains, readable by a same-origin ingest), generated and persisted automatically before any login;localStorageis the fallback when cookies are unavailable.- Client identity is a claim. Whatever the emitter sends is unverified (
verified: false); the dispatcher resolves the real identity from the session or JWT and it wins. identify()is not a catalog event. It flows through the dedicatedidentifychannel to adapters, never through the wiring. Want business logic on login? Declare an explicitsigned_inevent in your catalog.
See also
- Emitter concepts
- defineEvent —
deliveryandsource - Identity concepts
- Client-only apps guide