@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +130 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +185 -6
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +2 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +381 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +56 -6
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +32 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +16 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +59 -9
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +102 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +148 -47
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +59 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +381 -8
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +2 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +540 -0
- package/src/store/StoreRegistry.ts +418 -0
- package/src/store/create-store.ts +76 -15
- package/src/store/devtools.ts +20 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +64 -13
- package/src/store/store.ts +197 -60
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
- package/docs/building-with-livestore/crud/index.md +0 -5
- package/docs/building-with-livestore/data-modeling/index.md +0 -1
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -208
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -281
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -918
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -736
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -573
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -209
- package/docs/llms.txt +0 -147
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -5
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -5
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -226
- package/docs/patterns/effect/index.md +0 -1495
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -192
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -245
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -218
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Devtools
|
|
2
|
-
|
|
3
|
-
NOTE: Once LiveStore is open source, the devtools will be a [sponsor-only benefit](/sustainable-open-source/sponsoring).
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- Real-time data browser with 2-way sync
|
|
8
|
-

|
|
9
|
-
- Query inspector
|
|
10
|
-

|
|
11
|
-
- Eventlog browser
|
|
12
|
-

|
|
13
|
-
- Sync status
|
|
14
|
-

|
|
15
|
-
- Export/import
|
|
16
|
-

|
|
17
|
-
- Reactivity graph / signals inspector
|
|
18
|
-

|
|
19
|
-
- SQLite playground
|
|
20
|
-

|
|
21
|
-
|
|
22
|
-
## Adapters
|
|
23
|
-
|
|
24
|
-
### `@livestore/adapter-web`:
|
|
25
|
-
|
|
26
|
-
Requires the `@livestore/devtools-vite` package to be installed and configured in your Vite config:
|
|
27
|
-
|
|
28
|
-
## `reference/devtools/vite-config.ts`
|
|
29
|
-
|
|
30
|
-
```ts filename="reference/devtools/vite-config.ts"
|
|
31
|
-
|
|
32
|
-
export default defineConfig({
|
|
33
|
-
// ...
|
|
34
|
-
plugins: [livestoreDevtoolsPlugin({ schemaPath: './src/livestore/schema.ts' })],
|
|
35
|
-
})
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
The devtools can be opened in a separate tab (via e.g. `localhost:3000/_livestore/web). You should see the Devtools URL logged in the browser console when running the app.
|
|
39
|
-
|
|
40
|
-
#### Chrome extension
|
|
41
|
-
|
|
42
|
-
You can also use the Devtools Chrome extension.
|
|
43
|
-
|
|
44
|
-

|
|
45
|
-
|
|
46
|
-
Please make sure to manually install the extension version matching the LiveStore version you are using by downloading the appropriate version from the [GitHub releases page](https://github.com/livestorejs/livestore/releases) and installing it manually via `chrome://extensions/`.
|
|
47
|
-
|
|
48
|
-
To install the extension:
|
|
49
|
-
|
|
50
|
-
1. **Unpack the ZIP file** (e.g. `livestore-devtools-chrome-0.3.0.zip`) into a folder on your computer.
|
|
51
|
-
2. Navigate to `chrome://extensions/` and enable **Developer mode** (toggle in the top-right corner).
|
|
52
|
-
3. Click **"Load unpacked"** and select the unpacked folder or drag and drop the folder onto the page.
|
|
53
|
-
|
|
54
|
-
### `@livestore/adapter-expo`:
|
|
55
|
-
|
|
56
|
-
Requires the `@livestore/devtools-expo` package to be installed and configured in your metro config:
|
|
57
|
-
|
|
58
|
-
## `reference/devtools/metro-config.ts`
|
|
59
|
-
|
|
60
|
-
```ts filename="reference/devtools/metro-config.ts"
|
|
61
|
-
// @noErrors
|
|
62
|
-
// metro.config.js
|
|
63
|
-
const { getDefaultConfig } = require('expo/metro-config')
|
|
64
|
-
const { addLiveStoreDevtoolsMiddleware } = require('@livestore/devtools-expo')
|
|
65
|
-
|
|
66
|
-
const config = getDefaultConfig(__dirname)
|
|
67
|
-
|
|
68
|
-
addLiveStoreDevtoolsMiddleware(config, { schemaPath: './src/livestore/schema.ts' })
|
|
69
|
-
|
|
70
|
-
module.exports = config
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
You can open the devtools by pressing `Shift+m` in the Expo CLI process and then selecting `@livestore/devtools-expo` which will open the devtools in a new tab.
|
|
74
|
-
|
|
75
|
-
### `@livestore/adapter-node`:
|
|
76
|
-
|
|
77
|
-
Devtools are configured out of the box for the `makePersistedAdapter` variant (note currently not supported for the `makeInMemoryAdapter` variant).
|
|
78
|
-
|
|
79
|
-
You should see the Devtools URL logged when running the app.
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
# Events
|
|
2
|
-
|
|
3
|
-
## Event definitions
|
|
4
|
-
|
|
5
|
-
There are two types of events:
|
|
6
|
-
|
|
7
|
-
- `synced`: Events that are synced across clients
|
|
8
|
-
- `clientOnly`: Events that are only processed locally on the client (but still synced across client sessions e.g. across browser tabs/windows)
|
|
9
|
-
|
|
10
|
-
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.
|
|
11
|
-
|
|
12
|
-
Events will be synced across clients and materialized into state (i.e. SQLite tables) via [materializers](/building-with-livestore/state/materializers).
|
|
13
|
-
|
|
14
|
-
### Example
|
|
15
|
-
|
|
16
|
-
## `reference/events/livestore-schema.ts`
|
|
17
|
-
|
|
18
|
-
```ts filename="reference/events/livestore-schema.ts"
|
|
19
|
-
// livestore/schema.ts
|
|
20
|
-
|
|
21
|
-
export const events = {
|
|
22
|
-
todoCreated: Events.synced({
|
|
23
|
-
name: 'v1.TodoCreated',
|
|
24
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
25
|
-
}),
|
|
26
|
-
todoCompleted: Events.synced({
|
|
27
|
-
name: 'v1.TodoCompleted',
|
|
28
|
-
schema: Schema.Struct({ id: Schema.String }),
|
|
29
|
-
}),
|
|
30
|
-
} as const
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Commiting events
|
|
34
|
-
|
|
35
|
-
## `reference/events/commit.ts`
|
|
36
|
-
|
|
37
|
-
```ts filename="reference/events/commit.ts"
|
|
38
|
-
// somewhere in your app
|
|
39
|
-
|
|
40
|
-
declare const store: Store
|
|
41
|
-
|
|
42
|
-
store.commit(events.todoCreated({ id: '1', text: 'Buy milk' }))
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### `reference/events/livestore-schema.ts`
|
|
46
|
-
|
|
47
|
-
```ts filename="reference/events/livestore-schema.ts"
|
|
48
|
-
// livestore/schema.ts
|
|
49
|
-
|
|
50
|
-
export const events = {
|
|
51
|
-
todoCreated: Events.synced({
|
|
52
|
-
name: 'v1.TodoCreated',
|
|
53
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
54
|
-
}),
|
|
55
|
-
todoCompleted: Events.synced({
|
|
56
|
-
name: 'v1.TodoCompleted',
|
|
57
|
-
schema: Schema.Struct({ id: Schema.String }),
|
|
58
|
-
}),
|
|
59
|
-
} as const
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Streaming events
|
|
63
|
-
|
|
64
|
-
Currently only events confirmed by the sync backend are supported.
|
|
65
|
-
|
|
66
|
-
## `reference/events/stream.ts`
|
|
67
|
-
|
|
68
|
-
```ts filename="reference/events/stream.ts"
|
|
69
|
-
|
|
70
|
-
declare const store: Store
|
|
71
|
-
|
|
72
|
-
for await (const event of store.events()) {
|
|
73
|
-
console.log('event from leader', event)
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Best practices
|
|
78
|
-
|
|
79
|
-
- It's strongly recommended to use past-tense event names (e.g. `todoCreated`/`createdTodo` instead of `todoCreate`/`createTodo`) to indicate something already occurred.
|
|
80
|
-
- 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/livestore` re-exports the `nanoid` function.
|
|
81
|
-
- TODO: write down more best practices
|
|
82
|
-
- TODO: mention AI linting (either manually or via a CI step)
|
|
83
|
-
- core idea: feed list of best practices to AI and check if events adhere to them + get suggestions if not
|
|
84
|
-
- It's recommended to avoid `DELETE` events and instead use soft-deletes (e.g. add a `deleted` date/boolean column with a default value of `null`). This helps avoid some common concurrency issues.
|
|
85
|
-
|
|
86
|
-
## Nodes in the LiveStore system
|
|
87
|
-
|
|
88
|
-
<EventNodesDiagram class="my-8" />
|
|
89
|
-
|
|
90
|
-
#### Client Session
|
|
91
|
-
- `SyncState`: in-memory for pending events only
|
|
92
|
-
- `dbState`: Materialized state that matches the schema (SQLite database)
|
|
93
|
-
|
|
94
|
-
#### Client Leader
|
|
95
|
-
- `dbEventLog`: Database that stores the durable event log and tracks global sequence of events which the backend has acknowledged
|
|
96
|
-
- `dbState`: Materialized state that matches the schema (SQLite database) so leader can materialize events and handle rollbacks
|
|
97
|
-
|
|
98
|
-
#### Sync backend
|
|
99
|
-
- `EventLog`: Any storage solution that supports pushing and pulling events. Append only storage.
|
|
100
|
-
|
|
101
|
-
### Happy event path
|
|
102
|
-
|
|
103
|
-
1. Client session commits an event
|
|
104
|
-
- Client session merges the new event `e3` into its local `SyncState` as pending
|
|
105
|
-
- Client session pushes the pending event to the client leader thread; the leader still shows the previous head until it persists the event
|
|
106
|
-
<EventsVisualizer
|
|
107
|
-
client={["e1", "e2", "A:e3'{todoCreated}"]}
|
|
108
|
-
leader={["e1", "e2"]}
|
|
109
|
-
backend={["e1", "e2"]} />
|
|
110
|
-
|
|
111
|
-
2. Client leader persists the event
|
|
112
|
-
- Client leader materializes the event and writes it to `EventLog`
|
|
113
|
-
- Event `e3` remains unconfirmed from the leader's perspective because the backend has not acknowledged it yet
|
|
114
|
-
<EventsVisualizer
|
|
115
|
-
client={["e1", "e2", "A:e3'{todoCreated}"]}
|
|
116
|
-
leader={["e1", "e2", "A:e3'{todoCreated}"]}
|
|
117
|
-
backend={["e1", "e2"]} />
|
|
118
|
-
|
|
119
|
-
3. Leader thread emits signal back to subscribed clients
|
|
120
|
-
- Client session merges the authoritative event from the leader
|
|
121
|
-
- Event transitions from pending to confirmed on the client while the leader still waits for backend confirmation
|
|
122
|
-
<EventsVisualizer
|
|
123
|
-
client={["e1", "e2", "A:e3{todoCreated}"]}
|
|
124
|
-
leader={["e1", "e2", "A:e3'{todoCreated}"]}
|
|
125
|
-
backend={["e1", "e2"]} />
|
|
126
|
-
|
|
127
|
-
4. Leader thread pushes the event to the sync backend
|
|
128
|
-
- Leader pushes the pending event upstream; it stays marked as unconfirmed in the client leader's eventlog until the backend acknowledges receipt
|
|
129
|
-
<EventsVisualizer
|
|
130
|
-
client={["e1", "e2", "A:e3{todoCreated}"]}
|
|
131
|
-
leader={["e1", "e2", "A:e3'{todoCreated}"]}
|
|
132
|
-
backend={["e1", "e2"]} />
|
|
133
|
-
|
|
134
|
-
5. Sync backend pulls the event from the client leader
|
|
135
|
-
- Sync backend acknowledges the event and advances its head
|
|
136
|
-
- Client leader receives the acknowledgement and marks the event as confirmed
|
|
137
|
-
- All heads align on the confirmed sequence
|
|
138
|
-
<EventsVisualizer
|
|
139
|
-
client={["e1", "e2", "A:e3{todoCreated}"]}
|
|
140
|
-
leader={["e1", "e2", "A:e3{todoCreated}"]}
|
|
141
|
-
backend={["e1", "e2", "A:e3{todoCreated}"]} />
|
|
142
|
-
|
|
143
|
-
### Conflict resolution
|
|
144
|
-
|
|
145
|
-
This example shows how a client session rebases its pending events when new authoritative events arrive from upstream. Client `A` owns the local work that gets rebased, while client `B` introduces the authoritative change. Colors follow the client IDs so lineage remains visible, and origin notation tracks the rebased event.
|
|
146
|
-
|
|
147
|
-
1. Client session has local pending work while upstream advances
|
|
148
|
-
- Client session holds pending event `A:e3'{todoRenamed}` built on top of shared history `e1 → e2`
|
|
149
|
-
- Sync backend publishes authoritative event `B:e3{todoRenamed}` that replaces the client's local change
|
|
150
|
-
<EventsVisualizer
|
|
151
|
-
client={["e1", "e2", "A:e3{todoRenamed}"]}
|
|
152
|
-
leader={["e1", "e2", "A:e3'{todoRenamed}"]}
|
|
153
|
-
backend={["e1", "e2", "B:e3{todoRenamed}"]} />
|
|
154
|
-
|
|
155
|
-
2. Client leader pulls authoritative events from the sync backend
|
|
156
|
-
- Client compares its pending chain with upstream events and spots the divergence at `e2`
|
|
157
|
-
- Client rolls back events and state to the point of divergence
|
|
158
|
-
<EventsVisualizer
|
|
159
|
-
client={["e1", "e2"]}
|
|
160
|
-
leader={["e1", "e2"]}
|
|
161
|
-
backend={["e1", "e2", "B:e3{todoRenamed}"]} />
|
|
162
|
-
|
|
163
|
-
3. Client applies authoritative upstream events
|
|
164
|
-
- Client session and leader apply the authoritative upstream events and advances their heads to `e3`
|
|
165
|
-
<EventsVisualizer
|
|
166
|
-
client={["e1", "e2", "B:e3{todoRenamed}"]}
|
|
167
|
-
leader={["e1", "e2", "B:e3{todoRenamed}"]}
|
|
168
|
-
backend={["e1", "e2", "B:e3{todoRenamed}"]} />
|
|
169
|
-
|
|
170
|
-
4. Client replays its local pending events on top of the new head
|
|
171
|
-
- Stored original events keep their payload but their sequence number gets updated to follow upstream head
|
|
172
|
-
- Each newly numbered event is re-appplied and materialized to state in both client session and client leader
|
|
173
|
-
<EventsVisualizer
|
|
174
|
-
client={["e1", "e2", "B:e3{todoRenamed}", "A:e4{todoRenamed}/e3"]}
|
|
175
|
-
leader={["e1", "e2", "B:e3{todoRenamed}", "A:e4'{todoRenamed}/e3"]}
|
|
176
|
-
backend={["e1", "e2", "B:e3{todoRenamed}"]} />
|
|
177
|
-
|
|
178
|
-
5. Client pushes its local pending events to sync backend
|
|
179
|
-
- Upon receipt local pending events are marked as confirmed and the client leader advances its head to `e4`
|
|
180
|
-
<EventsVisualizer
|
|
181
|
-
client={["e1", "e2", "B:e3{todoRenamed}", "A:e4{todoRenamed}/e3"]}
|
|
182
|
-
leader={["e1", "e2", "B:e3{todoRenamed}", "A:e4{todoRenamed}/e3"]}
|
|
183
|
-
backend={["e1", "e2", "B:e3{todoRenamed}", "A:e4{todoRenamed}/e3"]} />
|
|
184
|
-
|
|
185
|
-
## Unknown events
|
|
186
|
-
|
|
187
|
-
Older clients might receive events that were introduced in newer app versions. Configure the behaviour centrally via `unknownEventHandling` when constructing the schema:
|
|
188
|
-
|
|
189
|
-
## `reference/events/unknown-event-handling.ts`
|
|
190
|
-
|
|
191
|
-
```ts filename="reference/events/unknown-event-handling.ts"
|
|
192
|
-
|
|
193
|
-
const tables = {
|
|
194
|
-
todos: State.SQLite.table({
|
|
195
|
-
name: 'todos',
|
|
196
|
-
columns: {
|
|
197
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
198
|
-
text: State.SQLite.text(),
|
|
199
|
-
},
|
|
200
|
-
}),
|
|
201
|
-
} as const
|
|
202
|
-
|
|
203
|
-
const events = {
|
|
204
|
-
todoCreated: Events.synced({
|
|
205
|
-
name: 'v1.TodoCreated',
|
|
206
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
207
|
-
}),
|
|
208
|
-
} as const
|
|
209
|
-
|
|
210
|
-
const materializers = State.SQLite.materializers(events, {
|
|
211
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
212
|
-
tables.todos.insert({ id, text }),
|
|
213
|
-
),
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
217
|
-
|
|
218
|
-
// ---cut---
|
|
219
|
-
|
|
220
|
-
const _schema = makeSchema({
|
|
221
|
-
events,
|
|
222
|
-
state,
|
|
223
|
-
unknownEventHandling: {
|
|
224
|
-
strategy: 'callback',
|
|
225
|
-
onUnknownEvent: (event, error) => {
|
|
226
|
-
console.warn('LiveStore saw an unknown event', { event, reason: error.reason })
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
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.
|
|
233
|
-
|
|
234
|
-
## Schema evolution \{#schema-evolution\}
|
|
235
|
-
|
|
236
|
-
- Event definitions can't be removed after they were added to your app.
|
|
237
|
-
- Event schema definitions can be evolved as long as the changes are forward-compatible.
|
|
238
|
-
- That means data encoded with the old schema can be decoded with the new schema.
|
|
239
|
-
- In practice, this means ...
|
|
240
|
-
- for structs ...
|
|
241
|
-
- you can add new fields if they have default values or are optional
|
|
242
|
-
- you can remove fields
|
|
243
|
-
|
|
244
|
-
## Event format
|
|
245
|
-
|
|
246
|
-
Each event has the following structure:
|
|
247
|
-
|
|
248
|
-
| Field | Description |
|
|
249
|
-
|-------|-------------|
|
|
250
|
-
| `name` | Event name matching the event definition |
|
|
251
|
-
| `args` | Event arguments as defined by the event schema |
|
|
252
|
-
| `seqNum` | Sequence number identifying this event |
|
|
253
|
-
| `parentSeqNum` | Parent event's sequence number (for causal ordering) |
|
|
254
|
-
| `clientId` | Identifier of the client that created the event |
|
|
255
|
-
| `sessionId` | Identifier of the session |
|
|
256
|
-
|
|
257
|
-
### Encoded vs decoded events
|
|
258
|
-
|
|
259
|
-
Events exist in two formats:
|
|
260
|
-
|
|
261
|
-
**Decoded** - Native TypeScript types used in application code:
|
|
262
|
-
|
|
263
|
-
```json
|
|
264
|
-
{
|
|
265
|
-
"name": "todoCreated-v1",
|
|
266
|
-
"args": { "id": "abc123", "text": "Buy milk", "createdAt": Date },
|
|
267
|
-
"seqNum": 5,
|
|
268
|
-
"parentSeqNum": 4,
|
|
269
|
-
"clientId": "client-xyz",
|
|
270
|
-
"sessionId": "session-123"
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**Encoded** - Serialized format for storage and sync:
|
|
275
|
-
|
|
276
|
-
```json
|
|
277
|
-
{
|
|
278
|
-
"name": "todoCreated-v1",
|
|
279
|
-
"args": { "id": "abc123", "text": "Buy milk", "createdAt": "2024-01-15T10:30:00.000Z" },
|
|
280
|
-
"seqNum": 5,
|
|
281
|
-
"parentSeqNum": 4,
|
|
282
|
-
"clientId": "client-xyz",
|
|
283
|
-
"sessionId": "session-123"
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
The `args` field is encoded according to the event's schema (e.g., `Date` objects become ISO strings, binary data becomes base64). LiveStore handles encoding/decoding automatically.
|
|
288
|
-
|
|
289
|
-
### Client-side sequence numbers
|
|
290
|
-
|
|
291
|
-
On the client, sequence numbers are expanded to track additional information for local events:
|
|
292
|
-
|
|
293
|
-
```json
|
|
294
|
-
{
|
|
295
|
-
"seqNum": { "global": 5, "client": 1, "rebaseGeneration": 0 },
|
|
296
|
-
"parentSeqNum": { "global": 5, "client": 0, "rebaseGeneration": 0 }
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
- **global**: Globally unique integer assigned by the sync backend (`EventSequenceNumber.Global`)
|
|
301
|
-
- **client**: Client-local counter (0 for synced events, increments for client-only events) (`EventSequenceNumber.Client`)
|
|
302
|
-
- **rebaseGeneration**: Increments when the client rebases unconfirmed events
|
|
303
|
-
|
|
304
|
-
Events can be represented as strings like `e5` (global event 5), `e5.1` (client-local event), or `e5r1` (after a rebase).
|
|
305
|
-
|
|
306
|
-
For the full type definitions, see [`LiveStoreEvent`](https://github.com/livestorejs/livestore/tree/dev/packages/@livestore/common/src/schema/LiveStoreEvent) and [`EventSequenceNumber`](https://github.com/livestorejs/livestore/tree/dev/packages/@livestore/common/src/schema/EventSequenceNumber).
|
|
307
|
-
|
|
308
|
-
### Event type namespaces
|
|
309
|
-
|
|
310
|
-
LiveStore organizes event types into namespaces based on their usage context:
|
|
311
|
-
|
|
312
|
-
| Namespace | Description | Sequence Number Format |
|
|
313
|
-
|-----------|-------------|----------------------|
|
|
314
|
-
| `LiveStoreEvent.Input` | Events without sequence numbers (for committing) | None |
|
|
315
|
-
| `LiveStoreEvent.Global` | Sync backend format | Integer (`seqNum: number`) |
|
|
316
|
-
| `LiveStoreEvent.Client` | Client-side format with full metadata | Struct (`seqNum: { global, client, rebaseGeneration }`) |
|
|
317
|
-
|
|
318
|
-
## `reference/events/event-type-namespaces.ts`
|
|
319
|
-
|
|
320
|
-
```ts filename="reference/events/event-type-namespaces.ts"
|
|
321
|
-
|
|
322
|
-
// Input events (no sequence numbers) - used when committing
|
|
323
|
-
const _input: LiveStoreEvent.Input.Decoded = {
|
|
324
|
-
name: 'todoCreated-v1',
|
|
325
|
-
args: { id: 'abc123', text: 'Buy milk' },
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Global events (sync backend format) - integer sequence numbers
|
|
329
|
-
const _global: LiveStoreEvent.Global.Encoded = {
|
|
330
|
-
name: 'todoCreated-v1',
|
|
331
|
-
args: { id: 'abc123', text: 'Buy milk' },
|
|
332
|
-
seqNum: EventSequenceNumber.Global.make(5),
|
|
333
|
-
parentSeqNum: EventSequenceNumber.Global.make(4),
|
|
334
|
-
clientId: 'client-xyz',
|
|
335
|
-
sessionId: 'session-123',
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Client events (local format) - composite sequence numbers
|
|
339
|
-
const _client: LiveStoreEvent.Client.Encoded = {
|
|
340
|
-
name: 'todoCreated-v1',
|
|
341
|
-
args: { id: 'abc123', text: 'Buy milk' },
|
|
342
|
-
seqNum: EventSequenceNumber.Client.Composite.make({ global: 5, client: 0, rebaseGeneration: 0 }),
|
|
343
|
-
parentSeqNum: EventSequenceNumber.Client.Composite.make({ global: 4, client: 0, rebaseGeneration: 0 }),
|
|
344
|
-
clientId: 'client-xyz',
|
|
345
|
-
sessionId: 'session-123',
|
|
346
|
-
}
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
## Eventlog
|
|
350
|
-
|
|
351
|
-
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.
|
|
352
|
-
|
|
353
|
-
Example `eventlog.db`:
|
|
354
|
-
|
|
355
|
-

|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Data modeling
|
|
2
|
-
|
|
3
|
-
## Core idea
|
|
4
|
-
|
|
5
|
-
- Data modeling is probably the most important part of any app and needs to be done carefully.
|
|
6
|
-
- The core idea is to model the read and write model separately.
|
|
7
|
-
- Depending on the use case, you might also want to split up the read/write model into separate "containers" (e.g. for data-sharing/scalability/access control reasons).
|
|
8
|
-
- There is no transactional consistency between containers.
|
|
9
|
-
- Caveat: Event sourcing is not ideal for all use cases - some apps might be better off with another approach (e.g. use CRDTs for rich text editing).
|
|
10
|
-
|
|
11
|
-
## Considerations for data modeling
|
|
12
|
-
|
|
13
|
-
- How much data do you expect to have and what is the shape of the data?
|
|
14
|
-
- Some kind of data needs special handling (e.g. blobs or rich text)
|
|
15
|
-
- Access patterns (performance, ...)
|
|
16
|
-
- Access control
|
|
17
|
-
- Data integrity / consistency
|
|
18
|
-
- Sharing / collaboration
|
|
19
|
-
- Regulatory requirements (e.g. GDPR, audit logs, ...)
|
|
20
|
-
|
|
21
|
-
## TODO
|
|
22
|
-
|
|
23
|
-
- TODO: actually write this section
|
|
24
|
-
- questions to answer
|
|
25
|
-
- When to split things into separate containers?
|
|
26
|
-
- How do migrations work?
|
|
27
|
-
- Read model migrations
|
|
28
|
-
- Write model migrations
|
|
29
|
-
- How to create new write models based on existing ones
|
|
30
|
-
- Example: An app has multiple workspaces and you now want to introduce the concept of "projects" inside a workspace. You might want to pre-populate a "default workspace project" for each workspace.
|