@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +130 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +185 -6
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +2 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +381 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +56 -6
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +32 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +16 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +59 -9
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +102 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +148 -47
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +59 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +381 -8
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +2 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +540 -0
- package/src/store/StoreRegistry.ts +418 -0
- package/src/store/create-store.ts +76 -15
- package/src/store/devtools.ts +20 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +64 -13
- package/src/store/store.ts +197 -60
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
- package/docs/building-with-livestore/crud/index.md +0 -5
- package/docs/building-with-livestore/data-modeling/index.md +0 -1
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -208
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -281
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -918
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -736
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -573
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -209
- package/docs/llms.txt +0 -147
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -5
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -5
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -226
- package/docs/patterns/effect/index.md +0 -1495
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -192
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -245
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -218
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
|
@@ -1,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.
|