@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,1495 +0,0 @@
|
|
|
1
|
-
# Effect
|
|
2
|
-
|
|
3
|
-
LiveStore itself is built on top of [Effect](https://effect.website) which is a powerful library to write production-grade TypeScript code. It's also possible (and recommended) to use Effect directly in your application code.
|
|
4
|
-
|
|
5
|
-
## Schema
|
|
6
|
-
|
|
7
|
-
LiveStore uses the [Effect Schema](https://effect.website/docs/schema/introduction/) library to define schemas for the following:
|
|
8
|
-
|
|
9
|
-
- Read model table column definitions
|
|
10
|
-
- Event event payloads definitions
|
|
11
|
-
- Query response types
|
|
12
|
-
|
|
13
|
-
For convenience, LiveStore re-exports the `Schema` module from the `effect` package, which is the same as if you'd import it via `import { Schema } from 'effect'` directly.
|
|
14
|
-
|
|
15
|
-
## `Equal` and `Hash` Traits
|
|
16
|
-
|
|
17
|
-
LiveStore's reactive primitives (`LiveQueryDef` and `SignalDef`) implement Effect's `Equal` and `Hash` traits, enabling efficient integration with Effect's data structures and collections.
|
|
18
|
-
|
|
19
|
-
## Effect atom integration
|
|
20
|
-
|
|
21
|
-
LiveStore integrates seamlessly with [Effect Atom](https://github.com/effect-atom/effect-atom) for reactive state management in React applications. This provides a powerful combination of Effect's functional programming capabilities with LiveStore's event sourcing and CQRS patterns.
|
|
22
|
-
|
|
23
|
-
Effect Atom is an external package developed by [Tim Smart](https://github.com/tim-smart) that provides a more Effect-idiomatic alternative to the `@livestore/react` package. While `@livestore/react` offers a straightforward React integration, Effect Atom leverages Effect API/patterns throughout, making it a natural choice for applications already using Effect.
|
|
24
|
-
|
|
25
|
-
### Installation
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pnpm install @effect-atom/atom-livestore @effect-atom/atom-react
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Store creation
|
|
32
|
-
|
|
33
|
-
Create a LiveStore-backed atom store with persistence and worker support using the `AtomLivestore.Tag` pattern:
|
|
34
|
-
|
|
35
|
-
## `patterns/effect/store-setup/atoms.ts`
|
|
36
|
-
|
|
37
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
38
|
-
|
|
39
|
-
export { schema } from './schema.ts'
|
|
40
|
-
|
|
41
|
-
// Create a persistent adapter with OPFS storage
|
|
42
|
-
const adapter = makePersistedAdapter({
|
|
43
|
-
storage: { type: 'opfs' },
|
|
44
|
-
worker: LiveStoreWorker,
|
|
45
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// Define the store as a service tag
|
|
49
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
50
|
-
schema,
|
|
51
|
-
storeId: 'default',
|
|
52
|
-
adapter,
|
|
53
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
54
|
-
}) {}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
58
|
-
|
|
59
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
60
|
-
|
|
61
|
-
// Define event payloads
|
|
62
|
-
export const events = {
|
|
63
|
-
userCreated: Events.clientOnly({
|
|
64
|
-
name: 'userCreated',
|
|
65
|
-
schema: Schema.Struct({
|
|
66
|
-
id: Schema.String,
|
|
67
|
-
name: Schema.String,
|
|
68
|
-
email: Schema.String,
|
|
69
|
-
}),
|
|
70
|
-
}),
|
|
71
|
-
userUpdated: Events.clientOnly({
|
|
72
|
-
name: 'userUpdated',
|
|
73
|
-
schema: Schema.Struct({
|
|
74
|
-
id: Schema.String,
|
|
75
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
76
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
77
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
78
|
-
}),
|
|
79
|
-
}),
|
|
80
|
-
productCreated: Events.clientOnly({
|
|
81
|
-
name: 'productCreated',
|
|
82
|
-
schema: Schema.Struct({
|
|
83
|
-
id: Schema.String,
|
|
84
|
-
name: Schema.String,
|
|
85
|
-
description: Schema.String,
|
|
86
|
-
price: Schema.Number,
|
|
87
|
-
}),
|
|
88
|
-
}),
|
|
89
|
-
productUpdated: Events.clientOnly({
|
|
90
|
-
name: 'productUpdated',
|
|
91
|
-
schema: Schema.Struct({
|
|
92
|
-
id: Schema.String,
|
|
93
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
94
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
95
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
96
|
-
}),
|
|
97
|
-
}),
|
|
98
|
-
todoCreated: Events.clientOnly({
|
|
99
|
-
name: 'todoCreated',
|
|
100
|
-
schema: Schema.Struct({
|
|
101
|
-
id: Schema.String,
|
|
102
|
-
text: Schema.String,
|
|
103
|
-
completed: Schema.Boolean,
|
|
104
|
-
}),
|
|
105
|
-
}),
|
|
106
|
-
todoToggled: Events.clientOnly({
|
|
107
|
-
name: 'todoToggled',
|
|
108
|
-
schema: Schema.Struct({
|
|
109
|
-
id: Schema.String,
|
|
110
|
-
completed: Schema.Boolean,
|
|
111
|
-
}),
|
|
112
|
-
}),
|
|
113
|
-
itemCreated: Events.clientOnly({
|
|
114
|
-
name: 'itemCreated',
|
|
115
|
-
schema: Schema.Struct({
|
|
116
|
-
id: Schema.String,
|
|
117
|
-
name: Schema.String,
|
|
118
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
119
|
-
}),
|
|
120
|
-
}),
|
|
121
|
-
itemUpdated: Events.clientOnly({
|
|
122
|
-
name: 'itemUpdated',
|
|
123
|
-
schema: Schema.Struct({
|
|
124
|
-
id: Schema.String,
|
|
125
|
-
status: Schema.String,
|
|
126
|
-
}),
|
|
127
|
-
}),
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Define tables
|
|
131
|
-
const tables = {
|
|
132
|
-
users: State.SQLite.table({
|
|
133
|
-
name: 'users',
|
|
134
|
-
columns: {
|
|
135
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
136
|
-
name: State.SQLite.text(),
|
|
137
|
-
email: State.SQLite.text(),
|
|
138
|
-
isActive: State.SQLite.boolean(),
|
|
139
|
-
createdAt: State.SQLite.datetime(),
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
products: State.SQLite.table({
|
|
143
|
-
name: 'products',
|
|
144
|
-
columns: {
|
|
145
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
146
|
-
name: State.SQLite.text(),
|
|
147
|
-
description: State.SQLite.text(),
|
|
148
|
-
price: State.SQLite.real(),
|
|
149
|
-
createdAt: State.SQLite.datetime(),
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
todos: State.SQLite.table({
|
|
153
|
-
name: 'todos',
|
|
154
|
-
columns: {
|
|
155
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
156
|
-
text: State.SQLite.text(),
|
|
157
|
-
completed: State.SQLite.boolean(),
|
|
158
|
-
createdAt: State.SQLite.datetime(),
|
|
159
|
-
},
|
|
160
|
-
}),
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Define materializers
|
|
164
|
-
const materializers = State.SQLite.materializers(events, {
|
|
165
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
166
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
167
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
168
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
169
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
170
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
171
|
-
return tables.users.update(updates).where({ id })
|
|
172
|
-
},
|
|
173
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
174
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
175
|
-
productCreated: ({ id, name, description, price }) =>
|
|
176
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
177
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
178
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
179
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
180
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
181
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
182
|
-
return tables.products.update(updates).where({ id })
|
|
183
|
-
},
|
|
184
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
185
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
// Create state
|
|
189
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
190
|
-
|
|
191
|
-
// Create the store schema
|
|
192
|
-
export const schema = makeSchema({ events, state })
|
|
193
|
-
|
|
194
|
-
export { tables }
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
The `StoreTag` class provides the following static methods:
|
|
198
|
-
- `StoreTag.runtime` - Access to Effect runtime
|
|
199
|
-
- `StoreTag.commit` - Commit events to the store
|
|
200
|
-
- `StoreTag.store` - Access store with Effect
|
|
201
|
-
- `StoreTag.storeUnsafe` - Direct store access when store is already loaded (synchronous)
|
|
202
|
-
- `StoreTag.makeQuery` - Create query atoms with Effect
|
|
203
|
-
- `StoreTag.makeQueryUnsafe` - Create query atoms without Effect
|
|
204
|
-
|
|
205
|
-
### Defining query atoms
|
|
206
|
-
|
|
207
|
-
Create reactive query atoms that automatically update when the underlying data changes:
|
|
208
|
-
|
|
209
|
-
## `patterns/effect/store-setup/queries.ts`
|
|
210
|
-
|
|
211
|
-
```ts filename="patterns/effect/store-setup/queries.ts"
|
|
212
|
-
|
|
213
|
-
// User schema for type safety
|
|
214
|
-
const User = Schema.Struct({
|
|
215
|
-
id: Schema.String,
|
|
216
|
-
name: Schema.String,
|
|
217
|
-
isActive: Schema.Boolean,
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
const Product = Schema.Struct({
|
|
221
|
-
id: Schema.String,
|
|
222
|
-
name: Schema.String,
|
|
223
|
-
createdAt: Schema.DateTimeUtc,
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// Search term atom for dynamic queries
|
|
227
|
-
export const searchTermAtom = Atom.make<string>('')
|
|
228
|
-
|
|
229
|
-
// Re-export from utils for convenience
|
|
230
|
-
export { usersQueryAtom as usersAtom } from './utils.ts'
|
|
231
|
-
|
|
232
|
-
// Query with SQL
|
|
233
|
-
export const activeUsersAtom = StoreTag.makeQuery(
|
|
234
|
-
queryDb({
|
|
235
|
-
query: sql`SELECT * FROM users WHERE isActive = true ORDER BY name`,
|
|
236
|
-
schema: Schema.Array(User),
|
|
237
|
-
}),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
// Static query example - dynamic queries would need a different approach
|
|
241
|
-
// For dynamic queries, you'd typically use a derived atom that depends on searchTermAtom
|
|
242
|
-
export const searchResultsAtom = StoreTag.makeQuery(
|
|
243
|
-
queryDb({
|
|
244
|
-
query: sql`SELECT * FROM products ORDER BY createdAt DESC`,
|
|
245
|
-
schema: Schema.Array(Product),
|
|
246
|
-
}),
|
|
247
|
-
)
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
251
|
-
|
|
252
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
253
|
-
|
|
254
|
-
export { schema } from './schema.ts'
|
|
255
|
-
|
|
256
|
-
// Create a persistent adapter with OPFS storage
|
|
257
|
-
const adapter = makePersistedAdapter({
|
|
258
|
-
storage: { type: 'opfs' },
|
|
259
|
-
worker: LiveStoreWorker,
|
|
260
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// Define the store as a service tag
|
|
264
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
265
|
-
schema,
|
|
266
|
-
storeId: 'default',
|
|
267
|
-
adapter,
|
|
268
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
269
|
-
}) {}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
273
|
-
|
|
274
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
275
|
-
|
|
276
|
-
// Define event payloads
|
|
277
|
-
export const events = {
|
|
278
|
-
userCreated: Events.clientOnly({
|
|
279
|
-
name: 'userCreated',
|
|
280
|
-
schema: Schema.Struct({
|
|
281
|
-
id: Schema.String,
|
|
282
|
-
name: Schema.String,
|
|
283
|
-
email: Schema.String,
|
|
284
|
-
}),
|
|
285
|
-
}),
|
|
286
|
-
userUpdated: Events.clientOnly({
|
|
287
|
-
name: 'userUpdated',
|
|
288
|
-
schema: Schema.Struct({
|
|
289
|
-
id: Schema.String,
|
|
290
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
291
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
292
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
293
|
-
}),
|
|
294
|
-
}),
|
|
295
|
-
productCreated: Events.clientOnly({
|
|
296
|
-
name: 'productCreated',
|
|
297
|
-
schema: Schema.Struct({
|
|
298
|
-
id: Schema.String,
|
|
299
|
-
name: Schema.String,
|
|
300
|
-
description: Schema.String,
|
|
301
|
-
price: Schema.Number,
|
|
302
|
-
}),
|
|
303
|
-
}),
|
|
304
|
-
productUpdated: Events.clientOnly({
|
|
305
|
-
name: 'productUpdated',
|
|
306
|
-
schema: Schema.Struct({
|
|
307
|
-
id: Schema.String,
|
|
308
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
309
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
310
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
311
|
-
}),
|
|
312
|
-
}),
|
|
313
|
-
todoCreated: Events.clientOnly({
|
|
314
|
-
name: 'todoCreated',
|
|
315
|
-
schema: Schema.Struct({
|
|
316
|
-
id: Schema.String,
|
|
317
|
-
text: Schema.String,
|
|
318
|
-
completed: Schema.Boolean,
|
|
319
|
-
}),
|
|
320
|
-
}),
|
|
321
|
-
todoToggled: Events.clientOnly({
|
|
322
|
-
name: 'todoToggled',
|
|
323
|
-
schema: Schema.Struct({
|
|
324
|
-
id: Schema.String,
|
|
325
|
-
completed: Schema.Boolean,
|
|
326
|
-
}),
|
|
327
|
-
}),
|
|
328
|
-
itemCreated: Events.clientOnly({
|
|
329
|
-
name: 'itemCreated',
|
|
330
|
-
schema: Schema.Struct({
|
|
331
|
-
id: Schema.String,
|
|
332
|
-
name: Schema.String,
|
|
333
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
334
|
-
}),
|
|
335
|
-
}),
|
|
336
|
-
itemUpdated: Events.clientOnly({
|
|
337
|
-
name: 'itemUpdated',
|
|
338
|
-
schema: Schema.Struct({
|
|
339
|
-
id: Schema.String,
|
|
340
|
-
status: Schema.String,
|
|
341
|
-
}),
|
|
342
|
-
}),
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Define tables
|
|
346
|
-
const tables = {
|
|
347
|
-
users: State.SQLite.table({
|
|
348
|
-
name: 'users',
|
|
349
|
-
columns: {
|
|
350
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
351
|
-
name: State.SQLite.text(),
|
|
352
|
-
email: State.SQLite.text(),
|
|
353
|
-
isActive: State.SQLite.boolean(),
|
|
354
|
-
createdAt: State.SQLite.datetime(),
|
|
355
|
-
},
|
|
356
|
-
}),
|
|
357
|
-
products: State.SQLite.table({
|
|
358
|
-
name: 'products',
|
|
359
|
-
columns: {
|
|
360
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
361
|
-
name: State.SQLite.text(),
|
|
362
|
-
description: State.SQLite.text(),
|
|
363
|
-
price: State.SQLite.real(),
|
|
364
|
-
createdAt: State.SQLite.datetime(),
|
|
365
|
-
},
|
|
366
|
-
}),
|
|
367
|
-
todos: State.SQLite.table({
|
|
368
|
-
name: 'todos',
|
|
369
|
-
columns: {
|
|
370
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
371
|
-
text: State.SQLite.text(),
|
|
372
|
-
completed: State.SQLite.boolean(),
|
|
373
|
-
createdAt: State.SQLite.datetime(),
|
|
374
|
-
},
|
|
375
|
-
}),
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Define materializers
|
|
379
|
-
const materializers = State.SQLite.materializers(events, {
|
|
380
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
381
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
382
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
383
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
384
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
385
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
386
|
-
return tables.users.update(updates).where({ id })
|
|
387
|
-
},
|
|
388
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
389
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
390
|
-
productCreated: ({ id, name, description, price }) =>
|
|
391
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
392
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
393
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
394
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
395
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
396
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
397
|
-
return tables.products.update(updates).where({ id })
|
|
398
|
-
},
|
|
399
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
400
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
// Create state
|
|
404
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
405
|
-
|
|
406
|
-
// Create the store schema
|
|
407
|
-
export const schema = makeSchema({ events, state })
|
|
408
|
-
|
|
409
|
-
export { tables }
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### `patterns/effect/store-setup/utils.ts`
|
|
413
|
-
|
|
414
|
-
```ts filename="patterns/effect/store-setup/utils.ts"
|
|
415
|
-
|
|
416
|
-
// Common query atoms that can be reused
|
|
417
|
-
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
|
|
418
|
-
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
|
|
419
|
-
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
|
|
420
|
-
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))
|
|
421
|
-
|
|
422
|
-
// Common types for optimistic updates
|
|
423
|
-
export type PendingTodo = { id: string; text: string; completed: boolean }
|
|
424
|
-
export type PendingUser = { id: string; name: string; email: string }
|
|
425
|
-
|
|
426
|
-
// Common pending state atoms
|
|
427
|
-
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
|
|
428
|
-
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
### Using queries in React components
|
|
432
|
-
|
|
433
|
-
Access query results in React components with the `useAtomValue` hook. When using `StoreTag.makeQuery` (non-unsafe API), the result is wrapped in a Result type for proper loading and error handling:
|
|
434
|
-
|
|
435
|
-
## `patterns/effect/store-setup/user-list.tsx`
|
|
436
|
-
|
|
437
|
-
```tsx filename="patterns/effect/store-setup/user-list.tsx"
|
|
438
|
-
|
|
439
|
-
export function UserList() {
|
|
440
|
-
const users = useAtomValue(activeUsersAtom)
|
|
441
|
-
|
|
442
|
-
return Result.builder(users)
|
|
443
|
-
.onInitial(() => <div>Loading users...</div>)
|
|
444
|
-
.onSuccess((users) => (
|
|
445
|
-
<ul>
|
|
446
|
-
{users.map((user) => (
|
|
447
|
-
<li key={user.id}>{user.name}</li>
|
|
448
|
-
))}
|
|
449
|
-
</ul>
|
|
450
|
-
))
|
|
451
|
-
.onDefect((error: any) => <div>Error: {error.message}</div>)
|
|
452
|
-
.render()
|
|
453
|
-
}
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
457
|
-
|
|
458
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
459
|
-
|
|
460
|
-
export { schema } from './schema.ts'
|
|
461
|
-
|
|
462
|
-
// Create a persistent adapter with OPFS storage
|
|
463
|
-
const adapter = makePersistedAdapter({
|
|
464
|
-
storage: { type: 'opfs' },
|
|
465
|
-
worker: LiveStoreWorker,
|
|
466
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
// Define the store as a service tag
|
|
470
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
471
|
-
schema,
|
|
472
|
-
storeId: 'default',
|
|
473
|
-
adapter,
|
|
474
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
475
|
-
}) {}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### `patterns/effect/store-setup/queries.ts`
|
|
479
|
-
|
|
480
|
-
```ts filename="patterns/effect/store-setup/queries.ts"
|
|
481
|
-
|
|
482
|
-
// User schema for type safety
|
|
483
|
-
const User = Schema.Struct({
|
|
484
|
-
id: Schema.String,
|
|
485
|
-
name: Schema.String,
|
|
486
|
-
isActive: Schema.Boolean,
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
const Product = Schema.Struct({
|
|
490
|
-
id: Schema.String,
|
|
491
|
-
name: Schema.String,
|
|
492
|
-
createdAt: Schema.DateTimeUtc,
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
// Search term atom for dynamic queries
|
|
496
|
-
export const searchTermAtom = Atom.make<string>('')
|
|
497
|
-
|
|
498
|
-
// Re-export from utils for convenience
|
|
499
|
-
export { usersQueryAtom as usersAtom } from './utils.ts'
|
|
500
|
-
|
|
501
|
-
// Query with SQL
|
|
502
|
-
export const activeUsersAtom = StoreTag.makeQuery(
|
|
503
|
-
queryDb({
|
|
504
|
-
query: sql`SELECT * FROM users WHERE isActive = true ORDER BY name`,
|
|
505
|
-
schema: Schema.Array(User),
|
|
506
|
-
}),
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
// Static query example - dynamic queries would need a different approach
|
|
510
|
-
// For dynamic queries, you'd typically use a derived atom that depends on searchTermAtom
|
|
511
|
-
export const searchResultsAtom = StoreTag.makeQuery(
|
|
512
|
-
queryDb({
|
|
513
|
-
query: sql`SELECT * FROM products ORDER BY createdAt DESC`,
|
|
514
|
-
schema: Schema.Array(Product),
|
|
515
|
-
}),
|
|
516
|
-
)
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
520
|
-
|
|
521
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
522
|
-
|
|
523
|
-
// Define event payloads
|
|
524
|
-
export const events = {
|
|
525
|
-
userCreated: Events.clientOnly({
|
|
526
|
-
name: 'userCreated',
|
|
527
|
-
schema: Schema.Struct({
|
|
528
|
-
id: Schema.String,
|
|
529
|
-
name: Schema.String,
|
|
530
|
-
email: Schema.String,
|
|
531
|
-
}),
|
|
532
|
-
}),
|
|
533
|
-
userUpdated: Events.clientOnly({
|
|
534
|
-
name: 'userUpdated',
|
|
535
|
-
schema: Schema.Struct({
|
|
536
|
-
id: Schema.String,
|
|
537
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
538
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
539
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
540
|
-
}),
|
|
541
|
-
}),
|
|
542
|
-
productCreated: Events.clientOnly({
|
|
543
|
-
name: 'productCreated',
|
|
544
|
-
schema: Schema.Struct({
|
|
545
|
-
id: Schema.String,
|
|
546
|
-
name: Schema.String,
|
|
547
|
-
description: Schema.String,
|
|
548
|
-
price: Schema.Number,
|
|
549
|
-
}),
|
|
550
|
-
}),
|
|
551
|
-
productUpdated: Events.clientOnly({
|
|
552
|
-
name: 'productUpdated',
|
|
553
|
-
schema: Schema.Struct({
|
|
554
|
-
id: Schema.String,
|
|
555
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
556
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
557
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
558
|
-
}),
|
|
559
|
-
}),
|
|
560
|
-
todoCreated: Events.clientOnly({
|
|
561
|
-
name: 'todoCreated',
|
|
562
|
-
schema: Schema.Struct({
|
|
563
|
-
id: Schema.String,
|
|
564
|
-
text: Schema.String,
|
|
565
|
-
completed: Schema.Boolean,
|
|
566
|
-
}),
|
|
567
|
-
}),
|
|
568
|
-
todoToggled: Events.clientOnly({
|
|
569
|
-
name: 'todoToggled',
|
|
570
|
-
schema: Schema.Struct({
|
|
571
|
-
id: Schema.String,
|
|
572
|
-
completed: Schema.Boolean,
|
|
573
|
-
}),
|
|
574
|
-
}),
|
|
575
|
-
itemCreated: Events.clientOnly({
|
|
576
|
-
name: 'itemCreated',
|
|
577
|
-
schema: Schema.Struct({
|
|
578
|
-
id: Schema.String,
|
|
579
|
-
name: Schema.String,
|
|
580
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
581
|
-
}),
|
|
582
|
-
}),
|
|
583
|
-
itemUpdated: Events.clientOnly({
|
|
584
|
-
name: 'itemUpdated',
|
|
585
|
-
schema: Schema.Struct({
|
|
586
|
-
id: Schema.String,
|
|
587
|
-
status: Schema.String,
|
|
588
|
-
}),
|
|
589
|
-
}),
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Define tables
|
|
593
|
-
const tables = {
|
|
594
|
-
users: State.SQLite.table({
|
|
595
|
-
name: 'users',
|
|
596
|
-
columns: {
|
|
597
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
598
|
-
name: State.SQLite.text(),
|
|
599
|
-
email: State.SQLite.text(),
|
|
600
|
-
isActive: State.SQLite.boolean(),
|
|
601
|
-
createdAt: State.SQLite.datetime(),
|
|
602
|
-
},
|
|
603
|
-
}),
|
|
604
|
-
products: State.SQLite.table({
|
|
605
|
-
name: 'products',
|
|
606
|
-
columns: {
|
|
607
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
608
|
-
name: State.SQLite.text(),
|
|
609
|
-
description: State.SQLite.text(),
|
|
610
|
-
price: State.SQLite.real(),
|
|
611
|
-
createdAt: State.SQLite.datetime(),
|
|
612
|
-
},
|
|
613
|
-
}),
|
|
614
|
-
todos: State.SQLite.table({
|
|
615
|
-
name: 'todos',
|
|
616
|
-
columns: {
|
|
617
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
618
|
-
text: State.SQLite.text(),
|
|
619
|
-
completed: State.SQLite.boolean(),
|
|
620
|
-
createdAt: State.SQLite.datetime(),
|
|
621
|
-
},
|
|
622
|
-
}),
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Define materializers
|
|
626
|
-
const materializers = State.SQLite.materializers(events, {
|
|
627
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
628
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
629
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
630
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
631
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
632
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
633
|
-
return tables.users.update(updates).where({ id })
|
|
634
|
-
},
|
|
635
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
636
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
637
|
-
productCreated: ({ id, name, description, price }) =>
|
|
638
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
639
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
640
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
641
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
642
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
643
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
644
|
-
return tables.products.update(updates).where({ id })
|
|
645
|
-
},
|
|
646
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
647
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
// Create state
|
|
651
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
652
|
-
|
|
653
|
-
// Create the store schema
|
|
654
|
-
export const schema = makeSchema({ events, state })
|
|
655
|
-
|
|
656
|
-
export { tables }
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### `patterns/effect/store-setup/utils.ts`
|
|
660
|
-
|
|
661
|
-
```ts filename="patterns/effect/store-setup/utils.ts"
|
|
662
|
-
|
|
663
|
-
// Common query atoms that can be reused
|
|
664
|
-
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
|
|
665
|
-
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
|
|
666
|
-
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
|
|
667
|
-
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))
|
|
668
|
-
|
|
669
|
-
// Common types for optimistic updates
|
|
670
|
-
export type PendingTodo = { id: string; text: string; completed: boolean }
|
|
671
|
-
export type PendingUser = { id: string; name: string; email: string }
|
|
672
|
-
|
|
673
|
-
// Common pending state atoms
|
|
674
|
-
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
|
|
675
|
-
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
### Integrating Effect services
|
|
679
|
-
|
|
680
|
-
Combine Effect services with LiveStore operations using the store's runtime:
|
|
681
|
-
|
|
682
|
-
## `patterns/effect/store-setup/services.tsx`
|
|
683
|
-
|
|
684
|
-
```tsx filename="patterns/effect/store-setup/services.tsx"
|
|
685
|
-
|
|
686
|
-
// Example service definition
|
|
687
|
-
export class MyService extends Context.Tag('MyService')<
|
|
688
|
-
MyService,
|
|
689
|
-
{
|
|
690
|
-
processItem: (name: string) => Effect.Effect<{
|
|
691
|
-
name: string
|
|
692
|
-
metadata: Record<string, unknown>
|
|
693
|
-
}>
|
|
694
|
-
}
|
|
695
|
-
>() {}
|
|
696
|
-
|
|
697
|
-
// Use the commit hook for event handling
|
|
698
|
-
export const useCommit = () => useAtomSet(StoreTag.commit)
|
|
699
|
-
|
|
700
|
-
// Simple commit example
|
|
701
|
-
export const createItemAtom = StoreTag.runtime.fn<string>()((itemName, get) => {
|
|
702
|
-
return Effect.sync(() => {
|
|
703
|
-
const store = get(StoreTag.storeUnsafe)
|
|
704
|
-
if (store) {
|
|
705
|
-
store.commit(
|
|
706
|
-
events.itemCreated({
|
|
707
|
-
id: crypto.randomUUID(),
|
|
708
|
-
name: itemName,
|
|
709
|
-
metadata: { createdAt: new Date().toISOString() },
|
|
710
|
-
}),
|
|
711
|
-
)
|
|
712
|
-
}
|
|
713
|
-
})
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
// Use in a React component
|
|
717
|
-
export function CreateItemButton() {
|
|
718
|
-
const createItem = useAtomSet(createItemAtom)
|
|
719
|
-
|
|
720
|
-
const handleClick = () => {
|
|
721
|
-
createItem('New Item')
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return (
|
|
725
|
-
<button type="button" onClick={handleClick}>
|
|
726
|
-
Create Item
|
|
727
|
-
</button>
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
733
|
-
|
|
734
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
735
|
-
|
|
736
|
-
export { schema } from './schema.ts'
|
|
737
|
-
|
|
738
|
-
// Create a persistent adapter with OPFS storage
|
|
739
|
-
const adapter = makePersistedAdapter({
|
|
740
|
-
storage: { type: 'opfs' },
|
|
741
|
-
worker: LiveStoreWorker,
|
|
742
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
743
|
-
})
|
|
744
|
-
|
|
745
|
-
// Define the store as a service tag
|
|
746
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
747
|
-
schema,
|
|
748
|
-
storeId: 'default',
|
|
749
|
-
adapter,
|
|
750
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
751
|
-
}) {}
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
755
|
-
|
|
756
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
757
|
-
|
|
758
|
-
// Define event payloads
|
|
759
|
-
export const events = {
|
|
760
|
-
userCreated: Events.clientOnly({
|
|
761
|
-
name: 'userCreated',
|
|
762
|
-
schema: Schema.Struct({
|
|
763
|
-
id: Schema.String,
|
|
764
|
-
name: Schema.String,
|
|
765
|
-
email: Schema.String,
|
|
766
|
-
}),
|
|
767
|
-
}),
|
|
768
|
-
userUpdated: Events.clientOnly({
|
|
769
|
-
name: 'userUpdated',
|
|
770
|
-
schema: Schema.Struct({
|
|
771
|
-
id: Schema.String,
|
|
772
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
773
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
774
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
775
|
-
}),
|
|
776
|
-
}),
|
|
777
|
-
productCreated: Events.clientOnly({
|
|
778
|
-
name: 'productCreated',
|
|
779
|
-
schema: Schema.Struct({
|
|
780
|
-
id: Schema.String,
|
|
781
|
-
name: Schema.String,
|
|
782
|
-
description: Schema.String,
|
|
783
|
-
price: Schema.Number,
|
|
784
|
-
}),
|
|
785
|
-
}),
|
|
786
|
-
productUpdated: Events.clientOnly({
|
|
787
|
-
name: 'productUpdated',
|
|
788
|
-
schema: Schema.Struct({
|
|
789
|
-
id: Schema.String,
|
|
790
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
791
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
792
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
793
|
-
}),
|
|
794
|
-
}),
|
|
795
|
-
todoCreated: Events.clientOnly({
|
|
796
|
-
name: 'todoCreated',
|
|
797
|
-
schema: Schema.Struct({
|
|
798
|
-
id: Schema.String,
|
|
799
|
-
text: Schema.String,
|
|
800
|
-
completed: Schema.Boolean,
|
|
801
|
-
}),
|
|
802
|
-
}),
|
|
803
|
-
todoToggled: Events.clientOnly({
|
|
804
|
-
name: 'todoToggled',
|
|
805
|
-
schema: Schema.Struct({
|
|
806
|
-
id: Schema.String,
|
|
807
|
-
completed: Schema.Boolean,
|
|
808
|
-
}),
|
|
809
|
-
}),
|
|
810
|
-
itemCreated: Events.clientOnly({
|
|
811
|
-
name: 'itemCreated',
|
|
812
|
-
schema: Schema.Struct({
|
|
813
|
-
id: Schema.String,
|
|
814
|
-
name: Schema.String,
|
|
815
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
816
|
-
}),
|
|
817
|
-
}),
|
|
818
|
-
itemUpdated: Events.clientOnly({
|
|
819
|
-
name: 'itemUpdated',
|
|
820
|
-
schema: Schema.Struct({
|
|
821
|
-
id: Schema.String,
|
|
822
|
-
status: Schema.String,
|
|
823
|
-
}),
|
|
824
|
-
}),
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Define tables
|
|
828
|
-
const tables = {
|
|
829
|
-
users: State.SQLite.table({
|
|
830
|
-
name: 'users',
|
|
831
|
-
columns: {
|
|
832
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
833
|
-
name: State.SQLite.text(),
|
|
834
|
-
email: State.SQLite.text(),
|
|
835
|
-
isActive: State.SQLite.boolean(),
|
|
836
|
-
createdAt: State.SQLite.datetime(),
|
|
837
|
-
},
|
|
838
|
-
}),
|
|
839
|
-
products: State.SQLite.table({
|
|
840
|
-
name: 'products',
|
|
841
|
-
columns: {
|
|
842
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
843
|
-
name: State.SQLite.text(),
|
|
844
|
-
description: State.SQLite.text(),
|
|
845
|
-
price: State.SQLite.real(),
|
|
846
|
-
createdAt: State.SQLite.datetime(),
|
|
847
|
-
},
|
|
848
|
-
}),
|
|
849
|
-
todos: State.SQLite.table({
|
|
850
|
-
name: 'todos',
|
|
851
|
-
columns: {
|
|
852
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
853
|
-
text: State.SQLite.text(),
|
|
854
|
-
completed: State.SQLite.boolean(),
|
|
855
|
-
createdAt: State.SQLite.datetime(),
|
|
856
|
-
},
|
|
857
|
-
}),
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Define materializers
|
|
861
|
-
const materializers = State.SQLite.materializers(events, {
|
|
862
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
863
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
864
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
865
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
866
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
867
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
868
|
-
return tables.users.update(updates).where({ id })
|
|
869
|
-
},
|
|
870
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
871
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
872
|
-
productCreated: ({ id, name, description, price }) =>
|
|
873
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
874
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
875
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
876
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
877
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
878
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
879
|
-
return tables.products.update(updates).where({ id })
|
|
880
|
-
},
|
|
881
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
882
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
883
|
-
})
|
|
884
|
-
|
|
885
|
-
// Create state
|
|
886
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
887
|
-
|
|
888
|
-
// Create the store schema
|
|
889
|
-
export const schema = makeSchema({ events, state })
|
|
890
|
-
|
|
891
|
-
export { tables }
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
### Advanced patterns
|
|
895
|
-
|
|
896
|
-
#### Optimistic updates
|
|
897
|
-
|
|
898
|
-
Combine local state with LiveStore for optimistic UI updates. When using `StoreTag.makeQueryUnsafe`, the data is directly available:
|
|
899
|
-
|
|
900
|
-
## `patterns/effect/optimistic-example/optimistic.ts`
|
|
901
|
-
|
|
902
|
-
```ts filename="patterns/effect/optimistic-example/optimistic.ts"
|
|
903
|
-
|
|
904
|
-
// Combine real and pending todos for optimistic UI
|
|
905
|
-
export const optimisticTodoAtom = Atom.make((get) => {
|
|
906
|
-
const todos = get(todosQueryUnsafeAtom) // Direct array, not wrapped in Result
|
|
907
|
-
const pending = get(pendingTodosAtom)
|
|
908
|
-
|
|
909
|
-
return [...(todos || []), ...pending]
|
|
910
|
-
})
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
914
|
-
|
|
915
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
916
|
-
|
|
917
|
-
export { schema } from './schema.ts'
|
|
918
|
-
|
|
919
|
-
// Create a persistent adapter with OPFS storage
|
|
920
|
-
const adapter = makePersistedAdapter({
|
|
921
|
-
storage: { type: 'opfs' },
|
|
922
|
-
worker: LiveStoreWorker,
|
|
923
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
924
|
-
})
|
|
925
|
-
|
|
926
|
-
// Define the store as a service tag
|
|
927
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
928
|
-
schema,
|
|
929
|
-
storeId: 'default',
|
|
930
|
-
adapter,
|
|
931
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
932
|
-
}) {}
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
936
|
-
|
|
937
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
938
|
-
|
|
939
|
-
// Define event payloads
|
|
940
|
-
export const events = {
|
|
941
|
-
userCreated: Events.clientOnly({
|
|
942
|
-
name: 'userCreated',
|
|
943
|
-
schema: Schema.Struct({
|
|
944
|
-
id: Schema.String,
|
|
945
|
-
name: Schema.String,
|
|
946
|
-
email: Schema.String,
|
|
947
|
-
}),
|
|
948
|
-
}),
|
|
949
|
-
userUpdated: Events.clientOnly({
|
|
950
|
-
name: 'userUpdated',
|
|
951
|
-
schema: Schema.Struct({
|
|
952
|
-
id: Schema.String,
|
|
953
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
954
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
955
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
956
|
-
}),
|
|
957
|
-
}),
|
|
958
|
-
productCreated: Events.clientOnly({
|
|
959
|
-
name: 'productCreated',
|
|
960
|
-
schema: Schema.Struct({
|
|
961
|
-
id: Schema.String,
|
|
962
|
-
name: Schema.String,
|
|
963
|
-
description: Schema.String,
|
|
964
|
-
price: Schema.Number,
|
|
965
|
-
}),
|
|
966
|
-
}),
|
|
967
|
-
productUpdated: Events.clientOnly({
|
|
968
|
-
name: 'productUpdated',
|
|
969
|
-
schema: Schema.Struct({
|
|
970
|
-
id: Schema.String,
|
|
971
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
972
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
973
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
974
|
-
}),
|
|
975
|
-
}),
|
|
976
|
-
todoCreated: Events.clientOnly({
|
|
977
|
-
name: 'todoCreated',
|
|
978
|
-
schema: Schema.Struct({
|
|
979
|
-
id: Schema.String,
|
|
980
|
-
text: Schema.String,
|
|
981
|
-
completed: Schema.Boolean,
|
|
982
|
-
}),
|
|
983
|
-
}),
|
|
984
|
-
todoToggled: Events.clientOnly({
|
|
985
|
-
name: 'todoToggled',
|
|
986
|
-
schema: Schema.Struct({
|
|
987
|
-
id: Schema.String,
|
|
988
|
-
completed: Schema.Boolean,
|
|
989
|
-
}),
|
|
990
|
-
}),
|
|
991
|
-
itemCreated: Events.clientOnly({
|
|
992
|
-
name: 'itemCreated',
|
|
993
|
-
schema: Schema.Struct({
|
|
994
|
-
id: Schema.String,
|
|
995
|
-
name: Schema.String,
|
|
996
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
997
|
-
}),
|
|
998
|
-
}),
|
|
999
|
-
itemUpdated: Events.clientOnly({
|
|
1000
|
-
name: 'itemUpdated',
|
|
1001
|
-
schema: Schema.Struct({
|
|
1002
|
-
id: Schema.String,
|
|
1003
|
-
status: Schema.String,
|
|
1004
|
-
}),
|
|
1005
|
-
}),
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Define tables
|
|
1009
|
-
const tables = {
|
|
1010
|
-
users: State.SQLite.table({
|
|
1011
|
-
name: 'users',
|
|
1012
|
-
columns: {
|
|
1013
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1014
|
-
name: State.SQLite.text(),
|
|
1015
|
-
email: State.SQLite.text(),
|
|
1016
|
-
isActive: State.SQLite.boolean(),
|
|
1017
|
-
createdAt: State.SQLite.datetime(),
|
|
1018
|
-
},
|
|
1019
|
-
}),
|
|
1020
|
-
products: State.SQLite.table({
|
|
1021
|
-
name: 'products',
|
|
1022
|
-
columns: {
|
|
1023
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1024
|
-
name: State.SQLite.text(),
|
|
1025
|
-
description: State.SQLite.text(),
|
|
1026
|
-
price: State.SQLite.real(),
|
|
1027
|
-
createdAt: State.SQLite.datetime(),
|
|
1028
|
-
},
|
|
1029
|
-
}),
|
|
1030
|
-
todos: State.SQLite.table({
|
|
1031
|
-
name: 'todos',
|
|
1032
|
-
columns: {
|
|
1033
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1034
|
-
text: State.SQLite.text(),
|
|
1035
|
-
completed: State.SQLite.boolean(),
|
|
1036
|
-
createdAt: State.SQLite.datetime(),
|
|
1037
|
-
},
|
|
1038
|
-
}),
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Define materializers
|
|
1042
|
-
const materializers = State.SQLite.materializers(events, {
|
|
1043
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
1044
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
1045
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
1046
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1047
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
1048
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
1049
|
-
return tables.users.update(updates).where({ id })
|
|
1050
|
-
},
|
|
1051
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
1052
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
1053
|
-
productCreated: ({ id, name, description, price }) =>
|
|
1054
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
1055
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
1056
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
1057
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1058
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
1059
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
1060
|
-
return tables.products.update(updates).where({ id })
|
|
1061
|
-
},
|
|
1062
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
1063
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
1064
|
-
})
|
|
1065
|
-
|
|
1066
|
-
// Create state
|
|
1067
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
1068
|
-
|
|
1069
|
-
// Create the store schema
|
|
1070
|
-
export const schema = makeSchema({ events, state })
|
|
1071
|
-
|
|
1072
|
-
export { tables }
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
### `patterns/effect/store-setup/utils.ts`
|
|
1076
|
-
|
|
1077
|
-
```ts filename="patterns/effect/store-setup/utils.ts"
|
|
1078
|
-
|
|
1079
|
-
// Common query atoms that can be reused
|
|
1080
|
-
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
|
|
1081
|
-
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
|
|
1082
|
-
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
|
|
1083
|
-
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))
|
|
1084
|
-
|
|
1085
|
-
// Common types for optimistic updates
|
|
1086
|
-
export type PendingTodo = { id: string; text: string; completed: boolean }
|
|
1087
|
-
export type PendingUser = { id: string; name: string; email: string }
|
|
1088
|
-
|
|
1089
|
-
// Common pending state atoms
|
|
1090
|
-
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
|
|
1091
|
-
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
#### Derived state
|
|
1095
|
-
|
|
1096
|
-
Create computed atoms based on LiveStore queries. When using the non-unsafe API, handle the Result type:
|
|
1097
|
-
|
|
1098
|
-
## `patterns/effect/derived-example/derived.ts`
|
|
1099
|
-
|
|
1100
|
-
```ts filename="patterns/effect/derived-example/derived.ts"
|
|
1101
|
-
|
|
1102
|
-
// Derive statistics from todos
|
|
1103
|
-
export const todoStatsAtom = Atom.make((get) => {
|
|
1104
|
-
const todos = get(todosQueryAtom) // Result wrapped
|
|
1105
|
-
|
|
1106
|
-
return Result.map(todos, (todoList) => ({
|
|
1107
|
-
total: todoList.length,
|
|
1108
|
-
completed: todoList.filter((t) => t.completed).length,
|
|
1109
|
-
pending: todoList.filter((t) => !t.completed).length,
|
|
1110
|
-
}))
|
|
1111
|
-
})
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
1115
|
-
|
|
1116
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
1117
|
-
|
|
1118
|
-
export { schema } from './schema.ts'
|
|
1119
|
-
|
|
1120
|
-
// Create a persistent adapter with OPFS storage
|
|
1121
|
-
const adapter = makePersistedAdapter({
|
|
1122
|
-
storage: { type: 'opfs' },
|
|
1123
|
-
worker: LiveStoreWorker,
|
|
1124
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
// Define the store as a service tag
|
|
1128
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
1129
|
-
schema,
|
|
1130
|
-
storeId: 'default',
|
|
1131
|
-
adapter,
|
|
1132
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
1133
|
-
}) {}
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
1137
|
-
|
|
1138
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
1139
|
-
|
|
1140
|
-
// Define event payloads
|
|
1141
|
-
export const events = {
|
|
1142
|
-
userCreated: Events.clientOnly({
|
|
1143
|
-
name: 'userCreated',
|
|
1144
|
-
schema: Schema.Struct({
|
|
1145
|
-
id: Schema.String,
|
|
1146
|
-
name: Schema.String,
|
|
1147
|
-
email: Schema.String,
|
|
1148
|
-
}),
|
|
1149
|
-
}),
|
|
1150
|
-
userUpdated: Events.clientOnly({
|
|
1151
|
-
name: 'userUpdated',
|
|
1152
|
-
schema: Schema.Struct({
|
|
1153
|
-
id: Schema.String,
|
|
1154
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1155
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1156
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
1157
|
-
}),
|
|
1158
|
-
}),
|
|
1159
|
-
productCreated: Events.clientOnly({
|
|
1160
|
-
name: 'productCreated',
|
|
1161
|
-
schema: Schema.Struct({
|
|
1162
|
-
id: Schema.String,
|
|
1163
|
-
name: Schema.String,
|
|
1164
|
-
description: Schema.String,
|
|
1165
|
-
price: Schema.Number,
|
|
1166
|
-
}),
|
|
1167
|
-
}),
|
|
1168
|
-
productUpdated: Events.clientOnly({
|
|
1169
|
-
name: 'productUpdated',
|
|
1170
|
-
schema: Schema.Struct({
|
|
1171
|
-
id: Schema.String,
|
|
1172
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1173
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1174
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
1175
|
-
}),
|
|
1176
|
-
}),
|
|
1177
|
-
todoCreated: Events.clientOnly({
|
|
1178
|
-
name: 'todoCreated',
|
|
1179
|
-
schema: Schema.Struct({
|
|
1180
|
-
id: Schema.String,
|
|
1181
|
-
text: Schema.String,
|
|
1182
|
-
completed: Schema.Boolean,
|
|
1183
|
-
}),
|
|
1184
|
-
}),
|
|
1185
|
-
todoToggled: Events.clientOnly({
|
|
1186
|
-
name: 'todoToggled',
|
|
1187
|
-
schema: Schema.Struct({
|
|
1188
|
-
id: Schema.String,
|
|
1189
|
-
completed: Schema.Boolean,
|
|
1190
|
-
}),
|
|
1191
|
-
}),
|
|
1192
|
-
itemCreated: Events.clientOnly({
|
|
1193
|
-
name: 'itemCreated',
|
|
1194
|
-
schema: Schema.Struct({
|
|
1195
|
-
id: Schema.String,
|
|
1196
|
-
name: Schema.String,
|
|
1197
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
1198
|
-
}),
|
|
1199
|
-
}),
|
|
1200
|
-
itemUpdated: Events.clientOnly({
|
|
1201
|
-
name: 'itemUpdated',
|
|
1202
|
-
schema: Schema.Struct({
|
|
1203
|
-
id: Schema.String,
|
|
1204
|
-
status: Schema.String,
|
|
1205
|
-
}),
|
|
1206
|
-
}),
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Define tables
|
|
1210
|
-
const tables = {
|
|
1211
|
-
users: State.SQLite.table({
|
|
1212
|
-
name: 'users',
|
|
1213
|
-
columns: {
|
|
1214
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1215
|
-
name: State.SQLite.text(),
|
|
1216
|
-
email: State.SQLite.text(),
|
|
1217
|
-
isActive: State.SQLite.boolean(),
|
|
1218
|
-
createdAt: State.SQLite.datetime(),
|
|
1219
|
-
},
|
|
1220
|
-
}),
|
|
1221
|
-
products: State.SQLite.table({
|
|
1222
|
-
name: 'products',
|
|
1223
|
-
columns: {
|
|
1224
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1225
|
-
name: State.SQLite.text(),
|
|
1226
|
-
description: State.SQLite.text(),
|
|
1227
|
-
price: State.SQLite.real(),
|
|
1228
|
-
createdAt: State.SQLite.datetime(),
|
|
1229
|
-
},
|
|
1230
|
-
}),
|
|
1231
|
-
todos: State.SQLite.table({
|
|
1232
|
-
name: 'todos',
|
|
1233
|
-
columns: {
|
|
1234
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1235
|
-
text: State.SQLite.text(),
|
|
1236
|
-
completed: State.SQLite.boolean(),
|
|
1237
|
-
createdAt: State.SQLite.datetime(),
|
|
1238
|
-
},
|
|
1239
|
-
}),
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Define materializers
|
|
1243
|
-
const materializers = State.SQLite.materializers(events, {
|
|
1244
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
1245
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
1246
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
1247
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1248
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
1249
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
1250
|
-
return tables.users.update(updates).where({ id })
|
|
1251
|
-
},
|
|
1252
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
1253
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
1254
|
-
productCreated: ({ id, name, description, price }) =>
|
|
1255
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
1256
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
1257
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
1258
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1259
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
1260
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
1261
|
-
return tables.products.update(updates).where({ id })
|
|
1262
|
-
},
|
|
1263
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
1264
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
1265
|
-
})
|
|
1266
|
-
|
|
1267
|
-
// Create state
|
|
1268
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
1269
|
-
|
|
1270
|
-
// Create the store schema
|
|
1271
|
-
export const schema = makeSchema({ events, state })
|
|
1272
|
-
|
|
1273
|
-
export { tables }
|
|
1274
|
-
```
|
|
1275
|
-
|
|
1276
|
-
### `patterns/effect/store-setup/utils.ts`
|
|
1277
|
-
|
|
1278
|
-
```ts filename="patterns/effect/store-setup/utils.ts"
|
|
1279
|
-
|
|
1280
|
-
// Common query atoms that can be reused
|
|
1281
|
-
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
|
|
1282
|
-
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
|
|
1283
|
-
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
|
|
1284
|
-
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))
|
|
1285
|
-
|
|
1286
|
-
// Common types for optimistic updates
|
|
1287
|
-
export type PendingTodo = { id: string; text: string; completed: boolean }
|
|
1288
|
-
export type PendingUser = { id: string; name: string; email: string }
|
|
1289
|
-
|
|
1290
|
-
// Common pending state atoms
|
|
1291
|
-
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
|
|
1292
|
-
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
|
|
1293
|
-
```
|
|
1294
|
-
|
|
1295
|
-
#### Batch operations
|
|
1296
|
-
|
|
1297
|
-
Perform multiple commits efficiently (commits are synchronous):
|
|
1298
|
-
|
|
1299
|
-
## `patterns/effect/batch-example/batch.ts`
|
|
1300
|
-
|
|
1301
|
-
```ts filename="patterns/effect/batch-example/batch.ts"
|
|
1302
|
-
|
|
1303
|
-
// Bulk update atom for batch operations
|
|
1304
|
-
export const bulkUpdateAtom = StoreTag.runtime.fn<string[]>()(
|
|
1305
|
-
Effect.fn(function* (ids, get) {
|
|
1306
|
-
const store = get(StoreTag.storeUnsafe)
|
|
1307
|
-
if (!store) return
|
|
1308
|
-
|
|
1309
|
-
// Commit multiple events synchronously
|
|
1310
|
-
for (const id of ids) {
|
|
1311
|
-
store.commit(events.itemUpdated({ id, status: 'processed' }))
|
|
1312
|
-
}
|
|
1313
|
-
}),
|
|
1314
|
-
)
|
|
1315
|
-
```
|
|
1316
|
-
|
|
1317
|
-
### `patterns/effect/store-setup/atoms.ts`
|
|
1318
|
-
|
|
1319
|
-
```ts filename="patterns/effect/store-setup/atoms.ts"
|
|
1320
|
-
|
|
1321
|
-
export { schema } from './schema.ts'
|
|
1322
|
-
|
|
1323
|
-
// Create a persistent adapter with OPFS storage
|
|
1324
|
-
const adapter = makePersistedAdapter({
|
|
1325
|
-
storage: { type: 'opfs' },
|
|
1326
|
-
worker: LiveStoreWorker,
|
|
1327
|
-
sharedWorker: LiveStoreSharedWorker,
|
|
1328
|
-
})
|
|
1329
|
-
|
|
1330
|
-
// Define the store as a service tag
|
|
1331
|
-
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
|
|
1332
|
-
schema,
|
|
1333
|
-
storeId: 'default',
|
|
1334
|
-
adapter,
|
|
1335
|
-
batchUpdates: unstable_batchedUpdates, // React batching for performance
|
|
1336
|
-
}) {}
|
|
1337
|
-
```
|
|
1338
|
-
|
|
1339
|
-
### `patterns/effect/store-setup/schema.ts`
|
|
1340
|
-
|
|
1341
|
-
```ts filename="patterns/effect/store-setup/schema.ts"
|
|
1342
|
-
|
|
1343
|
-
// Define event payloads
|
|
1344
|
-
export const events = {
|
|
1345
|
-
userCreated: Events.clientOnly({
|
|
1346
|
-
name: 'userCreated',
|
|
1347
|
-
schema: Schema.Struct({
|
|
1348
|
-
id: Schema.String,
|
|
1349
|
-
name: Schema.String,
|
|
1350
|
-
email: Schema.String,
|
|
1351
|
-
}),
|
|
1352
|
-
}),
|
|
1353
|
-
userUpdated: Events.clientOnly({
|
|
1354
|
-
name: 'userUpdated',
|
|
1355
|
-
schema: Schema.Struct({
|
|
1356
|
-
id: Schema.String,
|
|
1357
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1358
|
-
email: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1359
|
-
isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
|
|
1360
|
-
}),
|
|
1361
|
-
}),
|
|
1362
|
-
productCreated: Events.clientOnly({
|
|
1363
|
-
name: 'productCreated',
|
|
1364
|
-
schema: Schema.Struct({
|
|
1365
|
-
id: Schema.String,
|
|
1366
|
-
name: Schema.String,
|
|
1367
|
-
description: Schema.String,
|
|
1368
|
-
price: Schema.Number,
|
|
1369
|
-
}),
|
|
1370
|
-
}),
|
|
1371
|
-
productUpdated: Events.clientOnly({
|
|
1372
|
-
name: 'productUpdated',
|
|
1373
|
-
schema: Schema.Struct({
|
|
1374
|
-
id: Schema.String,
|
|
1375
|
-
name: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1376
|
-
description: Schema.optionalWith(Schema.String, { as: 'Option' }),
|
|
1377
|
-
price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
|
|
1378
|
-
}),
|
|
1379
|
-
}),
|
|
1380
|
-
todoCreated: Events.clientOnly({
|
|
1381
|
-
name: 'todoCreated',
|
|
1382
|
-
schema: Schema.Struct({
|
|
1383
|
-
id: Schema.String,
|
|
1384
|
-
text: Schema.String,
|
|
1385
|
-
completed: Schema.Boolean,
|
|
1386
|
-
}),
|
|
1387
|
-
}),
|
|
1388
|
-
todoToggled: Events.clientOnly({
|
|
1389
|
-
name: 'todoToggled',
|
|
1390
|
-
schema: Schema.Struct({
|
|
1391
|
-
id: Schema.String,
|
|
1392
|
-
completed: Schema.Boolean,
|
|
1393
|
-
}),
|
|
1394
|
-
}),
|
|
1395
|
-
itemCreated: Events.clientOnly({
|
|
1396
|
-
name: 'itemCreated',
|
|
1397
|
-
schema: Schema.Struct({
|
|
1398
|
-
id: Schema.String,
|
|
1399
|
-
name: Schema.String,
|
|
1400
|
-
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
1401
|
-
}),
|
|
1402
|
-
}),
|
|
1403
|
-
itemUpdated: Events.clientOnly({
|
|
1404
|
-
name: 'itemUpdated',
|
|
1405
|
-
schema: Schema.Struct({
|
|
1406
|
-
id: Schema.String,
|
|
1407
|
-
status: Schema.String,
|
|
1408
|
-
}),
|
|
1409
|
-
}),
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Define tables
|
|
1413
|
-
const tables = {
|
|
1414
|
-
users: State.SQLite.table({
|
|
1415
|
-
name: 'users',
|
|
1416
|
-
columns: {
|
|
1417
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1418
|
-
name: State.SQLite.text(),
|
|
1419
|
-
email: State.SQLite.text(),
|
|
1420
|
-
isActive: State.SQLite.boolean(),
|
|
1421
|
-
createdAt: State.SQLite.datetime(),
|
|
1422
|
-
},
|
|
1423
|
-
}),
|
|
1424
|
-
products: State.SQLite.table({
|
|
1425
|
-
name: 'products',
|
|
1426
|
-
columns: {
|
|
1427
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1428
|
-
name: State.SQLite.text(),
|
|
1429
|
-
description: State.SQLite.text(),
|
|
1430
|
-
price: State.SQLite.real(),
|
|
1431
|
-
createdAt: State.SQLite.datetime(),
|
|
1432
|
-
},
|
|
1433
|
-
}),
|
|
1434
|
-
todos: State.SQLite.table({
|
|
1435
|
-
name: 'todos',
|
|
1436
|
-
columns: {
|
|
1437
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
1438
|
-
text: State.SQLite.text(),
|
|
1439
|
-
completed: State.SQLite.boolean(),
|
|
1440
|
-
createdAt: State.SQLite.datetime(),
|
|
1441
|
-
},
|
|
1442
|
-
}),
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// Define materializers
|
|
1446
|
-
const materializers = State.SQLite.materializers(events, {
|
|
1447
|
-
userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
|
|
1448
|
-
userUpdated: ({ id, name, email, isActive }) => {
|
|
1449
|
-
const updates: { name?: string; email?: string; isActive?: boolean } = {}
|
|
1450
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1451
|
-
if (Option.isSome(email)) updates.email = email.value
|
|
1452
|
-
if (Option.isSome(isActive)) updates.isActive = isActive.value
|
|
1453
|
-
return tables.users.update(updates).where({ id })
|
|
1454
|
-
},
|
|
1455
|
-
todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
|
|
1456
|
-
todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
|
|
1457
|
-
productCreated: ({ id, name, description, price }) =>
|
|
1458
|
-
tables.products.insert({ id, name, description, price, createdAt: new Date() }),
|
|
1459
|
-
productUpdated: ({ id, name, description, price }) => {
|
|
1460
|
-
const updates: { name?: string; description?: string; price?: number } = {}
|
|
1461
|
-
if (Option.isSome(name)) updates.name = name.value
|
|
1462
|
-
if (Option.isSome(description)) updates.description = description.value
|
|
1463
|
-
if (Option.isSome(price)) updates.price = price.value
|
|
1464
|
-
return tables.products.update(updates).where({ id })
|
|
1465
|
-
},
|
|
1466
|
-
itemCreated: () => [], // Item events don't have a corresponding table
|
|
1467
|
-
itemUpdated: () => [], // Item events don't have a corresponding table
|
|
1468
|
-
})
|
|
1469
|
-
|
|
1470
|
-
// Create state
|
|
1471
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
1472
|
-
|
|
1473
|
-
// Create the store schema
|
|
1474
|
-
export const schema = makeSchema({ events, state })
|
|
1475
|
-
|
|
1476
|
-
export { tables }
|
|
1477
|
-
```
|
|
1478
|
-
|
|
1479
|
-
### Best practices
|
|
1480
|
-
|
|
1481
|
-
1. **Use `StoreTag.makeQuery` for queries**: This ensures proper Effect integration and error handling
|
|
1482
|
-
2. **Leverage Effect services**: Integrate business logic through Effect services for better testability
|
|
1483
|
-
3. **Handle loading states**: Use `Result.builder` pattern for consistent loading/error UI
|
|
1484
|
-
4. **Batch React updates**: Always provide `batchUpdates` for better performance
|
|
1485
|
-
5. **Label queries**: Add descriptive labels to queries for better debugging
|
|
1486
|
-
6. **Type safety**: Let TypeScript infer types from schemas rather than manual annotations
|
|
1487
|
-
|
|
1488
|
-
### Real-World Example
|
|
1489
|
-
|
|
1490
|
-
For a comprehensive example of LiveStore with Effect Atom in action, check out [Cheffect](https://github.com/tim-smart/cheffect) - a recipe management application that demonstrates:
|
|
1491
|
-
- Complete Effect service integration
|
|
1492
|
-
- AI-powered recipe extraction using Effect services
|
|
1493
|
-
- Complex query patterns with search and filtering
|
|
1494
|
-
- Worker-based persistence with OPFS
|
|
1495
|
-
- Production-ready error handling and logging
|