@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,65 +0,0 @@
|
|
|
1
|
-
# Build your own sync provider
|
|
2
|
-
|
|
3
|
-
It's very straightforward to implement your own sync provider. A sync provider implementation needs to do the following:
|
|
4
|
-
|
|
5
|
-
## Client-side
|
|
6
|
-
|
|
7
|
-
Implement the `SyncBackend` interface (running in the client) which describes the protocol for syncing events between the client and the server.
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
// Slightly simplified API (see packages/@livestore/common/src/sync/sync.ts for the full API)
|
|
11
|
-
export type SyncBackend = {
|
|
12
|
-
pull: (cursor: EventSequenceNumber) => Stream<{ batch: LiveStoreEvent[] }, InvalidPullError>
|
|
13
|
-
push: (batch: LiveStoreEvent[]) => Effect<void, InvalidPushError>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// my-sync-backend.ts
|
|
17
|
-
const makeMySyncBackend = (args: { /* ... */ }) => {
|
|
18
|
-
return {
|
|
19
|
-
pull: (cursor) => {
|
|
20
|
-
// ...
|
|
21
|
-
},
|
|
22
|
-
push: (batch) => {
|
|
23
|
-
// ...
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// my-app.ts
|
|
29
|
-
const adapter = makeAdapter({
|
|
30
|
-
sync: {
|
|
31
|
-
backend: makeMySyncBackend({ /* ... */ })
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The actual implementation of those methods is left to the developer and mostly depends on the network protocol used to communicate between the client and the server.
|
|
37
|
-
|
|
38
|
-
Ideally this implementation considers the following:
|
|
39
|
-
|
|
40
|
-
- Network connectivity (offline, unstable connection, etc.)
|
|
41
|
-
- Ordering of events in case of out-of-order delivery
|
|
42
|
-
- Backoff and retry logic
|
|
43
|
-
|
|
44
|
-
## Server-side
|
|
45
|
-
|
|
46
|
-
Implement the actual sync backend protocol (running in the server). At minimum this sync backend needs to do the following:
|
|
47
|
-
|
|
48
|
-
- For client `push` requests:
|
|
49
|
-
- Validate the batch of events
|
|
50
|
-
- Ensure the batch sequence numbers are in ascending order and larger than the sync backend head
|
|
51
|
-
- Further validation checks (e.g. schema-aware payload validation)
|
|
52
|
-
- Persist the events in the event store (implying a new sync backend head equal to the sequence number of the pushed last event)
|
|
53
|
-
- Return a success response
|
|
54
|
-
- It's important that the server only processes one push request at a time to ensure a total ordering of events.
|
|
55
|
-
|
|
56
|
-
- For client `pull` requests:
|
|
57
|
-
- Validate the cursor
|
|
58
|
-
- Query the events from the database
|
|
59
|
-
- Return the events to the client
|
|
60
|
-
- This can be done in a batch or streamed to the client
|
|
61
|
-
- `pull` requests can be handled in parallel by the server
|
|
62
|
-
|
|
63
|
-
## General recommendations
|
|
64
|
-
|
|
65
|
-
It's recommended to study the existing sync backend implementations for inspiration.
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# ElectricSQL
|
|
2
|
-
|
|
3
|
-
The `@livestore/sync-electric` package lets you sync LiveStore with ElectricSQL.
|
|
4
|
-
|
|
5
|
-
- Package: `pnpm add @livestore/sync-electric`
|
|
6
|
-
- Protocol: HTTP push/pull with long-polling support
|
|
7
|
-
|
|
8
|
-
## Architecture
|
|
9
|
-
|
|
10
|
-
```mermaid
|
|
11
|
-
graph LR
|
|
12
|
-
subgraph Browser
|
|
13
|
-
LS[LiveStore Client]
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
subgraph "Your Server"
|
|
17
|
-
AP[API Proxy<br/>'/api/electric']
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
subgraph "Infrastructure"
|
|
21
|
-
ES[Electric Server<br/>':30000']
|
|
22
|
-
PG[(Postgres DB)]
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
LS -->|"GET (pull)"| AP
|
|
26
|
-
LS -->|"POST (push)"| AP
|
|
27
|
-
AP -->|"Pull requests<br/>(proxied)"| ES
|
|
28
|
-
AP -->|"Push events<br/>(direct write)"| PG
|
|
29
|
-
ES -->|"Listen"| PG
|
|
30
|
-
|
|
31
|
-
style LS fill:#e1f5fe
|
|
32
|
-
style AP fill:#fff3e0
|
|
33
|
-
style ES fill:#f3e5f5
|
|
34
|
-
style PG fill:#e8f5e9
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The API proxy has dual responsibilities:
|
|
38
|
-
- **Push Events**: Writes events directly to Postgres tables (bypasses Electric)
|
|
39
|
-
- **Pull Requests**: Proxies to Electric server for reading events
|
|
40
|
-
- **Authentication**: Implements your custom auth logic
|
|
41
|
-
- **Database Management**: Initializes tables and manages connections
|
|
42
|
-
|
|
43
|
-
## Client setup
|
|
44
|
-
|
|
45
|
-
Basic usage in your worker/server code:
|
|
46
|
-
|
|
47
|
-
## `reference/syncing/sync-provider/electricsql/client-setup.ts`
|
|
48
|
-
|
|
49
|
-
```ts filename="reference/syncing/sync-provider/electricsql/client-setup.ts"
|
|
50
|
-
|
|
51
|
-
const _backend = makeSyncBackend({
|
|
52
|
-
endpoint: '/api/electric', // Your API proxy endpoint
|
|
53
|
-
ping: { enabled: true },
|
|
54
|
-
})
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## API proxy implementation
|
|
58
|
-
|
|
59
|
-
ElectricSQL requires an API proxy on your server to handle authentication and database operations. Your proxy needs two endpoints:
|
|
60
|
-
|
|
61
|
-
### Minimal implementation example
|
|
62
|
-
|
|
63
|
-
## `reference/syncing/sync-provider/electricsql/api-proxy.ts`
|
|
64
|
-
|
|
65
|
-
```ts filename="reference/syncing/sync-provider/electricsql/api-proxy.ts"
|
|
66
|
-
|
|
67
|
-
const electricHost = 'http://localhost:30000' // Your Electric server
|
|
68
|
-
|
|
69
|
-
/** Placeholder for your database factory function */
|
|
70
|
-
declare const makeDb: (storeId: string) => {
|
|
71
|
-
migrate: () => Promise<void>
|
|
72
|
-
disconnect: () => Promise<void>
|
|
73
|
-
createEvents: (batch: (typeof ApiSchema.PushPayload.Type)['batch']) => Promise<void>
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---cut---
|
|
77
|
-
|
|
78
|
-
// GET /api/electric - Pull events (proxied through Electric)
|
|
79
|
-
export async function GET(request: Request) {
|
|
80
|
-
const searchParams = new URL(request.url).searchParams
|
|
81
|
-
const { url, storeId, needsInit } = makeElectricUrl({
|
|
82
|
-
electricHost,
|
|
83
|
-
searchParams,
|
|
84
|
-
apiSecret: 'your-electric-secret',
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Add your authentication logic here
|
|
88
|
-
// if (!isAuthenticated(request)) {
|
|
89
|
-
// return new Response('Unauthorized', { status: 401 })
|
|
90
|
-
// }
|
|
91
|
-
|
|
92
|
-
// Initialize database tables if needed
|
|
93
|
-
if (needsInit) {
|
|
94
|
-
const db = makeDb(storeId)
|
|
95
|
-
await db.migrate()
|
|
96
|
-
await db.disconnect()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Proxy pull request to Electric server for reading
|
|
100
|
-
return fetch(url)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// POST /api/electric - Push events (direct database write)
|
|
104
|
-
export async function POST(request: Request) {
|
|
105
|
-
const payload = await request.json()
|
|
106
|
-
const parsed = Schema.decodeUnknownSync(ApiSchema.PushPayload)(payload)
|
|
107
|
-
|
|
108
|
-
// Write events directly to Postgres table (bypasses Electric)
|
|
109
|
-
const db = makeDb(parsed.storeId)
|
|
110
|
-
await db.createEvents(parsed.batch)
|
|
111
|
-
await db.disconnect()
|
|
112
|
-
|
|
113
|
-
return Response.json({ success: true })
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Important considerations
|
|
118
|
-
|
|
119
|
-
- **Database Setup**: Ensure your Postgres database is configured for Electric
|
|
120
|
-
- **Authentication**: Implement proper auth checks in your proxy
|
|
121
|
-
- **Error Handling**: Add robust error handling for database operations
|
|
122
|
-
- **Connection Management**: Properly manage database connections
|
|
123
|
-
|
|
124
|
-
## Example
|
|
125
|
-
|
|
126
|
-
See the
|
|
127
|
-
[todomvc-sync-electric](https://github.com/livestorejs/livestore/tree/main/examples/web-todomvc-sync-electric)
|
|
128
|
-
example for a complete implementation.
|
|
129
|
-
|
|
130
|
-
## How the sync provider works
|
|
131
|
-
|
|
132
|
-
The initial version of the ElectricSQL sync provider will use the server-side
|
|
133
|
-
Postgres DB as a store for the mutation event history.
|
|
134
|
-
|
|
135
|
-
Events are stored in a table following the pattern
|
|
136
|
-
`eventlog_${PERSISTENCE_FORMAT_VERSION}_${storeId}` where
|
|
137
|
-
`PERSISTENCE_FORMAT_VERSION` is a number that is incremented whenever the
|
|
138
|
-
`sync-electric` internal storage format changes.
|
|
139
|
-
|
|
140
|
-
## F.A.Q.
|
|
141
|
-
|
|
142
|
-
### Can I use my existing Postgres database with the sync provider?
|
|
143
|
-
|
|
144
|
-
Unless the database is already modelled as a eventlog following the
|
|
145
|
-
`@livestore/sync-electric` storage format, you won't be able to easily use your
|
|
146
|
-
existing database with this sync backend implementation.
|
|
147
|
-
|
|
148
|
-
We might support this use case in the future, you can follow the progress
|
|
149
|
-
[here](https://github.com/livestorejs/livestore/issues/286). Please share any
|
|
150
|
-
feedback you have on this use case there.
|
|
151
|
-
|
|
152
|
-
### Why do I need an API proxy in front of the ElectricSQL server?
|
|
153
|
-
|
|
154
|
-
The API proxy is used to handle pull/push requests between LiveStore and ElectricSQL,
|
|
155
|
-
allowing you to implement custom logic such as:
|
|
156
|
-
- Authentication and authorization
|
|
157
|
-
- Rate limiting and quota management
|
|
158
|
-
- Database initialization and migration
|
|
159
|
-
- Custom business logic and validation
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# S2
|
|
2
|
-
|
|
3
|
-
export const CODE = {
|
|
4
|
-
recordStructureExample: recordStructureExampleCode,
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
The `@livestore/sync-s2` package lets you sync LiveStore with the official S2 backend (s2.dev).
|
|
8
|
-
|
|
9
|
-
- Package: `pnpm add @livestore/sync-s2`
|
|
10
|
-
- Protocol: HTTP push/pull, live pull via SSE
|
|
11
|
-
|
|
12
|
-
## Architecture
|
|
13
|
-
|
|
14
|
-
```mermaid
|
|
15
|
-
graph LR
|
|
16
|
-
subgraph Browser
|
|
17
|
-
LS[LiveStore Client]
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
subgraph "Your Server"
|
|
21
|
-
AP[API Proxy<br/>'/api/s2']
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
subgraph "S2 Cloud"
|
|
25
|
-
S2[S2 Backend<br/>'*.s2.dev']
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
LS -->|"GET (pull)<br/>POST (push)<br/>HEAD (ping)"| AP
|
|
29
|
-
AP -->|"Authenticated<br/>Requests"| S2
|
|
30
|
-
|
|
31
|
-
style LS fill:#e1f5fe
|
|
32
|
-
style AP fill:#fff3e0
|
|
33
|
-
style S2 fill:#f3e5f5
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The API proxy handles:
|
|
37
|
-
- **Business logic**: Any kind of business logic that is specific to your application (e.g. rate limiting, auth, logging, etc.)
|
|
38
|
-
- **S2 Stream Management**: Creates basins and streams as needed
|
|
39
|
-
- **S2 Request Translation**: Converts LiveStore sync operations to authenticated S2 API calls
|
|
40
|
-
|
|
41
|
-
## Client setup
|
|
42
|
-
|
|
43
|
-
Basic usage in your worker/server code:
|
|
44
|
-
|
|
45
|
-
## `reference/syncing/s2/client-setup.ts`
|
|
46
|
-
|
|
47
|
-
```ts filename="reference/syncing/s2/client-setup.ts"
|
|
48
|
-
|
|
49
|
-
const _backend = makeSyncBackend({
|
|
50
|
-
endpoint: '/api/s2', // Your API proxy endpoint
|
|
51
|
-
// more options...
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## API proxy implementation
|
|
56
|
-
|
|
57
|
-
S2 requires authentication and stream management that can't be handled directly from the browser. You'll need to implement an API proxy on your server that:
|
|
58
|
-
|
|
59
|
-
1. **Handles authentication** with S2 using your access token
|
|
60
|
-
2. **Manages basins and streams** (creates them if they don't exist)
|
|
61
|
-
3. **Proxies requests** between LiveStore and S2
|
|
62
|
-
|
|
63
|
-
Your proxy needs three endpoints:
|
|
64
|
-
|
|
65
|
-
### Using helper functions
|
|
66
|
-
|
|
67
|
-
The `@livestore/sync-s2` package provides helper functions to simplify the proxy implementation:
|
|
68
|
-
|
|
69
|
-
## `reference/syncing/s2/api-proxy-implementation.ts`
|
|
70
|
-
|
|
71
|
-
```ts filename="reference/syncing/s2/api-proxy-implementation.ts"
|
|
72
|
-
|
|
73
|
-
// Configure S2 connection
|
|
74
|
-
const s2Config: S2Helpers.S2Config = {
|
|
75
|
-
basin: process.env.S2_BASIN ?? 'your-basin',
|
|
76
|
-
token: process.env.S2_ACCESS_TOKEN!, // Your S2 access token
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// HEAD /api/s2 - Health check/ping
|
|
80
|
-
export async function HEAD() {
|
|
81
|
-
return new Response(null, { status: 200 })
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// GET /api/s2 - Pull events
|
|
85
|
-
export async function GET(request: Request) {
|
|
86
|
-
const url = new URL(request.url)
|
|
87
|
-
const args = S2.decodePullArgsFromSearchParams(url.searchParams)
|
|
88
|
-
const streamName = S2.makeS2StreamName(args.storeId)
|
|
89
|
-
|
|
90
|
-
// Ensure basin and stream exist
|
|
91
|
-
await S2Helpers.ensureBasin(s2Config)
|
|
92
|
-
await S2Helpers.ensureStream(s2Config, streamName)
|
|
93
|
-
|
|
94
|
-
// Build request with appropriate headers and URL
|
|
95
|
-
// Note: buildPullRequest handles cursor+1 conversion internally
|
|
96
|
-
const { url: pullUrl, headers } = S2Helpers.buildPullRequest({ config: s2Config, args })
|
|
97
|
-
|
|
98
|
-
const res = await fetch(pullUrl, { headers })
|
|
99
|
-
|
|
100
|
-
// For live pulls (SSE), proxy the response
|
|
101
|
-
if (args.live === true) {
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
return S2Helpers.sseKeepAliveResponse()
|
|
104
|
-
}
|
|
105
|
-
return new Response(res.body, {
|
|
106
|
-
status: 200,
|
|
107
|
-
headers: { 'content-type': 'text/event-stream' },
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// For regular pulls
|
|
112
|
-
if (!res.ok) {
|
|
113
|
-
return S2Helpers.emptyBatchResponse()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const batch = await res.text()
|
|
117
|
-
return new Response(batch, {
|
|
118
|
-
headers: { 'content-type': 'application/json' },
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// POST /api/s2 - Push events
|
|
123
|
-
export async function POST(request: Request) {
|
|
124
|
-
const requestBody = await request.json()
|
|
125
|
-
const parsed = Schema.decodeUnknownSync(S2.ApiSchema.PushPayload)(requestBody)
|
|
126
|
-
const streamName = S2.makeS2StreamName(parsed.storeId)
|
|
127
|
-
|
|
128
|
-
// Ensure basin and stream exist
|
|
129
|
-
await S2Helpers.ensureBasin(s2Config)
|
|
130
|
-
await S2Helpers.ensureStream(s2Config, streamName)
|
|
131
|
-
|
|
132
|
-
// Build push request with proper formatting
|
|
133
|
-
const pushRequests = S2Helpers.buildPushRequests({
|
|
134
|
-
config: s2Config,
|
|
135
|
-
storeId: parsed.storeId,
|
|
136
|
-
batch: parsed.batch,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
for (const pushRequest of pushRequests) {
|
|
140
|
-
const res = await fetch(pushRequest.url, {
|
|
141
|
-
method: 'POST',
|
|
142
|
-
headers: pushRequest.headers,
|
|
143
|
-
body: pushRequest.body,
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
return S2Helpers.errorResponse('Push failed', 500)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return S2Helpers.successResponse()
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Cursor semantics
|
|
156
|
-
|
|
157
|
-
The S2 sync provider uses a cursor that represents the **last processed record**:
|
|
158
|
-
- The cursor points to the last S2 sequence number we've seen
|
|
159
|
-
- S2's `seq_num` parameter expects where to start reading from (inclusive)
|
|
160
|
-
- The helper functions automatically handle the `+1` conversion: `seq_num = cursor + 1`
|
|
161
|
-
- When starting from the beginning, cursor is `'from-start'` which maps to `seq_num = 0`
|
|
162
|
-
|
|
163
|
-
### Important considerations
|
|
164
|
-
|
|
165
|
-
- **Stream provisioning**: The helper functions provide `ensureBasin()` and `ensureStream()` to handle creation automatically.
|
|
166
|
-
- **Error handling**: The helpers include fallback responses (`emptyBatchResponse()`, `sseKeepAliveResponse()`) to maintain stream continuity during errors.
|
|
167
|
-
- **Authentication**: Store your S2 access token securely (e.g., environment variables).
|
|
168
|
-
- **Rate limiting**: Consider implementing rate limiting to protect your S2 quota.
|
|
169
|
-
- **Response helpers**: Use the provided response helpers (`successResponse()`, `errorResponse()`) for consistent API responses.
|
|
170
|
-
|
|
171
|
-
## Live pull (SSE)
|
|
172
|
-
|
|
173
|
-
S2 provider supports live pulls over Server-Sent Events (SSE). When `live: true` is passed to `pull`, the client:
|
|
174
|
-
- Immediately emits one page (possibly empty) with `pageInfo: NoMore`.
|
|
175
|
-
- Parses SSE frames robustly (multi-line `data:` support) and reacts to typed events:
|
|
176
|
-
- `event: batch` → parses `data` as S2 `ReadBatch` and emits items.
|
|
177
|
-
- `event: ping` → ignored; keeps the stream alive.
|
|
178
|
-
- `event: error` → mapped to `InvalidPullError`.
|
|
179
|
-
|
|
180
|
-
## Implementation notes
|
|
181
|
-
|
|
182
|
-
### Data storage & encoding
|
|
183
|
-
|
|
184
|
-
LiveStore leverages S2 streams for durable event storage. Understanding the mapping between LiveStore concepts and S2 primitives helps developers comprehend the persistence layer, though direct manipulation is discouraged.
|
|
185
|
-
|
|
186
|
-
#### LiveStore → S2 mapping
|
|
187
|
-
|
|
188
|
-
**Store to Stream**: Each LiveStore `storeId` maps to exactly one S2 stream. The stream name is derived from the `storeId` after sanitization to meet S2 naming requirements.
|
|
189
|
-
|
|
190
|
-
**Event Encoding**: LiveStore events (`AnyEncodedGlobal`) are JSON-serialized and stored as the `body` field of S2 records. Each event contains:
|
|
191
|
-
- `name`: Event type identifier
|
|
192
|
-
- `args`: Event-specific payload data
|
|
193
|
-
- `seqNum`: LiveStore's global event sequence number
|
|
194
|
-
- `parentSeqNum`: Previous event's sequence number for ordering
|
|
195
|
-
- `clientId`: Origin client identifier
|
|
196
|
-
- `sessionId`: Session that created the event
|
|
197
|
-
|
|
198
|
-
**Record Structure**: When pushed to S2, each LiveStore event becomes one S2 record:
|
|
199
|
-
|
|
200
|
-
<Code lang="json" code={CODE.recordStructureExample} title="S2 Record Example" />
|
|
201
|
-
|
|
202
|
-
#### Sequence number handling
|
|
203
|
-
|
|
204
|
-
**LiveStore and S2 maintain completely independent sequence numbering systems**:
|
|
205
|
-
|
|
206
|
-
- **LiveStore's `seqNum`**: Stored inside the JSON event payload (starts at 0). Used for logical event ordering and cursor management within LiveStore.
|
|
207
|
-
- **S2's `seq_num`**: Assigned by S2 to each record in the stream (also starts at 0). Used solely for stream positioning when reading records.
|
|
208
|
-
|
|
209
|
-
These are **two separate numbering systems** that happen to both start at 0. While they often align numerically (first event is LiveStore seqNum 0, stored in S2 record with seq_num 0), this is coincidental rather than a direct mapping. The sync provider:
|
|
210
|
-
- Preserves LiveStore's sequence numbers unchanged in the event payload
|
|
211
|
-
- Uses S2's seq_num only for querying records from the stream (e.g., "read from position X")
|
|
212
|
-
- Never relies on S2's seq_num for LiveStore's logical event ordering
|
|
213
|
-
|
|
214
|
-
#### Technical details
|
|
215
|
-
|
|
216
|
-
**Format**: The provider uses `s2-format: raw` when communicating with S2, treating record bodies as UTF-8 JSON strings.
|
|
217
|
-
|
|
218
|
-
**Headers**: S2 record headers are not utilized; all LiveStore metadata is contained within the JSON body.
|
|
219
|
-
|
|
220
|
-
**Batch Operations**: Multiple events can be pushed in a single batch, with each event becoming a separate S2 record while maintaining order.
|
|
221
|
-
|
|
222
|
-
#### Important note
|
|
223
|
-
|
|
224
|
-
**Direct stream manipulation is strongly discouraged**. Always interact with S2 streams through LiveStore's sync provider to ensure:
|
|
225
|
-
- Proper event encoding/decoding
|
|
226
|
-
- Sequence number integrity
|
|
227
|
-
- Cursor management consistency
|
|
228
|
-
- Compatibility with LiveStore's sync protocol
|
|
229
|
-
|
|
230
|
-
Bypassing LiveStore to modify S2 streams directly may corrupt the event log and break synchronization.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Overview and prerequisites
|
|
2
|
-
|
|
3
|
-
## Welcome to the LiveStore tutorial
|
|
4
|
-
|
|
5
|
-
This tutorial will guide you through the process of building a simple todo app with React, Vite & Tailwind, using LiveStore as its data layer.
|
|
6
|
-
|
|
7
|
-
It is focused on building a _minimalistic_ application to gradually introduce the main concepts of LiveStore. That being said, LiveStore itself has been specifically designed for large, complex applications, and shines especially when used in these contexts.
|
|
8
|
-
|
|
9
|
-
:::note[Goal of this tutorial: Education]
|
|
10
|
-
The goal of the tutorial is to _educate_. It reduces as much noise as possible so you can focus on the parts that actually matter for building an application with LiveStore.
|
|
11
|
-
|
|
12
|
-
**If you just want to see LiveStore in action and play around with it, consider setting up the [starter project](/getting-started/react-web/) directly.**
|
|
13
|
-
:::
|
|
14
|
-
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
### Useful knowledge
|
|
18
|
-
|
|
19
|
-
While the tutorial is aimed at LiveStore newcomers, it will be helpful to have some knowledge in basic areas of web development, such as of [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) and [event-driven architectures](/overview/how-livestore-works#event-sourcing).
|
|
20
|
-
|
|
21
|
-
### Technical setup
|
|
22
|
-
|
|
23
|
-
- The tutorial assumes that you're using a Unix-like shell, e.g. by using commands like `mkdir` and `touch` for creating directories and files.
|
|
24
|
-
- It gives you the option to use either [Bun](https://bun.sh/) or [`pnpm`](https://pnpm.io/) as the package manager.
|
|
25
|
-
- You'll deploy the application to Cloudflare. If you don't have an account there yet, you can sign up for a free one [here](https://dash.cloudflare.com/sign-up) (or skip the deployment steps in the tutorial).
|
|
26
|
-
|
|
27
|
-
## What you'll do
|
|
28
|
-
|
|
29
|
-
Here's an overview of the steps you'll take in the tutorial:
|
|
30
|
-
|
|
31
|
-
1. Set up a starter project with React, Vite & Tailwind
|
|
32
|
-
- This project will be used as a starting point for the tutorial and already comes with basic functionality for a todo app.
|
|
33
|
-
- It uses local React state to manage the todo list. Throughout the tutorial you'll gradually replace the ephemeral, local state with LiveStore persistent storage.
|
|
34
|
-
1. Deploy the application to Cloudflare
|
|
35
|
-
- This enables you to observe the evolution in behaviour as you're introducing LiveStore.
|
|
36
|
-
1. Add LiveStore to the project
|
|
37
|
-
- You'll add LiveStore dependencies to the project and implement persisting the todos so that they survive a page refresh.
|
|
38
|
-
1. Automatically sync data to Cloudflare
|
|
39
|
-
- You'll use LiveStore to set up syncing of the todo list data via Cloudflare Workers and Durable Objects in the background.
|
|
40
|
-
- Now your todos will not only survive a page refresh, but also automatically sync across browser sessions, and even across devices.
|
|
41
|
-
1. Expand the business logic with more LiveStore events
|
|
42
|
-
- You'll learn how to use LiveStore events to expand the business logic of the todo app.
|
|
43
|
-
1. Persist UI state
|
|
44
|
-
- LiveStore can also be used to persist UI state, such as the text from an input field or a filter selection.
|
|
45
|
-
|
|
46
|
-
## Credits
|
|
47
|
-
|
|
48
|
-
This tutorial has been written by [Nikolas Burk](https://x.com/nikolasburk).
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# 1. Set up starter project with React, Vite & Tailwind
|
|
2
|
-
|
|
3
|
-
## Set up a starter project with React, Vite & Tailwind
|
|
4
|
-
|
|
5
|
-
We have prepared a [starter project](https://github.com/livestorejs/livestore/tree/dev/examples/tutorial-starter) for you that you can use as a starting point for the tutorial. Download it via the following command:
|
|
6
|
-
|
|
7
|
-
<Tabs syncKey="package-manager">
|
|
8
|
-
|
|
9
|
-
<TabItem label="bun">
|
|
10
|
-
|
|
11
|
-
<Code code={`bunx @livestore/cli@dev create \\\n --example tutorial-starter livestore-todo-app`} lang="sh" />
|
|
12
|
-
|
|
13
|
-
</TabItem>
|
|
14
|
-
|
|
15
|
-
<TabItem label="pnpm">
|
|
16
|
-
|
|
17
|
-
<Code code={`pnpm dlx @livestore/cli@dev create \\\n --example tutorial-starter livestore-todo-app`} lang="sh" />
|
|
18
|
-
|
|
19
|
-
</TabItem>
|
|
20
|
-
|
|
21
|
-
</Tabs>
|
|
22
|
-
|
|
23
|
-
Once you've downloaded the project, you can navigate to the project directory and install the dependencies:
|
|
24
|
-
|
|
25
|
-
The project currently is set up as follows:
|
|
26
|
-
- Minimal project created via [`vite create`](https://vite.dev/guide/#scaffolding-your-first-vite-project) using React and TypeScript.
|
|
27
|
-
- Using [Tailwind CSS](https://tailwindcss.com/) for styling.
|
|
28
|
-
- Has basic functionality for adding and deleting todos via local [`React.useState`](https://react.dev/learn/state-a-components-memory).
|
|
29
|
-
|
|
30
|
-
## Understand the current project state
|
|
31
|
-
|
|
32
|
-
Run the app with:
|
|
33
|
-
|
|
34
|
-
<Tabs syncKey="package-manager">
|
|
35
|
-
|
|
36
|
-
<TabItem label="bun">
|
|
37
|
-
|
|
38
|
-
<Code code={`bun dev`} lang="sh" />
|
|
39
|
-
|
|
40
|
-
</TabItem>
|
|
41
|
-
|
|
42
|
-
<TabItem label="pnpm">
|
|
43
|
-
|
|
44
|
-
<Code code={`pnpm dev`} lang="sh" />
|
|
45
|
-
|
|
46
|
-
</TabItem>
|
|
47
|
-
|
|
48
|
-
</Tabs>
|
|
49
|
-
|
|
50
|
-
Here's the UI you're going to see after adding a few todos:
|
|
51
|
-
|
|
52
|
-

|
|
53
|
-
|
|
54
|
-
Let's take a quick moment to understand how the app is currently implemented:
|
|
55
|
-
|
|
56
|
-
All relevant code lives in `App.tsx`. Here's a simplified version of it:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
interface Todo {
|
|
60
|
-
id: number
|
|
61
|
-
text: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function App() {
|
|
65
|
-
const [todos, setTodos] = useState<Todo[]>([])
|
|
66
|
-
const [input, setInput] = useState('')
|
|
67
|
-
|
|
68
|
-
const addTodo = () => {
|
|
69
|
-
const newTodo: Todo = {
|
|
70
|
-
id: Date.now(),
|
|
71
|
-
text: input
|
|
72
|
-
}
|
|
73
|
-
setTodos([...todos, newTodo])
|
|
74
|
-
setInput('')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const deleteTodo = (id: number) => {
|
|
78
|
-
setTodos(todos.filter(todo => todo.id !== id))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
// Render input text field and todo list ...
|
|
83
|
-
// ... and invoke `addTodo` and `deleteTodo`
|
|
84
|
-
// ... when the buttons are clicked.
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
For any React developer, this is a very familiar setup:
|
|
90
|
-
|
|
91
|
-
<StateAndUiDiagram class="my-8" />
|
|
92
|
-
|
|
93
|
-
You have two pieces of state:
|
|
94
|
-
- application state: `todos: Todo[]` → manipulated by the `addTodo` and `deleteTodo` functions.
|
|
95
|
-
- UI state: `input: string` → manipulated when the text in the input field changes.
|
|
96
|
-
|
|
97
|
-
The "problem" with this code is that the todo items are not _persisted_, meaning they vanish when:
|
|
98
|
-
- the page is refreshed in the browser.
|
|
99
|
-
- the development server is restarted.
|
|
100
|
-
|
|
101
|
-
In the next chapters, you'll learn how to persist the todos in the list, so that they'll "survive" both actions.
|
|
102
|
-
|
|
103
|
-
Even more: They will not only persist, they will automatically sync across multiple browsers tabs/windows, and even across devices—without you needing to think about the syncing logic and managing remote state.
|
|
104
|
-
|
|
105
|
-
That's the power of LiveStore!
|