Events
Event definitions
Section titled “Event definitions”There are two types of events:
synced: Events that are synced across clientsclientOnly: Events that are only processed locally on the client (but still synced across client sessions e.g. across browser tabs/windows)
An event definition consists of a unique name of the event and a schema for the event arguments. It’s recommended to version event definitions to make it easier to evolve them over time.
Events will be synced across clients and materialized into state (i.e. SQLite tables) via materializers.
Example
Section titled “Example”// livestore/schema.tsimport { import Events
Events, import Schema
Schema } from '@livestore/livestore'
export const const events: { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
events = { todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, false>
todoCreated: import Events
Events.synced<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}>(args: { name: "v1.TodoCreated"; schema: Schema.Schema<{ readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, never>;} & Omit<DefineEventOptions<{ readonly id: string; readonly text: string;}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, false>export synced
synced({ name: "v1.TodoCreated"
name: 'v1.TodoCreated', schema: Schema.Schema<{ readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, never>
schema: import Schema
Schema.function Struct<{ id: typeof Schema.String; text: typeof Schema.String;}>(fields: { id: typeof Schema.String; text: typeof Schema.String;}): Schema.Struct<{ id: typeof Schema.String; text: typeof Schema.String;}> (+1 overload)
Struct({ id: typeof Schema.String
id: import Schema
Schema.class Stringexport String
String, text: typeof Schema.String
text: import Schema
Schema.class Stringexport String
String }), }), todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}, false>
todoCompleted: import Events
Events.synced<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}>(args: { name: "v1.TodoCompleted"; schema: Schema.Schema<{ readonly id: string; }, { readonly id: string; }, never>;} & Omit<DefineEventOptions<{ readonly id: string;}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}, false>export synced
synced({ name: "v1.TodoCompleted"
name: 'v1.TodoCompleted', schema: Schema.Schema<{ readonly id: string;}, { readonly id: string;}, never>
schema: import Schema
Schema.function Struct<{ id: typeof Schema.String;}>(fields: { id: typeof Schema.String;}): Schema.Struct<{ id: typeof Schema.String;}> (+1 overload)
Struct({ id: typeof Schema.String
id: import Schema
Schema.class Stringexport String
String }), }),} as type const = { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
constBest Practices
Section titled “Best Practices”- It’s strongly recommended to use past-tense event names (e.g.
todoCreated/createdTodoinstead oftodoCreate/createTodo) to indicate something already occurred. - When generating IDs for events (e.g. for the todo in the example above), it’s recommended to use a globally unique ID generator (e.g. UUID, nanoid, etc.) to avoid conflicts. For convenience,
@livestore/livestorere-exports thenanoidfunction. - TODO: write down more best practices
- TODO: mention AI linting (either manually or via a CI step)
- core idea: feed list of best practices to AI and check if events adhere to them + get suggestions if not
- It’s recommended to avoid
DELETEevents and instead use soft-deletes (e.g. add adeleteddate/boolean column with a default value ofnull). This helps avoid some common concurrency issues.
Unknown events
Section titled “Unknown events”Older clients might receive events that were introduced in newer app versions. Configure the behaviour centrally via unknownEventHandling when constructing the schema:
const schema = makeSchema({ events, state, unknownEventHandling: { strategy: 'callback', onUnknownEvent: (event, error) => { console.warn('LiveStore saw an unknown event', { event, reason: error.reason }) }, },})Pick 'warn' (default) to log every occurrence, 'ignore' to silently drop new events until the client updates, 'fail' to halt immediately, or 'callback' to delegate to custom logging/telemetry while continuing to process the log.
Schema evolution
Section titled “Schema evolution”- Event definitions can’t be removed after they were added to your app.
- Event schema definitions can be evolved as long as the changes are forward-compatible.
- That means data encoded with the old schema can be decoded with the new schema.
- In practice, this means …
- for structs …
- you can add new fields if they have default values or are optional
- you can remove fields
- for structs …
Commiting events
Section titled “Commiting events”// somewhere in your appimport type { class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>
Store } from '@livestore/livestore'
import { const events: { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
events } from './livestore-schema.ts'
declare const const store: Store<LiveStoreSchema.Any, {}>
store: class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>
Store
const store: Store<LiveStoreSchema.Any, {}>
store.Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [{ name: "v1.TodoCreated"; args: { readonly id: string; readonly text: string; };}]>(list_0: { name: "v1.TodoCreated"; args: { readonly id: string; readonly text: string; };}) => void (+3 overloads)
commit(const events: { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
events.todoCreated: (args: { readonly id: string; readonly text: string;}) => { name: "v1.TodoCreated"; args: { readonly id: string; readonly text: string; };}
Helper function to construct a partial event
todoCreated({ id: string
id: '1', text: string
text: 'Buy milk' }))// livestore/schema.tsimport { import Events
Events, import Schema
Schema } from '@livestore/livestore'
export const const events: { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
events = { todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, false>
todoCreated: import Events
Events.synced<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}>(args: { name: "v1.TodoCreated"; schema: Schema.Schema<{ readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, never>;} & Omit<DefineEventOptions<{ readonly id: string; readonly text: string;}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, false>export synced
synced({ name: "v1.TodoCreated"
name: 'v1.TodoCreated', schema: Schema.Schema<{ readonly id: string; readonly text: string;}, { readonly id: string; readonly text: string;}, never>
schema: import Schema
Schema.function Struct<{ id: typeof Schema.String; text: typeof Schema.String;}>(fields: { id: typeof Schema.String; text: typeof Schema.String;}): Schema.Struct<{ id: typeof Schema.String; text: typeof Schema.String;}> (+1 overload)
Struct({ id: typeof Schema.String
id: import Schema
Schema.class Stringexport String
String, text: typeof Schema.String
text: import Schema
Schema.class Stringexport String
String }), }), todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}, false>
todoCompleted: import Events
Events.synced<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}>(args: { name: "v1.TodoCompleted"; schema: Schema.Schema<{ readonly id: string; }, { readonly id: string; }, never>;} & Omit<DefineEventOptions<{ readonly id: string;}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCompleted", { readonly id: string;}, { readonly id: string;}, false>export synced
synced({ name: "v1.TodoCompleted"
name: 'v1.TodoCompleted', schema: Schema.Schema<{ readonly id: string;}, { readonly id: string;}, never>
schema: import Schema
Schema.function Struct<{ id: typeof Schema.String;}>(fields: { id: typeof Schema.String;}): Schema.Struct<{ id: typeof Schema.String;}> (+1 overload)
Struct({ id: typeof Schema.String
id: import Schema
Schema.class Stringexport String
String }), }),} as type const = { readonly todoCreated: EventDef<"v1.TodoCreated", { readonly id: string; readonly text: string; }, { readonly id: string; readonly text: string; }, false>; readonly todoCompleted: EventDef<"v1.TodoCompleted", { readonly id: string; }, { readonly id: string; }, false>;}
constEventlog
Section titled “Eventlog”The history of all events that have been committed is stored forms the “eventlog”. It is persisted in the client as well as in the sync backend.
Example eventlog.db: