@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,891 +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
- :::caution[Experimental API]
198
- This guide uses the [experimental multi-store API](/framework-integrations/react-integration#multi-store) which is still early in its development.
199
-
200
- If you have feedback or questions about this API, please don't hesitate to comment on the [RFC](https://github.com/livestorejs/livestore/pull/585)
201
- :::
202
-
203
- ### Store Configuration
204
-
205
- First, define store options for each store type using [`storeOptions()`](/framework-integrations/react-integration#storeoptionsoptions):
206
-
207
- **Workspace store:**
208
-
209
- ## `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
210
-
211
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
212
-
213
- const adapter = makePersistedAdapter({
214
- storage: { type: 'opfs' },
215
- worker,
216
- sharedWorker,
217
- })
218
-
219
- // Define workspace store configuration
220
- // Each workspace gets its own isolated store instance
221
- export const workspaceStoreOptions = (workspaceId: string) =>
222
- storeOptions({
223
- storeId: `workspace-${workspaceId}`,
224
- schema,
225
- adapter,
226
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
227
- })
228
- ```
229
-
230
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
231
-
232
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
233
-
234
- // Emitted when a new workspace is created (originates this store)
235
- const workspaceCreated = Events.synced({
236
- name: 'v1.WorkspaceCreated',
237
- schema: Schema.Struct({
238
- workspaceId: Schema.String,
239
- name: Schema.String,
240
- createdByUsername: Schema.String,
241
- }),
242
- })
243
-
244
- // Emitted when a todo item is added to this workspace
245
- const todoAdded = Events.synced({
246
- name: 'v1.TodoAdded',
247
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
248
- })
249
-
250
- // Emitted when a todo item is marked as completed
251
- const todoCompleted = Events.synced({
252
- name: 'v1.TodoCompleted',
253
- schema: Schema.Struct({ todoId: Schema.String }),
254
- })
255
-
256
- // Emitted when a todo item is deleted (soft delete)
257
- const todoDeleted = Events.synced({
258
- name: 'v1.TodoDeleted',
259
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
260
- })
261
-
262
- // Emitted when a new user joins this workspace
263
- const userJoined = Events.synced({
264
- name: 'v1.UserJoined',
265
- schema: Schema.Struct({ username: Schema.String }),
266
- })
267
-
268
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
269
-
270
- // Table for the workspace itself (only one row as this store is per-workspace)
271
- const workspaceTable = State.SQLite.table({
272
- name: 'workspace',
273
- columns: {
274
- workspaceId: State.SQLite.text({ primaryKey: true }),
275
- name: State.SQLite.text(),
276
- createdByUsername: State.SQLite.text(),
277
- },
278
- })
279
-
280
- // Table for the todo items in this workspace
281
- const todoTable = State.SQLite.table({
282
- name: 'todo',
283
- columns: {
284
- todoId: State.SQLite.text({ primaryKey: true }),
285
- text: State.SQLite.text(),
286
- completed: State.SQLite.boolean({ default: false }),
287
- // Using soft delete by adding a deletedAt timestamp
288
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
289
- },
290
- })
291
-
292
- // Table for members of this workspace
293
- const memberTable = State.SQLite.table({
294
- name: 'member',
295
- columns: {
296
- username: State.SQLite.text({ primaryKey: true }),
297
- // Could add role/permissions here later
298
- },
299
- })
300
-
301
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
302
-
303
- const materializers = State.SQLite.materializers(workspaceEvents, {
304
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
305
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
306
- // Add the creator as the first member
307
- workspaceTables.member.insert({ username: createdByUsername }),
308
- ],
309
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
310
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
311
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
312
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
313
- })
314
-
315
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
316
-
317
- export const schema = makeSchema({ events: workspaceEvents, state })
318
- ```
319
-
320
- **User store:**
321
-
322
- ## `data-modeling/todo-workspaces/multi-store/user.store.ts`
323
-
324
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.store.ts"
325
-
326
- const adapter = makePersistedAdapter({
327
- storage: { type: 'opfs' },
328
- worker,
329
- sharedWorker,
330
- })
331
-
332
- // Define user store configuration
333
- // Each user has their own store to track which workspaces they're part of
334
- export const userStoreOptions = (username: string) =>
335
- storeOptions({
336
- storeId: `user-${username}`,
337
- schema,
338
- adapter,
339
- unusedCacheTime: Number.POSITIVE_INFINITY, // Keep user store in memory indefinitely
340
- })
341
- ```
342
-
343
- ### `data-modeling/todo-workspaces/multi-store/user.schema.ts`
344
-
345
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.schema.ts"
346
-
347
- // Emitted when this user creates a new workspace
348
- const workspaceCreated = Events.synced({
349
- name: 'v1.WorkspaceCreated',
350
- schema: Schema.Struct({ workspaceId: Schema.String }),
351
- })
352
-
353
- // Emitted when this user joins an existing workspace
354
- const workspaceJoined = Events.synced({
355
- name: 'v1.WorkspaceJoined',
356
- schema: Schema.Struct({ workspaceId: Schema.String }),
357
- })
358
-
359
- const events = { workspaceCreated, workspaceJoined }
360
-
361
- // Table to store basic user info
362
- // Contains only one row as this store is per-user.
363
- const userTable = State.SQLite.table({
364
- name: 'user',
365
- columns: {
366
- // Assuming username is unique and used as the identifier
367
- username: State.SQLite.text({ primaryKey: true }),
368
- },
369
- })
370
-
371
- // Table to track which workspaces this user is part of
372
- const userWorkspaceTable = State.SQLite.table({
373
- name: 'userWorkspace',
374
- columns: {
375
- workspaceId: State.SQLite.text({ primaryKey: true }),
376
- // Could add role/permissions here later
377
- },
378
- })
379
-
380
- export const userTables = { user: userTable, userWorkspace: userWorkspaceTable }
381
-
382
- const materializers = State.SQLite.materializers(events, {
383
- // When the user creates or joins a workspace, add it to their workspace table
384
- 'v1.WorkspaceCreated': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
385
- 'v1.WorkspaceJoined': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
386
- })
387
-
388
- const state = State.SQLite.makeState({ tables: userTables, materializers })
389
-
390
- export const schema = makeSchema({ events, state })
391
- ```
392
-
393
- ### App Setup
394
-
395
- Create a [`StoreRegistry`](/framework-integrations/react-integration#new-storeregistryconfig) and provide it to your React app:
396
-
397
- ## `data-modeling/todo-workspaces/multi-store/App.tsx`
398
-
399
- ```tsx filename="data-modeling/todo-workspaces/multi-store/App.tsx"
400
-
401
- export function App({ children }: { children: ReactNode }) {
402
- const [storeRegistry] = useState(
403
- () =>
404
- new StoreRegistry({
405
- defaultOptions: {
406
- batchUpdates,
407
- },
408
- }),
409
- )
410
-
411
- return <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
412
- }
413
- ```
414
-
415
- ### Accessing Stores
416
-
417
- Use the [`useStore()`](/framework-integrations/react-integration#usestoreoptions) hook to access specific workspace instances:
418
-
419
- ## `data-modeling/todo-workspaces/multi-store/Workspace.tsx`
420
-
421
- ```tsx filename="data-modeling/todo-workspaces/multi-store/Workspace.tsx"
422
-
423
- // Component that accesses a specific workspace store
424
- function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
425
- // Load the workspace store for this specific workspace
426
- const workspaceStore = useStore(workspaceStoreOptions(workspaceId))
427
-
428
- // Query workspace data
429
- const [workspace] = workspaceStore.useQuery(queryDb(workspaceTables.workspace.select().limit(1)))
430
- const todos = workspaceStore.useQuery(queryDb(workspaceTables.todo.select()))
431
-
432
- if (!workspace) return <div>Workspace not found</div>
433
-
434
- const addTodo = (text: string) => {
435
- workspaceStore.commit(
436
- workspaceEvents.todoAdded({
437
- todoId: `todo-${Date.now()}`,
438
- text,
439
- }),
440
- )
441
- }
442
-
443
- return (
444
- <div>
445
- <h2>{workspace.name}</h2>
446
- <p>Created by: {workspace.createdByUsername}</p>
447
- <p>Store ID: {workspaceStore.storeId}</p>
448
-
449
- <h3>Todos ({todos.length})</h3>
450
- <ul>
451
- {todos.map((todo) => (
452
- <li key={todo.todoId}>
453
- {todo.text} {todo.completed ? '✓' : ''}
454
- </li>
455
- ))}
456
- </ul>
457
-
458
- <button type="button" onClick={() => addTodo('New todo')}>
459
- Add Todo
460
- </button>
461
- </div>
462
- )
463
- }
464
-
465
- // Workspace component with Suspense and ErrorBoundary
466
- export function Workspace({ workspaceId }: { workspaceId: string }) {
467
- return (
468
- <ErrorBoundary fallback={<div>Error loading workspace</div>}>
469
- <Suspense fallback={<div>Loading workspace...</div>}>
470
- <WorkspaceContent workspaceId={workspaceId} />
471
- </Suspense>
472
- </ErrorBoundary>
473
- )
474
- }
475
- ```
476
-
477
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
478
-
479
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
480
-
481
- // Emitted when a new workspace is created (originates this store)
482
- const workspaceCreated = Events.synced({
483
- name: 'v1.WorkspaceCreated',
484
- schema: Schema.Struct({
485
- workspaceId: Schema.String,
486
- name: Schema.String,
487
- createdByUsername: Schema.String,
488
- }),
489
- })
490
-
491
- // Emitted when a todo item is added to this workspace
492
- const todoAdded = Events.synced({
493
- name: 'v1.TodoAdded',
494
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
495
- })
496
-
497
- // Emitted when a todo item is marked as completed
498
- const todoCompleted = Events.synced({
499
- name: 'v1.TodoCompleted',
500
- schema: Schema.Struct({ todoId: Schema.String }),
501
- })
502
-
503
- // Emitted when a todo item is deleted (soft delete)
504
- const todoDeleted = Events.synced({
505
- name: 'v1.TodoDeleted',
506
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
507
- })
508
-
509
- // Emitted when a new user joins this workspace
510
- const userJoined = Events.synced({
511
- name: 'v1.UserJoined',
512
- schema: Schema.Struct({ username: Schema.String }),
513
- })
514
-
515
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
516
-
517
- // Table for the workspace itself (only one row as this store is per-workspace)
518
- const workspaceTable = State.SQLite.table({
519
- name: 'workspace',
520
- columns: {
521
- workspaceId: State.SQLite.text({ primaryKey: true }),
522
- name: State.SQLite.text(),
523
- createdByUsername: State.SQLite.text(),
524
- },
525
- })
526
-
527
- // Table for the todo items in this workspace
528
- const todoTable = State.SQLite.table({
529
- name: 'todo',
530
- columns: {
531
- todoId: State.SQLite.text({ primaryKey: true }),
532
- text: State.SQLite.text(),
533
- completed: State.SQLite.boolean({ default: false }),
534
- // Using soft delete by adding a deletedAt timestamp
535
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
536
- },
537
- })
538
-
539
- // Table for members of this workspace
540
- const memberTable = State.SQLite.table({
541
- name: 'member',
542
- columns: {
543
- username: State.SQLite.text({ primaryKey: true }),
544
- // Could add role/permissions here later
545
- },
546
- })
547
-
548
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
549
-
550
- const materializers = State.SQLite.materializers(workspaceEvents, {
551
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
552
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
553
- // Add the creator as the first member
554
- workspaceTables.member.insert({ username: createdByUsername }),
555
- ],
556
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
557
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
558
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
559
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
560
- })
561
-
562
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
563
-
564
- export const schema = makeSchema({ events: workspaceEvents, state })
565
- ```
566
-
567
- ### `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
568
-
569
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
570
-
571
- const adapter = makePersistedAdapter({
572
- storage: { type: 'opfs' },
573
- worker,
574
- sharedWorker,
575
- })
576
-
577
- // Define workspace store configuration
578
- // Each workspace gets its own isolated store instance
579
- export const workspaceStoreOptions = (workspaceId: string) =>
580
- storeOptions({
581
- storeId: `workspace-${workspaceId}`,
582
- schema,
583
- adapter,
584
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
585
- })
586
- ```
587
-
588
- ### Loading Multiple Workspaces
589
-
590
- To display all workspaces for a user, first load the user store to get their workspace list, then dynamically load each workspace:
591
-
592
- ## `data-modeling/todo-workspaces/multi-store/WorkspaceList.tsx`
593
-
594
- ```tsx filename="data-modeling/todo-workspaces/multi-store/WorkspaceList.tsx"
595
-
596
- // Component that displays all workspaces for a user
597
- function WorkspaceListContent({ username }: { username: string }) {
598
- // Load the user store to get their workspace list
599
- const userStore = useStore(userStoreOptions(username))
600
-
601
- // Query all workspaces this user belongs to
602
- const workspaces = userStore.useQuery(queryDb(userTables.userWorkspace.select()))
603
-
604
- return (
605
- <div>
606
- <h1>My Workspaces</h1>
607
- {workspaces.length === 0 ? (
608
- <p>No workspaces yet</p>
609
- ) : (
610
- workspaces.map((w) => (
611
- <div key={w.workspaceId}>
612
- <Workspace workspaceId={w.workspaceId} />
613
- </div>
614
- ))
615
- )}
616
- </div>
617
- )
618
- }
619
-
620
- // Full workspace list with Suspense
621
- export function WorkspaceList({ username }: { username: string }) {
622
- return (
623
- <ErrorBoundary fallback={<div>Error loading workspaces</div>}>
624
- <Suspense fallback={<div>Loading workspaces...</div>}>
625
- <WorkspaceListContent username={username} />
626
- </Suspense>
627
- </ErrorBoundary>
628
- )
629
- }
630
- ```
631
-
632
- ### `data-modeling/todo-workspaces/multi-store/user.schema.ts`
633
-
634
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.schema.ts"
635
-
636
- // Emitted when this user creates a new workspace
637
- const workspaceCreated = Events.synced({
638
- name: 'v1.WorkspaceCreated',
639
- schema: Schema.Struct({ workspaceId: Schema.String }),
640
- })
641
-
642
- // Emitted when this user joins an existing workspace
643
- const workspaceJoined = Events.synced({
644
- name: 'v1.WorkspaceJoined',
645
- schema: Schema.Struct({ workspaceId: Schema.String }),
646
- })
647
-
648
- const events = { workspaceCreated, workspaceJoined }
649
-
650
- // Table to store basic user info
651
- // Contains only one row as this store is per-user.
652
- const userTable = State.SQLite.table({
653
- name: 'user',
654
- columns: {
655
- // Assuming username is unique and used as the identifier
656
- username: State.SQLite.text({ primaryKey: true }),
657
- },
658
- })
659
-
660
- // Table to track which workspaces this user is part of
661
- const userWorkspaceTable = State.SQLite.table({
662
- name: 'userWorkspace',
663
- columns: {
664
- workspaceId: State.SQLite.text({ primaryKey: true }),
665
- // Could add role/permissions here later
666
- },
667
- })
668
-
669
- export const userTables = { user: userTable, userWorkspace: userWorkspaceTable }
670
-
671
- const materializers = State.SQLite.materializers(events, {
672
- // When the user creates or joins a workspace, add it to their workspace table
673
- 'v1.WorkspaceCreated': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
674
- 'v1.WorkspaceJoined': ({ workspaceId }) => userTables.userWorkspace.insert({ workspaceId }),
675
- })
676
-
677
- const state = State.SQLite.makeState({ tables: userTables, materializers })
678
-
679
- export const schema = makeSchema({ events, state })
680
- ```
681
-
682
- ### `data-modeling/todo-workspaces/multi-store/user.store.ts`
683
-
684
- ```ts filename="data-modeling/todo-workspaces/multi-store/user.store.ts"
685
-
686
- const adapter = makePersistedAdapter({
687
- storage: { type: 'opfs' },
688
- worker,
689
- sharedWorker,
690
- })
691
-
692
- // Define user store configuration
693
- // Each user has their own store to track which workspaces they're part of
694
- export const userStoreOptions = (username: string) =>
695
- storeOptions({
696
- storeId: `user-${username}`,
697
- schema,
698
- adapter,
699
- unusedCacheTime: Number.POSITIVE_INFINITY, // Keep user store in memory indefinitely
700
- })
701
- ```
702
-
703
- ### `data-modeling/todo-workspaces/multi-store/workspace.schema.ts`
704
-
705
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.schema.ts"
706
-
707
- // Emitted when a new workspace is created (originates this store)
708
- const workspaceCreated = Events.synced({
709
- name: 'v1.WorkspaceCreated',
710
- schema: Schema.Struct({
711
- workspaceId: Schema.String,
712
- name: Schema.String,
713
- createdByUsername: Schema.String,
714
- }),
715
- })
716
-
717
- // Emitted when a todo item is added to this workspace
718
- const todoAdded = Events.synced({
719
- name: 'v1.TodoAdded',
720
- schema: Schema.Struct({ todoId: Schema.String, text: Schema.String }),
721
- })
722
-
723
- // Emitted when a todo item is marked as completed
724
- const todoCompleted = Events.synced({
725
- name: 'v1.TodoCompleted',
726
- schema: Schema.Struct({ todoId: Schema.String }),
727
- })
728
-
729
- // Emitted when a todo item is deleted (soft delete)
730
- const todoDeleted = Events.synced({
731
- name: 'v1.TodoDeleted',
732
- schema: Schema.Struct({ todoId: Schema.String, deletedAt: Schema.Date }),
733
- })
734
-
735
- // Emitted when a new user joins this workspace
736
- const userJoined = Events.synced({
737
- name: 'v1.UserJoined',
738
- schema: Schema.Struct({ username: Schema.String }),
739
- })
740
-
741
- export const workspaceEvents = { workspaceCreated, todoAdded, todoCompleted, todoDeleted, userJoined }
742
-
743
- // Table for the workspace itself (only one row as this store is per-workspace)
744
- const workspaceTable = State.SQLite.table({
745
- name: 'workspace',
746
- columns: {
747
- workspaceId: State.SQLite.text({ primaryKey: true }),
748
- name: State.SQLite.text(),
749
- createdByUsername: State.SQLite.text(),
750
- },
751
- })
752
-
753
- // Table for the todo items in this workspace
754
- const todoTable = State.SQLite.table({
755
- name: 'todo',
756
- columns: {
757
- todoId: State.SQLite.text({ primaryKey: true }),
758
- text: State.SQLite.text(),
759
- completed: State.SQLite.boolean({ default: false }),
760
- // Using soft delete by adding a deletedAt timestamp
761
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
762
- },
763
- })
764
-
765
- // Table for members of this workspace
766
- const memberTable = State.SQLite.table({
767
- name: 'member',
768
- columns: {
769
- username: State.SQLite.text({ primaryKey: true }),
770
- // Could add role/permissions here later
771
- },
772
- })
773
-
774
- export const workspaceTables = { workspace: workspaceTable, todo: todoTable, member: memberTable }
775
-
776
- const materializers = State.SQLite.materializers(workspaceEvents, {
777
- 'v1.WorkspaceCreated': ({ workspaceId, name, createdByUsername }) => [
778
- workspaceTables.workspace.insert({ workspaceId, name, createdByUsername }),
779
- // Add the creator as the first member
780
- workspaceTables.member.insert({ username: createdByUsername }),
781
- ],
782
- 'v1.TodoAdded': ({ todoId, text }) => workspaceTables.todo.insert({ todoId, text }),
783
- 'v1.TodoCompleted': ({ todoId }) => workspaceTables.todo.update({ completed: true }).where({ todoId }),
784
- 'v1.TodoDeleted': ({ todoId, deletedAt }) => workspaceTables.todo.update({ deletedAt }).where({ todoId }),
785
- 'v1.UserJoined': ({ username }) => workspaceTables.member.insert({ username }),
786
- })
787
-
788
- const state = State.SQLite.makeState({ tables: workspaceTables, materializers })
789
-
790
- export const schema = makeSchema({ events: workspaceEvents, state })
791
- ```
792
-
793
- ### `data-modeling/todo-workspaces/multi-store/workspace.store.ts`
794
-
795
- ```ts filename="data-modeling/todo-workspaces/multi-store/workspace.store.ts"
796
-
797
- const adapter = makePersistedAdapter({
798
- storage: { type: 'opfs' },
799
- worker,
800
- sharedWorker,
801
- })
802
-
803
- // Define workspace store configuration
804
- // Each workspace gets its own isolated store instance
805
- export const workspaceStoreOptions = (workspaceId: string) =>
806
- storeOptions({
807
- storeId: `workspace-${workspaceId}`,
808
- schema,
809
- adapter,
810
- unusedCacheTime: 60_000, // Keep in memory for 60 seconds after last use
811
- })
812
- ```
813
-
814
- ### `data-modeling/todo-workspaces/multi-store/Workspace.tsx`
815
-
816
- ```tsx filename="data-modeling/todo-workspaces/multi-store/Workspace.tsx"
817
-
818
- // Component that accesses a specific workspace store
819
- function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
820
- // Load the workspace store for this specific workspace
821
- const workspaceStore = useStore(workspaceStoreOptions(workspaceId))
822
-
823
- // Query workspace data
824
- const [workspace] = workspaceStore.useQuery(queryDb(workspaceTables.workspace.select().limit(1)))
825
- const todos = workspaceStore.useQuery(queryDb(workspaceTables.todo.select()))
826
-
827
- if (!workspace) return <div>Workspace not found</div>
828
-
829
- const addTodo = (text: string) => {
830
- workspaceStore.commit(
831
- workspaceEvents.todoAdded({
832
- todoId: `todo-${Date.now()}`,
833
- text,
834
- }),
835
- )
836
- }
837
-
838
- return (
839
- <div>
840
- <h2>{workspace.name}</h2>
841
- <p>Created by: {workspace.createdByUsername}</p>
842
- <p>Store ID: {workspaceStore.storeId}</p>
843
-
844
- <h3>Todos ({todos.length})</h3>
845
- <ul>
846
- {todos.map((todo) => (
847
- <li key={todo.todoId}>
848
- {todo.text} {todo.completed ? '✓' : ''}
849
- </li>
850
- ))}
851
- </ul>
852
-
853
- <button type="button" onClick={() => addTodo('New todo')}>
854
- Add Todo
855
- </button>
856
- </div>
857
- )
858
- }
859
-
860
- // Workspace component with Suspense and ErrorBoundary
861
- export function Workspace({ workspaceId }: { workspaceId: string }) {
862
- return (
863
- <ErrorBoundary fallback={<div>Error loading workspace</div>}>
864
- <Suspense fallback={<div>Loading workspace...</div>}>
865
- <WorkspaceContent workspaceId={workspaceId} />
866
- </Suspense>
867
- </ErrorBoundary>
868
- )
869
- }
870
- ```
871
-
872
- ## Further notes
873
-
874
- To make this app more production-ready, we might want to do the following:
875
- - Use a proper auth setup to enforce a trusted user identity
876
- - Introduce a proper user invite process
877
- - Introduce access levels (e.g. read-only, read-write)
878
- - Introduce end-to-end encryption
879
-
880
- ### Individual todo stores for complex data
881
-
882
- 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.
883
-
884
- This would create **3 store types** instead of 2:
885
- - **User stores** (one per user) - unchanged
886
- - **Workspace stores** (one per workspace) - only basic todo metadata
887
- - **Todo stores** (one per todo item) - rich todo data
888
-
889
- Your app would then have **N + M + K stores** total (N workspaces + M users + K todo items).
890
-
891
- This pattern improves performance by only loading detailed todo data when specifically viewing that item, and prevents large todos from slowing down workspace syncing.