@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,6 +0,0 @@
|
|
|
1
|
-
# Encryption
|
|
2
|
-
|
|
3
|
-
LiveStore doesn't yet support encryption but might in the future.
|
|
4
|
-
See [this issue](https://github.com/livestorejs/livestore/issues/70) for more details.
|
|
5
|
-
|
|
6
|
-
For now you can implement encryption yourself e.g. by encrypting the events using a custom Effect Schema definition which applies a encryption transformation to the events.
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# External data
|
|
2
|
-
|
|
3
|
-
LiveStore doesn't provide any built-in functionality to deal with external data. However, LiveStore was designed with this use case in mind (e.g. Overtone integrates with lots of external data like Spotify, ...). One way to deal with external data is to also model it as an event log and materialize it into LiveStore state as well.
|
|
4
|
-
|
|
5
|
-
(If you're interested in learning more about the solution we're using for Overtone, get in touch.)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# File management
|
|
2
|
-
|
|
3
|
-
LiveStore doesn't have built-in support for file management but it's easy to use LiveStore alongside existing file storage solutions (e.g. S3).
|
|
4
|
-
|
|
5
|
-
The basic idea is to store the file metadata (e.g. url, name, size, type) in LiveStore and the file content separately.
|
|
6
|
-
|
|
7
|
-
## Example
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
// TODO (contribution welcome)
|
|
11
|
-
```
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# File structure
|
|
2
|
-
|
|
3
|
-
While there are no strict requirements/conventions for how to structure your project (files, folders, etc), a common pattern is to have a `src/livestore` folder which contains all the LiveStore related code.
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
src/
|
|
7
|
-
livestore/
|
|
8
|
-
index.ts # re-exports everything
|
|
9
|
-
schema.ts # schema definitions
|
|
10
|
-
queries.ts # query definitions
|
|
11
|
-
events.ts # event definitions
|
|
12
|
-
...
|
|
13
|
-
...
|
|
14
|
-
```
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
# List Ordering
|
|
2
|
-
|
|
3
|
-
Fractional indexing enables conflict-free list ordering in distributed systems by assigning string-based position values that maintain lexicographic order. This makes it ideal for implementing drag-and-drop reordering in LiveStore applications where multiple clients may reorder items concurrently.
|
|
4
|
-
|
|
5
|
-
To understand how the algorithm works, see these visual and interactive explanations:
|
|
6
|
-
- [CRDT Fractional Indexing](https://madebyevan.com/algos/crdt-fractional-indexing/) by Evan Wallace - Visual explanation of the algorithm
|
|
7
|
-
- [Implementing Fractional Indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) by David Greenspan - Interactive notebook walkthrough
|
|
8
|
-
|
|
9
|
-
## Why Fractional Indexing?
|
|
10
|
-
|
|
11
|
-
Traditional numeric ordering (1, 2, 3...) requires renumbering multiple items when inserting or reordering, which creates conflicts in distributed systems. Fractional indexing solves this by:
|
|
12
|
-
|
|
13
|
-
- Generating position values that can always be inserted between any two existing positions
|
|
14
|
-
- Using lexicographic string ordering that works naturally with SQL `ORDER BY`
|
|
15
|
-
- Eliminating the need for coordination between clients when reordering
|
|
16
|
-
- Avoiding cascading updates when inserting items
|
|
17
|
-
|
|
18
|
-
For example, inserting a new item between positions "a0" and "b0" generates "aV", which sorts correctly without modifying existing items.
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
21
|
-
|
|
22
|
-
Install the `fractional-indexing` package from [Rocicorp](https://github.com/rocicorp/fractional-indexing) (the same team behind Replicache):
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
pnpm install fractional-indexing
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Schema Design
|
|
29
|
-
|
|
30
|
-
Define a text column to store the fractional index and add a database index for efficient ordering:
|
|
31
|
-
|
|
32
|
-
## `patterns/list-ordering/schema.ts`
|
|
33
|
-
|
|
34
|
-
```ts filename="patterns/list-ordering/schema.ts"
|
|
35
|
-
|
|
36
|
-
export const task = State.SQLite.table({
|
|
37
|
-
name: 'task',
|
|
38
|
-
columns: {
|
|
39
|
-
id: State.SQLite.integer({ primaryKey: true }),
|
|
40
|
-
title: State.SQLite.text({ default: '' }),
|
|
41
|
-
completed: State.SQLite.integer({ default: 0 }),
|
|
42
|
-
/** Fractional index for ordering tasks in the list */
|
|
43
|
-
order: State.SQLite.text({ nullable: false, default: '' }),
|
|
44
|
-
},
|
|
45
|
-
indexes: [
|
|
46
|
-
/** Index for efficient ordering queries */
|
|
47
|
-
{ name: 'task_order', columns: ['order'] },
|
|
48
|
-
],
|
|
49
|
-
deriveEvents: true,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
export type Task = typeof task.Type
|
|
53
|
-
|
|
54
|
-
export const tables = { task }
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The `order` column stores string values like "a0", "a1", "aV" that maintain lexicographic ordering. Adding a database index on this column ensures efficient queries when retrieving ordered lists.
|
|
58
|
-
|
|
59
|
-
## Creating Ordered Items
|
|
60
|
-
|
|
61
|
-
Use `generateKeyBetween(a, b)` to generate position values when creating new items:
|
|
62
|
-
|
|
63
|
-
## `patterns/list-ordering/create-item.ts`
|
|
64
|
-
|
|
65
|
-
```ts filename="patterns/list-ordering/create-item.ts"
|
|
66
|
-
|
|
67
|
-
declare const store: Store
|
|
68
|
-
|
|
69
|
-
/** Create a new task at the end of the list */
|
|
70
|
-
export const createTaskAtEnd = (title: string) => {
|
|
71
|
-
// Get the highest order value
|
|
72
|
-
const highestOrder = store.query(tables.task.select('order').orderBy('order', 'desc').limit(1))[0] ?? null
|
|
73
|
-
|
|
74
|
-
// Generate new order after the highest
|
|
75
|
-
const order = generateKeyBetween(highestOrder, null)
|
|
76
|
-
|
|
77
|
-
// Commit the event
|
|
78
|
-
store.commit(events.createTask({ title, order }))
|
|
79
|
-
|
|
80
|
-
return order
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Create a new task at the beginning of the list */
|
|
84
|
-
export const createTaskAtStart = (title: string) => {
|
|
85
|
-
// Get the lowest order value
|
|
86
|
-
const lowestOrder = store.query(tables.task.select('order').orderBy('order', 'asc').limit(1))[0] ?? null
|
|
87
|
-
|
|
88
|
-
// Generate new order before the lowest
|
|
89
|
-
const order = generateKeyBetween(null, lowestOrder)
|
|
90
|
-
|
|
91
|
-
store.commit(events.createTask({ title, order }))
|
|
92
|
-
|
|
93
|
-
return order
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Create the very first task in an empty list */
|
|
97
|
-
export const createFirstTask = (title: string) => {
|
|
98
|
-
// When the list is empty, use a simple default value
|
|
99
|
-
const order = 'a1'
|
|
100
|
-
|
|
101
|
-
store.commit(events.createTask({ title, order }))
|
|
102
|
-
|
|
103
|
-
return order
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### `patterns/list-ordering/events.ts`
|
|
108
|
-
|
|
109
|
-
```ts filename="patterns/list-ordering/events.ts"
|
|
110
|
-
|
|
111
|
-
export const events = {
|
|
112
|
-
createTask: Events.synced({
|
|
113
|
-
name: 'v1.CreateTask',
|
|
114
|
-
schema: Schema.Struct({
|
|
115
|
-
title: Schema.String,
|
|
116
|
-
order: Schema.String,
|
|
117
|
-
}),
|
|
118
|
-
}),
|
|
119
|
-
updateTaskOrder: Events.synced({
|
|
120
|
-
name: 'v1.UpdateTaskOrder',
|
|
121
|
-
schema: Schema.Struct({
|
|
122
|
-
id: Schema.Number,
|
|
123
|
-
order: Schema.String,
|
|
124
|
-
}),
|
|
125
|
-
}),
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### `patterns/list-ordering/schema.ts`
|
|
130
|
-
|
|
131
|
-
```ts filename="patterns/list-ordering/schema.ts"
|
|
132
|
-
|
|
133
|
-
export const task = State.SQLite.table({
|
|
134
|
-
name: 'task',
|
|
135
|
-
columns: {
|
|
136
|
-
id: State.SQLite.integer({ primaryKey: true }),
|
|
137
|
-
title: State.SQLite.text({ default: '' }),
|
|
138
|
-
completed: State.SQLite.integer({ default: 0 }),
|
|
139
|
-
/** Fractional index for ordering tasks in the list */
|
|
140
|
-
order: State.SQLite.text({ nullable: false, default: '' }),
|
|
141
|
-
},
|
|
142
|
-
indexes: [
|
|
143
|
-
/** Index for efficient ordering queries */
|
|
144
|
-
{ name: 'task_order', columns: ['order'] },
|
|
145
|
-
],
|
|
146
|
-
deriveEvents: true,
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
export type Task = typeof task.Type
|
|
150
|
-
|
|
151
|
-
export const tables = { task }
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Key patterns:
|
|
155
|
-
- `generateKeyBetween(highest, null)` - Append to end of list
|
|
156
|
-
- `generateKeyBetween(null, lowest)` - Prepend to start of list
|
|
157
|
-
- Use `"a1"` as a simple default for the first item in an empty list
|
|
158
|
-
|
|
159
|
-
## Reordering Items
|
|
160
|
-
|
|
161
|
-
Handle drag-and-drop by generating a new position between the target boundaries:
|
|
162
|
-
|
|
163
|
-
## `patterns/list-ordering/reorder.ts`
|
|
164
|
-
|
|
165
|
-
```ts filename="patterns/list-ordering/reorder.ts"
|
|
166
|
-
|
|
167
|
-
declare const store: Store
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Reorder a task by moving it between two other tasks
|
|
171
|
-
*
|
|
172
|
-
* @param taskId - The task to reorder
|
|
173
|
-
* @param beforeOrder - The order value of the task that will be before this one (null if moving to end)
|
|
174
|
-
* @param afterOrder - The order value of the task that will be after this one (null if moving to start)
|
|
175
|
-
*/
|
|
176
|
-
export const reorderTask = (taskId: number, beforeOrder: string | null, afterOrder: string | null) => {
|
|
177
|
-
// Generate a new fractional index between the two positions
|
|
178
|
-
const newOrder = generateKeyBetween(beforeOrder, afterOrder)
|
|
179
|
-
|
|
180
|
-
// Commit the update event
|
|
181
|
-
store.commit(events.updateTaskOrder({ id: taskId, order: newOrder }))
|
|
182
|
-
|
|
183
|
-
return newOrder
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Handle drag-and-drop reordering
|
|
188
|
-
*
|
|
189
|
-
* This is a more complete example showing how to handle drag-and-drop
|
|
190
|
-
* with proper boundary checks.
|
|
191
|
-
*/
|
|
192
|
-
export const handleDragDrop = (draggedTaskId: number, targetTaskId: number, dropPosition: 'before' | 'after') => {
|
|
193
|
-
const before = dropPosition === 'before'
|
|
194
|
-
|
|
195
|
-
// Get the target task's order
|
|
196
|
-
const targetOrder = store.query(tables.task.select('order').where({ id: targetTaskId }).first({ behaviour: 'error' }))
|
|
197
|
-
|
|
198
|
-
// Find the nearest task in the drop direction
|
|
199
|
-
const nearestOrder =
|
|
200
|
-
store.query(
|
|
201
|
-
tables.task
|
|
202
|
-
.select('order')
|
|
203
|
-
.where({
|
|
204
|
-
order: { op: before ? '>' : '<', value: targetOrder },
|
|
205
|
-
})
|
|
206
|
-
.orderBy('order', before ? 'asc' : 'desc')
|
|
207
|
-
.limit(1),
|
|
208
|
-
)[0] ?? null
|
|
209
|
-
|
|
210
|
-
// Generate new order between target and nearest
|
|
211
|
-
const newOrder = generateKeyBetween(before ? targetOrder : nearestOrder, before ? nearestOrder : targetOrder)
|
|
212
|
-
|
|
213
|
-
// Commit the update
|
|
214
|
-
store.commit(events.updateTaskOrder({ id: draggedTaskId, order: newOrder }))
|
|
215
|
-
|
|
216
|
-
return newOrder
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### `patterns/list-ordering/events.ts`
|
|
221
|
-
|
|
222
|
-
```ts filename="patterns/list-ordering/events.ts"
|
|
223
|
-
|
|
224
|
-
export const events = {
|
|
225
|
-
createTask: Events.synced({
|
|
226
|
-
name: 'v1.CreateTask',
|
|
227
|
-
schema: Schema.Struct({
|
|
228
|
-
title: Schema.String,
|
|
229
|
-
order: Schema.String,
|
|
230
|
-
}),
|
|
231
|
-
}),
|
|
232
|
-
updateTaskOrder: Events.synced({
|
|
233
|
-
name: 'v1.UpdateTaskOrder',
|
|
234
|
-
schema: Schema.Struct({
|
|
235
|
-
id: Schema.Number,
|
|
236
|
-
order: Schema.String,
|
|
237
|
-
}),
|
|
238
|
-
}),
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### `patterns/list-ordering/schema.ts`
|
|
243
|
-
|
|
244
|
-
```ts filename="patterns/list-ordering/schema.ts"
|
|
245
|
-
|
|
246
|
-
export const task = State.SQLite.table({
|
|
247
|
-
name: 'task',
|
|
248
|
-
columns: {
|
|
249
|
-
id: State.SQLite.integer({ primaryKey: true }),
|
|
250
|
-
title: State.SQLite.text({ default: '' }),
|
|
251
|
-
completed: State.SQLite.integer({ default: 0 }),
|
|
252
|
-
/** Fractional index for ordering tasks in the list */
|
|
253
|
-
order: State.SQLite.text({ nullable: false, default: '' }),
|
|
254
|
-
},
|
|
255
|
-
indexes: [
|
|
256
|
-
/** Index for efficient ordering queries */
|
|
257
|
-
{ name: 'task_order', columns: ['order'] },
|
|
258
|
-
],
|
|
259
|
-
deriveEvents: true,
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
export type Task = typeof task.Type
|
|
263
|
-
|
|
264
|
-
export const tables = { task }
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
The `generateKeyBetween` function automatically creates a string value that sorts lexicographically between the two boundary positions. Pass `null` to represent the start or end of the list.
|
|
268
|
-
|
|
269
|
-
## Querying Ordered Lists
|
|
270
|
-
|
|
271
|
-
Query items using standard SQL ordering on the fractional index column:
|
|
272
|
-
|
|
273
|
-
## `patterns/list-ordering/query.ts`
|
|
274
|
-
|
|
275
|
-
```ts filename="patterns/list-ordering/query.ts"
|
|
276
|
-
|
|
277
|
-
declare const store: Store
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Query tasks in their proper order
|
|
281
|
-
*
|
|
282
|
-
* The fractional index values maintain lexicographic ordering,
|
|
283
|
-
* so we can simply order by the 'order' column.
|
|
284
|
-
*/
|
|
285
|
-
export const getOrderedTasks = () => {
|
|
286
|
-
return store.query(tables.task.select().orderBy('order', 'asc'))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Get the highest order value (for appending new items)
|
|
291
|
-
*/
|
|
292
|
-
export const getHighestOrder = (): string | null => {
|
|
293
|
-
const order = store.query(tables.task.select('order').orderBy('order', 'desc').limit(1))[0]
|
|
294
|
-
|
|
295
|
-
return order ?? null
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get the lowest order value (for prepending new items)
|
|
300
|
-
*/
|
|
301
|
-
export const getLowestOrder = (): string | null => {
|
|
302
|
-
const order = store.query(tables.task.select('order').orderBy('order', 'asc').limit(1))[0]
|
|
303
|
-
|
|
304
|
-
return order ?? null
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### `patterns/list-ordering/schema.ts`
|
|
309
|
-
|
|
310
|
-
```ts filename="patterns/list-ordering/schema.ts"
|
|
311
|
-
|
|
312
|
-
export const task = State.SQLite.table({
|
|
313
|
-
name: 'task',
|
|
314
|
-
columns: {
|
|
315
|
-
id: State.SQLite.integer({ primaryKey: true }),
|
|
316
|
-
title: State.SQLite.text({ default: '' }),
|
|
317
|
-
completed: State.SQLite.integer({ default: 0 }),
|
|
318
|
-
/** Fractional index for ordering tasks in the list */
|
|
319
|
-
order: State.SQLite.text({ nullable: false, default: '' }),
|
|
320
|
-
},
|
|
321
|
-
indexes: [
|
|
322
|
-
/** Index for efficient ordering queries */
|
|
323
|
-
{ name: 'task_order', columns: ['order'] },
|
|
324
|
-
],
|
|
325
|
-
deriveEvents: true,
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
export type Task = typeof task.Type
|
|
329
|
-
|
|
330
|
-
export const tables = { task }
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
Since fractional index values maintain lexicographic ordering, you can use simple `ORDER BY` clauses without special handling.
|
|
334
|
-
|
|
335
|
-
## Best Practices
|
|
336
|
-
|
|
337
|
-
- **Use text/string columns** - Fractional index values are strings, not numbers
|
|
338
|
-
- **Add database indexes** - Index the order column for efficient queries
|
|
339
|
-
- **Validate lexicographic ordering** - Ensure your sorting logic uses standard string comparison, not locale-aware comparison (avoid `String.prototype.localeCompare()`)
|
|
340
|
-
- **Handle empty lists** - Use a simple default like `"a1"` for the first item
|
|
341
|
-
- **Consider bulk operations** - For creating multiple items at once, use `generateNKeysBetween(a, b, n)` from the same package
|
|
342
|
-
|
|
343
|
-
## Real-World Example
|
|
344
|
-
|
|
345
|
-
The [web-linearlite example](https://github.com/livestorejs/livestore/tree/dev/examples/web-linearlite) demonstrates fractional indexing for Kanban board ordering. It shows:
|
|
346
|
-
- Schema definition with `kanbanorder` column
|
|
347
|
-
- Creating issues with proper ordering
|
|
348
|
-
- Drag-and-drop reordering across columns
|
|
349
|
-
- Querying issues in order by status
|
|
350
|
-
|
|
351
|
-
Check `examples/web-linearlite/src/livestore/schema/issue.ts` for the schema and `examples/web-linearlite/src/components/column.tsx` for the drag-and-drop implementation.
|
|
352
|
-
|
|
353
|
-
## How It Works
|
|
354
|
-
|
|
355
|
-
Fractional indexing generates position strings that always allow insertion between any two positions:
|
|
356
|
-
|
|
357
|
-
```
|
|
358
|
-
Initial: a0 a1 a2
|
|
359
|
-
Insert between a0 and a1: a0V (sorts: a0 < a0V < a1)
|
|
360
|
-
Insert between a0V and a1: a0n (sorts: a0 < a0V < a0n < a1)
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
The algorithm ensures:
|
|
364
|
-
- Position strings stay reasonably short
|
|
365
|
-
- No coordination needed between clients
|
|
366
|
-
- Conflicts resolve naturally through lexicographic ordering
|
|
367
|
-
- Works seamlessly with LiveStore's event sourcing model
|
|
368
|
-
|
|
369
|
-
For more details on the algorithm, see the [fractional-indexing documentation](https://github.com/rocicorp/fractional-indexing).
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Offline support
|
|
2
|
-
|
|
3
|
-
- LiveStore supports offline data management out of the box. In order to make your app work fully offline, you might need to also consider the following:
|
|
4
|
-
- Design your app in a way to treat the network as an optional feature (e.g. when relying on other APIs / external data)
|
|
5
|
-
- Use service workers to cache assets locally (e.g. images, videos, etc.)
|
|
6
|
-
|
|
7
|
-
## Tracking connectivity
|
|
8
|
-
|
|
9
|
-
Use `store.networkStatus` to react to connectivity transitions. The subscribable emits every time the sync backend connection flips or the devtools latch simulates an offline state.
|
|
10
|
-
|
|
11
|
-
## `patterns/offline/tracking-connectivity.ts`
|
|
12
|
-
|
|
13
|
-
```ts filename="patterns/offline/tracking-connectivity.ts"
|
|
14
|
-
|
|
15
|
-
declare const store: Store
|
|
16
|
-
|
|
17
|
-
// ---cut---
|
|
18
|
-
|
|
19
|
-
const status = await store.networkStatus.pipe(Effect.runPromise)
|
|
20
|
-
if (status.isConnected === false) {
|
|
21
|
-
console.warn('Sync backend offline since', new Date(status.timestampMs))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
await store.networkStatus.changes.pipe(
|
|
25
|
-
Stream.tap((next) => Effect.sync(() => console.log('network status updated', next))),
|
|
26
|
-
Stream.runDrain,
|
|
27
|
-
Effect.scoped,
|
|
28
|
-
Effect.runPromise,
|
|
29
|
-
)
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
When devtools close the sync latch to simulate an offline client, `status.devtools.latchClosed` is `true`, allowing you to differentiate between real and simulated outages. Remember to dispose of long-lived subscriptions using the Effect scope you already manage for your runtime.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# ORM
|
|
2
|
-
|
|
3
|
-
- LiveStore has a built-in query builder which should be sufficient for most simple use cases.
|
|
4
|
-
- You can always fall back to using raw SQL queries if you need more complex queries.
|
|
5
|
-
- As long as the ORM allows supports synchronously generating SQL statements (and binding parameters), you should be able to use it with LiveStore.
|
|
6
|
-
- Supported ORMs:
|
|
7
|
-
- [Knex](https://knexjs.org/)
|
|
8
|
-
- [Kysely](https://kysely.dev/)
|
|
9
|
-
- [Drizzle](https://orm.drizzle.team/)
|
|
10
|
-
- [Objection.js](https://vincit.github.io/objection.js/)
|
|
11
|
-
- Unsupported ORMs:
|
|
12
|
-
- [Prisma](https://www.prisma.io/) (because it's async)
|
|
13
|
-
|
|
14
|
-
## Example
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
// TODO (contribution welcome)
|
|
18
|
-
```
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Presence
|
|
2
|
-
|
|
3
|
-
LiveStore doesn't yet have any built-in presence functionality (e.g. to track online/offline users).
|
|
4
|
-
|
|
5
|
-
Common presence use cases are:
|
|
6
|
-
- Track which users are online / in a room
|
|
7
|
-
- Track which users are typing (e.g. in a chat)
|
|
8
|
-
- Text cursor (similar to Google Docs)
|
|
9
|
-
- Cursor movements (similar to Figma)
|
|
10
|
-
|
|
11
|
-
For now it's recommend to implement presence functionality in your application or use a third party service (e.g. Liveblocks).
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Rich text editing
|
|
2
|
-
|
|
3
|
-
LiveStore doesn't yet have any built-in support for rich text editing. It's currently recommended to use a purpose-built library (e.g. [Yjs](https://yjs.dev/) or [Automerge](https://automerge.org/)) for this use case in combination with LiveStore.
|
|
4
|
-
|
|
5
|
-
The idea here is to reference the rich text document from within LiveStore's event log and sync both in parallel.
|
|
6
|
-
|
|
7
|
-
## Example
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
// TODO
|
|
11
|
-
```
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Server-side clients
|
|
2
|
-
|
|
3
|
-
You can also use LiveStore on the server side e.g. via the `@livestore/adapter-node` adapter. This allows you to:
|
|
4
|
-
- have an up-to-date server-side SQLite database (read model)
|
|
5
|
-
- react to events / state changes on the server side (e.g. to send emails/push notifications)
|
|
6
|
-
- commit events on the server side (e.g. for sensitive/trusted operations)
|
|
7
|
-
|
|
8
|
-
<ServerSideClientDiagram />
|
|
9
|
-
|
|
10
|
-
Note about the schema: While the `events` schema needs to be shared across all clients, the `state` schema can be different for each client (e.g. to allow for a different SQLite table design on the server side).
|
|
11
|
-
|
|
12
|
-
## Example
|
|
13
|
-
|
|
14
|
-
<Tabs>
|
|
15
|
-
<TabItem label="main.ts">
|
|
16
|
-
|
|
17
|
-
## `reference/syncing/server-side-clients/main.ts`
|
|
18
|
-
|
|
19
|
-
```ts filename="reference/syncing/server-side-clients/main.ts"
|
|
20
|
-
/** biome-ignore-all lint/correctness/noUnusedVariables: docs snippet keeps inline setup */
|
|
21
|
-
// ---cut---
|
|
22
|
-
|
|
23
|
-
const adapter = makeAdapter({
|
|
24
|
-
storage: { type: 'fs', baseDirectory: 'tmp' },
|
|
25
|
-
sync: { backend: makeWsSync({ url: 'ws://localhost:8787' }), onSyncError: 'shutdown' },
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const store = await createStorePromise({
|
|
29
|
-
adapter,
|
|
30
|
-
schema,
|
|
31
|
-
storeId: 'test',
|
|
32
|
-
syncPayload: { authToken: 'insecure-token-change-me' },
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const todos = store.query(tables.todos.where({ completed: false }))
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### `reference/syncing/server-side-clients/schema.ts`
|
|
39
|
-
|
|
40
|
-
```ts filename="reference/syncing/server-side-clients/schema.ts"
|
|
41
|
-
|
|
42
|
-
const events = {}
|
|
43
|
-
|
|
44
|
-
const tables = {
|
|
45
|
-
todos: State.SQLite.table({
|
|
46
|
-
name: 'todos',
|
|
47
|
-
columns: {
|
|
48
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
49
|
-
text: State.SQLite.text(),
|
|
50
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
51
|
-
},
|
|
52
|
-
}),
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const state = State.SQLite.makeState({ tables, materializers: {} })
|
|
56
|
-
|
|
57
|
-
export const schema = makeSchema({ events, state })
|
|
58
|
-
|
|
59
|
-
export { tables }
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
</TabItem>
|
|
63
|
-
<TabItem label="schema.ts">
|
|
64
|
-
|
|
65
|
-
## `reference/syncing/server-side-clients/schema.ts`
|
|
66
|
-
|
|
67
|
-
```ts filename="reference/syncing/server-side-clients/schema.ts"
|
|
68
|
-
|
|
69
|
-
const events = {}
|
|
70
|
-
|
|
71
|
-
const tables = {
|
|
72
|
-
todos: State.SQLite.table({
|
|
73
|
-
name: 'todos',
|
|
74
|
-
columns: {
|
|
75
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
76
|
-
text: State.SQLite.text(),
|
|
77
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
78
|
-
},
|
|
79
|
-
}),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const state = State.SQLite.makeState({ tables, materializers: {} })
|
|
83
|
-
|
|
84
|
-
export const schema = makeSchema({ events, state })
|
|
85
|
-
|
|
86
|
-
export { tables }
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
</TabItem>
|
|
90
|
-
</Tabs>
|
|
91
|
-
|
|
92
|
-
## Further notes
|
|
93
|
-
|
|
94
|
-
### Cloudflare Workers
|
|
95
|
-
|
|
96
|
-
- The `@livestore/adapter-node` adapter doesn't yet work with Cloudflare Workers but you can follow [this issue](https://github.com/livestorejs/livestore/issues/266) for a Cloudflare adapter to enable this use case.
|
|
97
|
-
- Having a `@livestore/adapter-cf-worker` adapter could enable serverless server-side client scenarios.
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Side effect
|
|
2
|
-
|
|
3
|
-
TODO: Document how to safely run side-effects as response to LiveStore events.
|
|
4
|
-
|
|
5
|
-
Notes for writing those docs:
|
|
6
|
-
- Scenarios:
|
|
7
|
-
- Run side-effect in each client session
|
|
8
|
-
- Run side-effect only once per client (i.e. use a lock between client sessions)
|
|
9
|
-
- Run side-effect only once globally (will require some kind of global transaction)
|
|
10
|
-
- How to deal with rollbacks/rebases
|
|
11
|
-
- Allow for filtering events based on whether they have been confirmed by the sync backend or include unconfirmed events
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# State machines
|
|
2
|
-
|
|
3
|
-
LiveStore can be used to implement state machines or together with existing state machine libraries (e.g. [XState](https://stately.ai/docs/xstate)).
|
|
4
|
-
|
|
5
|
-
The basic idea is to listen query results and emit events when the query results change. The state machine side effects can then further commit new mutations to LiveStore.
|
|
6
|
-
|
|
7
|
-
## Example
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
// TODO (contribution welcome)
|
|
11
|
-
```
|