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