@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,885 +0,0 @@
1
- # Todo app with shared workspaces
2
-
3
- Let's consider a fairly common application scenario: An app (in this case a todo app) with shared workspaces. For the sake of this guide, we'll keep things simple but you should be able to nicely extend this to a more complex app.
4
-
5
- ## Requirements
6
-
7
- - There are multiple independent todo workspaces
8
- - Each workspace is initially created by a single user
9
- - Users can join the workspace by knowing the workspace id and get read and write access
10
- - For simplicity, the user identity is chosen when the app initially starts (i.e. a username) but in a real app this would be handled by a proper auth setup
11
-
12
- ## Data model
13
-
14
- - We are splitting up our data model into two kinds of stores (with respective eventlogs and SQLite databases): The `workspace` store and the `user` store.
15
-
16
- ### `workspace` store (one per workspace)
17
-
18
- For the `workspace` store we have the following events:
19
-
20
- - `workspaceCreated`
21
- - `todoAdded`
22
- - `todoCompleted`
23
- - `todoDeleted`
24
- - `userJoined`
25
-
26
- And the following state model:
27
-
28
- - `workspace` table (with a single row for the workspace itself)
29
- - `todo` table (with one row per todo item)
30
- - `member` table (with one row per user who has joined the workspace)
31
-
32
- ### `user` store (one per user)
33
-
34
- For the `user` store we have the following events:
35
-
36
- - `workspaceCreated`
37
- - `workspaceJoined`
38
-
39
- And the following state model:
40
-
41
- - `user` table (with a single row for the user itself)
42
-
43
- Note that the `workspaceCreated` event is used both in the `workspace` and the `user` store. This is because each eventlog should be "self-sufficient" and not rely on other eventlogs to be present to fulfill its purpose.
44
-
45
- <EventlogModelingDiagram class="my-8" />
46
-
47
- ## Schemas
48
-
49
- **User store:**
50
-
51
- ## `data-modeling/todo-workspaces/multi-store/user.schema.ts`
52
-
53
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.schema.ts"
54
-
55
- // Emitted when this user creates a new workspace
56
- const workspaceCreated = Events.synced({
57
- name: 'v1.WorkspaceCreated',
58
- schema: Schema.Struct({ workspaceId: Schema.String }),
59
- })
60
-
61
- // Emitted when this user joins an existing workspace
62
- const workspaceJoined = Events.synced({
63
- name: 'v1.WorkspaceJoined',
64
- schema: Schema.Struct({ workspaceId: Schema.String }),
65
- })
66
-
67
- const events = { workspaceCreated, workspaceJoined }
68
-
69
- // Table to store basic user info
70
- // Contains only one row as this store is per-user.
71
- const userTable = State.SQLite.table({
72
- name: 'user',
73
- columns: {
74
- // Assuming username is unique and used as the identifier
75
- username: State.SQLite.text({ primaryKey: true }),
76
- },
77
- })
78
-
79
- // Table to track which workspaces this user is part of
80
- const userWorkspaceTable = State.SQLite.table({
81
- name: 'userWorkspace',
82
- columns: {
83
- workspaceId: State.SQLite.text({ primaryKey: true }),
84
- // Could add role/permissions here later
85
- },
86
- })
87
-
88
- export const userTables = { user: userTable, userWorkspace: userWorkspaceTable }
89
-
90
- const materializers = State.SQLite.materializers(events, {
91
- // When the user creates or joins a workspace, add it to their workspace table
92
- 'v1.WorkspaceCreated': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
93
- 'v1.WorkspaceJoined': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
94
- })
95
-
96
- const state = State.SQLite.makeState({ tables: userTables, materializers })
97
-
98
- export const schema = makeSchema({ events, state })
99
- ```
100
-
101
- **Workspace store:**
102
-
103
- ## `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
104
-
105
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
106
-
107
- // Emitted when a new workspace is created (originates this store)
108
- const workspaceCreated = Events.synced({
109
- name: 'v1.WorkspaceCreated',
110
- schema: Schema.Struct({
111
- workspaceId: Schema.String,
112
- name: Schema.String,
113
- createdByUsername: Schema.String,
114
- }),
115
- })
116
-
117
- // Emitted when a todo item is added to this workspace
118
- const todoAdded = Events.synced({
119
- name: 'v1.TodoAdded',
120
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
121
- })
122
-
123
- // Emitted when a todo item is marked as completed
124
- const todoCompleted = Events.synced({
125
- name: 'v1.TodoCompleted',
126
- schema: Schema.Struct({ todoId: Schema.String }),
127
- })
128
-
129
- // Emitted when a todo item is deleted (soft delete)
130
- const todoDeleted = Events.synced({
131
- name: 'v1.TodoDeleted',
132
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
133
- })
134
-
135
- // Emitted when a new user joins this workspace
136
- const userJoined = Events.synced({
137
- name: 'v1.UserJoined',
138
- schema: Schema.Struct({ username: Schema.String }),
139
- })
140
-
141
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
142
-
143
- // Table for the workspace itself (only one row as this store is per-workspace)
144
- const workspaceTable = State.SQLite.table({
145
- name: 'workspace',
146
- columns: {
147
- workspaceId: State.SQLite.text({ primaryKey: true }),
148
- name: State.SQLite.text(),
149
- createdByUsername: State.SQLite.text(),
150
- },
151
- })
152
-
153
- // Table for the todo items in this workspace
154
- const todoTable = State.SQLite.table({
155
- name: 'todo',
156
- columns: {
157
- todoId: State.SQLite.text({ primaryKey: true }),
158
- text: State.SQLite.text(),
159
- completed: State.SQLite.boolean({ default: false }),
160
- // Using soft delete by adding a deletedAt timestamp
161
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
162
- },
163
- })
164
-
165
- // Table for members of this workspace
166
- const memberTable = State.SQLite.table({
167
- name: 'member',
168
- columns: {
169
- username: State.SQLite.text({ primaryKey: true }),
170
- // Could add role/permissions here later
171
- },
172
- })
173
-
174
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
175
-
176
- const materializers = State.SQLite.materializers(workspaceEvents, {
177
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
178
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
179
- // Add the creator as the first member
180
- workspaceTables.member.insert({ username: createdByUsername }),
181
- ],
182
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
183
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
184
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
185
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
186
- })
187
-
188
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
189
-
190
- export const schema = makeSchema({ events: workspaceEvents, state })
191
- ```
192
-
193
- ## Using the Multi-Store API
194
-
195
- Now that we've defined our schemas, let's set up the multi-store API to manage workspace and user stores dynamically.
196
-
197
- ### Store Configuration
198
-
199
- First, define store options for each store type using [`storeOptions()`](/framework-integrations/react-integration#storeoptionsoptions):
200
-
201
- **Workspace store:**
202
-
203
- ## `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
204
-
205
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
206
-
207
- const adapter = makePersistedAdapter({
208
- storage: { type: 'opfs' },
209
- worker,
210
- sharedWorker,
211
- })
212
-
213
- // Define workspace store configuration
214
- // Each workspace gets its own isolated store instance
215
- export const workspaceStoreOptions = (workspaceId: string) =>
216
- storeOptions({
217
- storeId: `workspace-${workspaceId}`,
218
- schema,
219
- adapter,
220
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
221
- })
222
- ```
223
-
224
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
225
-
226
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
227
-
228
- // Emitted when a new workspace is created (originates this store)
229
- const workspaceCreated = Events.synced({
230
- name: 'v1.WorkspaceCreated',
231
- schema: Schema.Struct({
232
- workspaceId: Schema.String,
233
- name: Schema.String,
234
- createdByUsername: Schema.String,
235
- }),
236
- })
237
-
238
- // Emitted when a todo item is added to this workspace
239
- const todoAdded = Events.synced({
240
- name: 'v1.TodoAdded',
241
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
242
- })
243
-
244
- // Emitted when a todo item is marked as completed
245
- const todoCompleted = Events.synced({
246
- name: 'v1.TodoCompleted',
247
- schema: Schema.Struct({ todoId: Schema.String }),
248
- })
249
-
250
- // Emitted when a todo item is deleted (soft delete)
251
- const todoDeleted = Events.synced({
252
- name: 'v1.TodoDeleted',
253
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
254
- })
255
-
256
- // Emitted when a new user joins this workspace
257
- const userJoined = Events.synced({
258
- name: 'v1.UserJoined',
259
- schema: Schema.Struct({ username: Schema.String }),
260
- })
261
-
262
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
263
-
264
- // Table for the workspace itself (only one row as this store is per-workspace)
265
- const workspaceTable = State.SQLite.table({
266
- name: 'workspace',
267
- columns: {
268
- workspaceId: State.SQLite.text({ primaryKey: true }),
269
- name: State.SQLite.text(),
270
- createdByUsername: State.SQLite.text(),
271
- },
272
- })
273
-
274
- // Table for the todo items in this workspace
275
- const todoTable = State.SQLite.table({
276
- name: 'todo',
277
- columns: {
278
- todoId: State.SQLite.text({ primaryKey: true }),
279
- text: State.SQLite.text(),
280
- completed: State.SQLite.boolean({ default: false }),
281
- // Using soft delete by adding a deletedAt timestamp
282
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
283
- },
284
- })
285
-
286
- // Table for members of this workspace
287
- const memberTable = State.SQLite.table({
288
- name: 'member',
289
- columns: {
290
- username: State.SQLite.text({ primaryKey: true }),
291
- // Could add role/permissions here later
292
- },
293
- })
294
-
295
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
296
-
297
- const materializers = State.SQLite.materializers(workspaceEvents, {
298
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
299
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
300
- // Add the creator as the first member
301
- workspaceTables.member.insert({ username: createdByUsername }),
302
- ],
303
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
304
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
305
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
306
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
307
- })
308
-
309
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
310
-
311
- export const schema = makeSchema({ events: workspaceEvents, state })
312
- ```
313
-
314
- **User store:**
315
-
316
- ## `data-modeling/todo-workspaces/multi-store/user.store.ts`
317
-
318
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.store.ts"
319
-
320
- const adapter = makePersistedAdapter({
321
- storage: { type: 'opfs' },
322
- worker,
323
- sharedWorker,
324
- })
325
-
326
- // Define user store configuration
327
- // Each user has their own store to track which workspaces they're part of
328
- export const userStoreOptions = (username: string) =>
329
- storeOptions({
330
- storeId: `user-${username}`,
331
- schema,
332
- adapter,
333
- unusedCacheTime: Number.POSITIVE_INFINITY, // Keep user store in memory indefinitely
334
- })
335
- ```
336
-
337
- ### `data-modeling/todo-workspaces/multi-store/user.schema.ts`
338
-
339
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.schema.ts"
340
-
341
- // Emitted when this user creates a new workspace
342
- const workspaceCreated = Events.synced({
343
- name: 'v1.WorkspaceCreated',
344
- schema: Schema.Struct({ workspaceId: Schema.String }),
345
- })
346
-
347
- // Emitted when this user joins an existing workspace
348
- const workspaceJoined = Events.synced({
349
- name: 'v1.WorkspaceJoined',
350
- schema: Schema.Struct({ workspaceId: Schema.String }),
351
- })
352
-
353
- const events = { workspaceCreated, workspaceJoined }
354
-
355
- // Table to store basic user info
356
- // Contains only one row as this store is per-user.
357
- const userTable = State.SQLite.table({
358
- name: 'user',
359
- columns: {
360
- // Assuming username is unique and used as the identifier
361
- username: State.SQLite.text({ primaryKey: true }),
362
- },
363
- })
364
-
365
- // Table to track which workspaces this user is part of
366
- const userWorkspaceTable = State.SQLite.table({
367
- name: 'userWorkspace',
368
- columns: {
369
- workspaceId: State.SQLite.text({ primaryKey: true }),
370
- // Could add role/permissions here later
371
- },
372
- })
373
-
374
- export const userTables = { user: userTable, userWorkspace: userWorkspaceTable }
375
-
376
- const materializers = State.SQLite.materializers(events, {
377
- // When the user creates or joins a workspace, add it to their workspace table
378
- 'v1.WorkspaceCreated': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
379
- 'v1.WorkspaceJoined': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
380
- })
381
-
382
- const state = State.SQLite.makeState({ tables: userTables, materializers })
383
-
384
- export const schema = makeSchema({ events, state })
385
- ```
386
-
387
- ### App Setup
388
-
389
- Create a [`StoreRegistry`](/framework-integrations/react-integration#new-storeregistryconfig) and provide it to your React app:
390
-
391
- ## `data-modeling/todo-workspaces/multi-store/App.tsx`
392
-
393
- ```tsx filename="data-modeling/todo-workspaces/multi-store/App.tsx"
394
-
395
- export function App({ children }: { children: ReactNode }) {
396
- const [storeRegistry] = useState(
397
- () =>
398
- new StoreRegistry({
399
- defaultOptions: {
400
- batchUpdates,
401
- },
402
- }),
403
- )
404
-
405
- return <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
406
- }
407
- ```
408
-
409
- ### Accessing Stores
410
-
411
- Use the [`useStore()`](/framework-integrations/react-integration#usestoreoptions) hook to access specific workspace instances:
412
-
413
- ## `data-modeling/todo-workspaces/multi-store/Workspace.tsx`
414
-
415
- ```tsx filename="data-modeling/todo-workspaces/multi-store/Workspace.tsx"
416
-
417
- // Component that accesses a specific workspace store
418
- function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
419
- // Load the workspace store for this specific workspace
420
- const workspaceStore = useStore(workspaceStoreOptions(workspaceId))
421
-
422
- // Query workspace data
423
- const [workspace] = workspaceStore.useQuery(queryDb(workspaceTables.workspace.select().limit(1)))
424
- const todos = workspaceStore.useQuery(queryDb(workspaceTables.todo.select()))
425
-
426
- if (!workspace) return <div>Workspace not found</div>
427
-
428
- const addTodo = (text: string) => {
429
- workspaceStore.commit(
430
- workspaceEvents.todoAdded({
431
- todoId: `todo-${Date.now()}`,
432
- text,
433
- }),
434
- )
435
- }
436
-
437
- return (
438
- <div>
439
- <h2>{workspace.name}</h2>
440
- <p>Created by: {workspace.createdByUsername}</p>
441
- <p>Store ID: {workspaceStore.storeId}</p>
442
-
443
- <h3>Todos ({todos.length})</h3>
444
- <ul>
445
- {todos.map((todo) => (
446
- <li key={todo.todoId}>
447
- {todo.text} {todo.completed ? '✓' : ''}
448
- </li>
449
- ))}
450
- </ul>
451
-
452
- <button type="button" onClick={() => addTodo('New todo')}>
453
- Add Todo
454
- </button>
455
- </div>
456
- )
457
- }
458
-
459
- // Workspace component with Suspense and ErrorBoundary
460
- export function Workspace({ workspaceId }: { workspaceId: string }) {
461
- return (
462
- <ErrorBoundary fallback={<div>Error loading workspace</div>}>
463
- <Suspense fallback={<div>Loading workspace...</div>}>
464
- <WorkspaceContent workspaceId={workspaceId} />
465
- </Suspense>
466
- </ErrorBoundary>
467
- )
468
- }
469
- ```
470
-
471
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
472
-
473
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
474
-
475
- // Emitted when a new workspace is created (originates this store)
476
- const workspaceCreated = Events.synced({
477
- name: 'v1.WorkspaceCreated',
478
- schema: Schema.Struct({
479
- workspaceId: Schema.String,
480
- name: Schema.String,
481
- createdByUsername: Schema.String,
482
- }),
483
- })
484
-
485
- // Emitted when a todo item is added to this workspace
486
- const todoAdded = Events.synced({
487
- name: 'v1.TodoAdded',
488
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
489
- })
490
-
491
- // Emitted when a todo item is marked as completed
492
- const todoCompleted = Events.synced({
493
- name: 'v1.TodoCompleted',
494
- schema: Schema.Struct({ todoId: Schema.String }),
495
- })
496
-
497
- // Emitted when a todo item is deleted (soft delete)
498
- const todoDeleted = Events.synced({
499
- name: 'v1.TodoDeleted',
500
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
501
- })
502
-
503
- // Emitted when a new user joins this workspace
504
- const userJoined = Events.synced({
505
- name: 'v1.UserJoined',
506
- schema: Schema.Struct({ username: Schema.String }),
507
- })
508
-
509
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
510
-
511
- // Table for the workspace itself (only one row as this store is per-workspace)
512
- const workspaceTable = State.SQLite.table({
513
- name: 'workspace',
514
- columns: {
515
- workspaceId: State.SQLite.text({ primaryKey: true }),
516
- name: State.SQLite.text(),
517
- createdByUsername: State.SQLite.text(),
518
- },
519
- })
520
-
521
- // Table for the todo items in this workspace
522
- const todoTable = State.SQLite.table({
523
- name: 'todo',
524
- columns: {
525
- todoId: State.SQLite.text({ primaryKey: true }),
526
- text: State.SQLite.text(),
527
- completed: State.SQLite.boolean({ default: false }),
528
- // Using soft delete by adding a deletedAt timestamp
529
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
530
- },
531
- })
532
-
533
- // Table for members of this workspace
534
- const memberTable = State.SQLite.table({
535
- name: 'member',
536
- columns: {
537
- username: State.SQLite.text({ primaryKey: true }),
538
- // Could add role/permissions here later
539
- },
540
- })
541
-
542
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
543
-
544
- const materializers = State.SQLite.materializers(workspaceEvents, {
545
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
546
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
547
- // Add the creator as the first member
548
- workspaceTables.member.insert({ username: createdByUsername }),
549
- ],
550
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
551
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
552
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
553
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
554
- })
555
-
556
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
557
-
558
- export const schema = makeSchema({ events: workspaceEvents, state })
559
- ```
560
-
561
- ### `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
562
-
563
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
564
-
565
- const adapter = makePersistedAdapter({
566
- storage: { type: 'opfs' },
567
- worker,
568
- sharedWorker,
569
- })
570
-
571
- // Define workspace store configuration
572
- // Each workspace gets its own isolated store instance
573
- export const workspaceStoreOptions = (workspaceId: string) =>
574
- storeOptions({
575
- storeId: `workspace-${workspaceId}`,
576
- schema,
577
- adapter,
578
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
579
- })
580
- ```
581
-
582
- ### Loading Multiple Workspaces
583
-
584
- To display all workspaces for a user, first load the user store to get their workspace list, then dynamically load each workspace:
585
-
586
- ## `data-modeling/todo-workspaces/multi-store/WorkspaceList.tsx`
587
-
588
- ```tsx filename="data-modeling/todo-workspaces/multi-store/WorkspaceList.tsx"
589
-
590
- // Component that displays all workspaces for a user
591
- function WorkspaceListContent({ username }: { username: string }) {
592
- // Load the user store to get their workspace list
593
- const userStore = useStore(userStoreOptions(username))
594
-
595
- // Query all workspaces this user belongs to
596
- const workspaces = userStore.useQuery(queryDb(userTables.userWorkspace.select()))
597
-
598
- return (
599
- <div>
600
- <h1>My Workspaces</h1>
601
- {workspaces.length === 0 ? (
602
- <p>No workspaces yet</p>
603
- ) : (
604
- workspaces.map((w) => (
605
- <div key={w.workspaceId}>
606
- <Workspace workspaceId={w.workspaceId} />
607
- </div>
608
- ))
609
- )}
610
- </div>
611
- )
612
- }
613
-
614
- // Full workspace list with Suspense
615
- export function WorkspaceList({ username }: { username: string }) {
616
- return (
617
- <ErrorBoundary fallback={<div>Error loading workspaces</div>}>
618
- <Suspense fallback={<div>Loading workspaces...</div>}>
619
- <WorkspaceListContent username={username} />
620
- </Suspense>
621
- </ErrorBoundary>
622
- )
623
- }
624
- ```
625
-
626
- ### `data-modeling/todo-workspaces/multi-store/user.schema.ts`
627
-
628
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.schema.ts"
629
-
630
- // Emitted when this user creates a new workspace
631
- const workspaceCreated = Events.synced({
632
- name: 'v1.WorkspaceCreated',
633
- schema: Schema.Struct({ workspaceId: Schema.String }),
634
- })
635
-
636
- // Emitted when this user joins an existing workspace
637
- const workspaceJoined = Events.synced({
638
- name: 'v1.WorkspaceJoined',
639
- schema: Schema.Struct({ workspaceId: Schema.String }),
640
- })
641
-
642
- const events = { workspaceCreated, workspaceJoined }
643
-
644
- // Table to store basic user info
645
- // Contains only one row as this store is per-user.
646
- const userTable = State.SQLite.table({
647
- name: 'user',
648
- columns: {
649
- // Assuming username is unique and used as the identifier
650
- username: State.SQLite.text({ primaryKey: true }),
651
- },
652
- })
653
-
654
- // Table to track which workspaces this user is part of
655
- const userWorkspaceTable = State.SQLite.table({
656
- name: 'userWorkspace',
657
- columns: {
658
- workspaceId: State.SQLite.text({ primaryKey: true }),
659
- // Could add role/permissions here later
660
- },
661
- })
662
-
663
- export const userTables = { user: userTable, userWorkspace: userWorkspaceTable }
664
-
665
- const materializers = State.SQLite.materializers(events, {
666
- // When the user creates or joins a workspace, add it to their workspace table
667
- 'v1.WorkspaceCreated': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
668
- 'v1.WorkspaceJoined': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
669
- })
670
-
671
- const state = State.SQLite.makeState({ tables: userTables, materializers })
672
-
673
- export const schema = makeSchema({ events, state })
674
- ```
675
-
676
- ### `data-modeling/todo-workspaces/multi-store/user.store.ts`
677
-
678
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.store.ts"
679
-
680
- const adapter = makePersistedAdapter({
681
- storage: { type: 'opfs' },
682
- worker,
683
- sharedWorker,
684
- })
685
-
686
- // Define user store configuration
687
- // Each user has their own store to track which workspaces they're part of
688
- export const userStoreOptions = (username: string) =>
689
- storeOptions({
690
- storeId: `user-${username}`,
691
- schema,
692
- adapter,
693
- unusedCacheTime: Number.POSITIVE_INFINITY, // Keep user store in memory indefinitely
694
- })
695
- ```
696
-
697
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
698
-
699
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
700
-
701
- // Emitted when a new workspace is created (originates this store)
702
- const workspaceCreated = Events.synced({
703
- name: 'v1.WorkspaceCreated',
704
- schema: Schema.Struct({
705
- workspaceId: Schema.String,
706
- name: Schema.String,
707
- createdByUsername: Schema.String,
708
- }),
709
- })
710
-
711
- // Emitted when a todo item is added to this workspace
712
- const todoAdded = Events.synced({
713
- name: 'v1.TodoAdded',
714
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
715
- })
716
-
717
- // Emitted when a todo item is marked as completed
718
- const todoCompleted = Events.synced({
719
- name: 'v1.TodoCompleted',
720
- schema: Schema.Struct({ todoId: Schema.String }),
721
- })
722
-
723
- // Emitted when a todo item is deleted (soft delete)
724
- const todoDeleted = Events.synced({
725
- name: 'v1.TodoDeleted',
726
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
727
- })
728
-
729
- // Emitted when a new user joins this workspace
730
- const userJoined = Events.synced({
731
- name: 'v1.UserJoined',
732
- schema: Schema.Struct({ username: Schema.String }),
733
- })
734
-
735
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
736
-
737
- // Table for the workspace itself (only one row as this store is per-workspace)
738
- const workspaceTable = State.SQLite.table({
739
- name: 'workspace',
740
- columns: {
741
- workspaceId: State.SQLite.text({ primaryKey: true }),
742
- name: State.SQLite.text(),
743
- createdByUsername: State.SQLite.text(),
744
- },
745
- })
746
-
747
- // Table for the todo items in this workspace
748
- const todoTable = State.SQLite.table({
749
- name: 'todo',
750
- columns: {
751
- todoId: State.SQLite.text({ primaryKey: true }),
752
- text: State.SQLite.text(),
753
- completed: State.SQLite.boolean({ default: false }),
754
- // Using soft delete by adding a deletedAt timestamp
755
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
756
- },
757
- })
758
-
759
- // Table for members of this workspace
760
- const memberTable = State.SQLite.table({
761
- name: 'member',
762
- columns: {
763
- username: State.SQLite.text({ primaryKey: true }),
764
- // Could add role/permissions here later
765
- },
766
- })
767
-
768
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
769
-
770
- const materializers = State.SQLite.materializers(workspaceEvents, {
771
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
772
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
773
- // Add the creator as the first member
774
- workspaceTables.member.insert({ username: createdByUsername }),
775
- ],
776
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
777
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
778
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
779
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
780
- })
781
-
782
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
783
-
784
- export const schema = makeSchema({ events: workspaceEvents, state })
785
- ```
786
-
787
- ### `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
788
-
789
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
790
-
791
- const adapter = makePersistedAdapter({
792
- storage: { type: 'opfs' },
793
- worker,
794
- sharedWorker,
795
- })
796
-
797
- // Define workspace store configuration
798
- // Each workspace gets its own isolated store instance
799
- export const workspaceStoreOptions = (workspaceId: string) =>
800
- storeOptions({
801
- storeId: `workspace-${workspaceId}`,
802
- schema,
803
- adapter,
804
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
805
- })
806
- ```
807
-
808
- ### `data-modeling/todo-workspaces/multi-store/Workspace.tsx`
809
-
810
- ```tsx filename="data-modeling/todo-workspaces/multi-store/Workspace.tsx"
811
-
812
- // Component that accesses a specific workspace store
813
- function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
814
- // Load the workspace store for this specific workspace
815
- const workspaceStore = useStore(workspaceStoreOptions(workspaceId))
816
-
817
- // Query workspace data
818
- const [workspace] = workspaceStore.useQuery(queryDb(workspaceTables.workspace.select().limit(1)))
819
- const todos = workspaceStore.useQuery(queryDb(workspaceTables.todo.select()))
820
-
821
- if (!workspace) return <div>Workspace not found</div>
822
-
823
- const addTodo = (text: string) => {
824
- workspaceStore.commit(
825
- workspaceEvents.todoAdded({
826
- todoId: `todo-${Date.now()}`,
827
- text,
828
- }),
829
- )
830
- }
831
-
832
- return (
833
- <div>
834
- <h2>{workspace.name}</h2>
835
- <p>Created by: {workspace.createdByUsername}</p>
836
- <p>Store ID: {workspaceStore.storeId}</p>
837
-
838
- <h3>Todos ({todos.length})</h3>
839
- <ul>
840
- {todos.map((todo) => (
841
- <li key={todo.todoId}>
842
- {todo.text} {todo.completed ? '✓' : ''}
843
- </li>
844
- ))}
845
- </ul>
846
-
847
- <button type="button" onClick={() => addTodo('New todo')}>
848
- Add Todo
849
- </button>
850
- </div>
851
- )
852
- }
853
-
854
- // Workspace component with Suspense and ErrorBoundary
855
- export function Workspace({ workspaceId }: { workspaceId: string }) {
856
- return (
857
- <ErrorBoundary fallback={<div>Error loading workspace</div>}>
858
- <Suspense fallback={<div>Loading workspace...</div>}>
859
- <WorkspaceContent workspaceId={workspaceId} />
860
- </Suspense>
861
- </ErrorBoundary>
862
- )
863
- }
864
- ```
865
-
866
- ## Further notes
867
-
868
- To make this app more production-ready, we might want to do the following:
869
- - Use a proper auth setup to enforce a trusted user identity
870
- - Introduce a proper user invite process
871
- - Introduce access levels (e.g. read-only, read-write)
872
- - Introduce end-to-end encryption
873
-
874
- ### Individual todo stores for complex data
875
-
876
- If each todo item has a lot of data (e.g. think of a GitHub/Linear issue with lots of details), it might make sense to split up each todo item into its own store.
877
-
878
- This would create **3 store types** instead of 2:
879
- - **User stores** (one per user) - unchanged
880
- - **Workspace stores** (one per workspace) - only basic todo metadata
881
- - **Todo stores** (one per todo item) - rich todo data
882
-
883
- Your app would then have **N + M + K stores** total (N workspaces + M users + K todo items).
884
-
885
- This pattern improves performance by only loading detailed todo data when specifically viewing that item, and prevents large todos from slowing down workspace syncing.