@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.
Files changed (216) 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 +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  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/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. 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