@livestore/cli 0.4.0 → 0.5.0-dev.0
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/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixtures/mock-config.d.ts +7 -9
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-config.js +6 -6
- package/dist/__tests__/fixtures/mock-config.js.map +1 -1
- package/dist/__tests__/sync-operations.test.js +20 -20
- package/dist/__tests__/sync-operations.test.js.map +1 -1
- package/dist/bin.js +6 -5
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +2 -23
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
- package/dist/commands/import-export.d.ts +5 -19
- package/dist/commands/import-export.d.ts.map +1 -1
- package/dist/commands/import-export.js +12 -12
- package/dist/commands/import-export.js.map +1 -1
- package/dist/commands/mcp-coach.d.ts +8 -7
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +10 -10
- package/dist/commands/mcp-coach.js.map +1 -1
- package/dist/commands/mcp-protocols.d.ts +7 -0
- package/dist/commands/mcp-protocols.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.js +12 -0
- package/dist/commands/mcp-protocols.js.map +1 -0
- package/dist/commands/mcp-protocols.test.d.ts +2 -0
- package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.test.js +60 -0
- package/dist/commands/mcp-protocols.test.js.map +1 -0
- package/dist/commands/mcp-stdio.test.d.ts +2 -0
- package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
- package/dist/commands/mcp-stdio.test.js +79 -0
- package/dist/commands/mcp-stdio.test.js.map +1 -0
- package/dist/commands/mcp-tool-handlers.d.ts +1 -1
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
- package/dist/commands/mcp-tool-handlers.js +1 -0
- package/dist/commands/mcp-tool-handlers.js.map +1 -1
- package/dist/commands/mcp-tools-defs.d.ts +65 -64
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
- package/dist/commands/mcp-tools-defs.js +59 -52
- package/dist/commands/mcp-tools-defs.js.map +1 -1
- package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
- package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.test.js +11 -0
- package/dist/commands/mcp-tools-defs.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +1 -3
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +19 -27
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +17 -16
- package/dist/commands/new-project.js.map +1 -1
- package/dist/mcp-content/architecture.d.ts +1 -1
- package/dist/mcp-content/architecture.d.ts.map +1 -1
- package/dist/mcp-content/architecture.js +3 -3
- package/dist/mcp-content/features.d.ts +1 -1
- package/dist/mcp-content/features.d.ts.map +1 -1
- package/dist/mcp-content/features.js +1 -1
- package/dist/mcp-content/getting-started.d.ts +1 -1
- package/dist/mcp-content/getting-started.d.ts.map +1 -1
- package/dist/mcp-content/getting-started.js +37 -37
- package/dist/mcp-content/overview.d.ts +1 -1
- package/dist/mcp-content/overview.d.ts.map +1 -1
- package/dist/mcp-content/overview.js +6 -6
- package/dist/mcp-content/schemas/blog.d.ts +1 -1
- package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
- package/dist/mcp-content/schemas/blog.js +25 -25
- package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
- package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
- package/dist/mcp-content/schemas/ecommerce.js +67 -67
- package/dist/mcp-content/schemas/social.d.ts +1 -1
- package/dist/mcp-content/schemas/social.d.ts.map +1 -1
- package/dist/mcp-content/schemas/social.js +48 -48
- package/dist/mcp-content/schemas/todo.d.ts +1 -1
- package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
- package/dist/mcp-content/schemas/todo.js +35 -35
- package/dist/mcp-runtime/runtime.d.ts +2 -2
- package/dist/mcp-runtime/runtime.d.ts.map +1 -1
- package/dist/mcp-runtime/runtime.js +4 -4
- package/dist/mcp-runtime/runtime.js.map +1 -1
- package/dist/module-loader.d.ts +2 -2
- package/dist/module-loader.d.ts.map +1 -1
- package/dist/module-loader.js +6 -6
- package/dist/module-loader.js.map +1 -1
- package/dist/sync-operations.d.ts +24 -30
- package/dist/sync-operations.d.ts.map +1 -1
- package/dist/sync-operations.js +10 -12
- package/dist/sync-operations.js.map +1 -1
- package/package.json +30 -44
- package/src/__tests__/fixtures/mock-config.ts +8 -8
- package/src/__tests__/sync-operations.test.ts +21 -35
- package/src/bin.ts +7 -6
- package/src/cli.ts +1 -1
- package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
- package/src/commands/import-export.ts +29 -29
- package/src/commands/mcp-coach.ts +12 -11
- package/src/commands/mcp-protocols.test.ts +76 -0
- package/src/commands/mcp-protocols.ts +12 -0
- package/src/commands/mcp-stdio.test.ts +93 -0
- package/src/commands/mcp-tool-handlers.ts +11 -8
- package/src/commands/mcp-tools-defs.test.ts +13 -0
- package/src/commands/mcp-tools-defs.ts +53 -51
- package/src/commands/mcp.ts +7 -4
- package/src/commands/new-project.ts +49 -36
- package/src/mcp-content/architecture.ts +3 -3
- package/src/mcp-content/features.ts +1 -1
- package/src/mcp-content/getting-started.ts +37 -37
- package/src/mcp-content/overview.ts +6 -6
- package/src/mcp-content/schemas/blog.ts +25 -25
- package/src/mcp-content/schemas/ecommerce.ts +67 -67
- package/src/mcp-content/schemas/social.ts +48 -48
- package/src/mcp-content/schemas/todo.ts +35 -35
- package/src/mcp-runtime/runtime.ts +7 -7
- package/src/module-loader.ts +10 -10
- package/src/sync-operations.ts +16 -16
|
@@ -3,7 +3,8 @@ import * as nodePath from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import { sluggify } from '@livestore/utils'
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
ChildProcess,
|
|
7
|
+
ChildProcessSpawner,
|
|
7
8
|
Console,
|
|
8
9
|
Effect,
|
|
9
10
|
FileSystem,
|
|
@@ -19,7 +20,7 @@ import { detectPackageManager, pmCommands } from '../package-manager.ts'
|
|
|
19
20
|
// Schema for GitHub API response
|
|
20
21
|
const GitHubContentSchema = Schema.Struct({
|
|
21
22
|
name: Schema.String,
|
|
22
|
-
type: Schema.
|
|
23
|
+
type: Schema.Literals(['dir', 'file']),
|
|
23
24
|
path: Schema.String,
|
|
24
25
|
download_url: Schema.NullOr(Schema.String),
|
|
25
26
|
})
|
|
@@ -34,11 +35,13 @@ const githubRequest = (url: string) => {
|
|
|
34
35
|
|
|
35
36
|
/** Schema for parsing package.json scripts (dev or start) */
|
|
36
37
|
const PackageJsonScriptsSchema = Schema.Struct({
|
|
37
|
-
scripts: Schema.Union(Schema.Struct({ dev: Schema.String }), Schema.Struct({ start: Schema.String })),
|
|
38
|
+
scripts: Schema.Union([Schema.Struct({ dev: Schema.String }), Schema.Struct({ start: Schema.String })]),
|
|
38
39
|
})
|
|
39
40
|
|
|
40
41
|
// Error types
|
|
41
|
-
export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>(
|
|
42
|
+
export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>(
|
|
43
|
+
'~@livestore/cli/ExampleNotFoundError',
|
|
44
|
+
)('ExampleNotFoundError', {
|
|
42
45
|
exampleName: Schema.String,
|
|
43
46
|
availableExamples: Schema.Array(Schema.String),
|
|
44
47
|
message: Schema.String,
|
|
@@ -49,14 +52,19 @@ export class NetworkError extends Schema.TaggedError<NetworkError>('~@livestore/
|
|
|
49
52
|
message: Schema.String,
|
|
50
53
|
}) {}
|
|
51
54
|
|
|
52
|
-
export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>(
|
|
55
|
+
export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>(
|
|
56
|
+
'~@livestore/cli/DirectoryExistsError',
|
|
57
|
+
)('DirectoryExistsError', {
|
|
53
58
|
path: Schema.String,
|
|
54
59
|
message: Schema.String,
|
|
55
60
|
}) {}
|
|
56
61
|
|
|
57
|
-
export class NoExamplesError extends Schema.TaggedError<NoExamplesError>('~@livestore/cli/NoExamplesError')(
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
export class NoExamplesError extends Schema.TaggedError<NoExamplesError>('~@livestore/cli/NoExamplesError')(
|
|
63
|
+
'NoExamplesError',
|
|
64
|
+
{
|
|
65
|
+
message: Schema.String,
|
|
66
|
+
},
|
|
67
|
+
) {}
|
|
60
68
|
|
|
61
69
|
// Fetch available examples from GitHub
|
|
62
70
|
const fetchExamples = (ref: string) =>
|
|
@@ -68,7 +76,7 @@ const fetchExamples = (ref: string) =>
|
|
|
68
76
|
const request = githubRequest(url)
|
|
69
77
|
const response = yield* HttpClient.execute(request).pipe(
|
|
70
78
|
Effect.scoped,
|
|
71
|
-
Effect.
|
|
79
|
+
Effect.catch(
|
|
72
80
|
(error) =>
|
|
73
81
|
new NetworkError({
|
|
74
82
|
cause: error,
|
|
@@ -79,8 +87,10 @@ const fetchExamples = (ref: string) =>
|
|
|
79
87
|
|
|
80
88
|
const responseText = yield* response.text
|
|
81
89
|
|
|
82
|
-
const examples = yield* Schema.
|
|
83
|
-
|
|
90
|
+
const examples = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GitHubContentsResponseSchema))(
|
|
91
|
+
responseText,
|
|
92
|
+
).pipe(
|
|
93
|
+
Effect.catch(
|
|
84
94
|
(error) =>
|
|
85
95
|
new NetworkError({
|
|
86
96
|
cause: error,
|
|
@@ -123,6 +133,8 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
123
133
|
Effect.gen(function* () {
|
|
124
134
|
yield* Console.log(`📥 Downloading example "${exampleName}" from ref "${ref}"...`)
|
|
125
135
|
|
|
136
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
137
|
+
|
|
126
138
|
const tempDir = yield* Effect.sync(() => os.tmpdir())
|
|
127
139
|
const tarballPath = nodePath.join(tempDir, `livestore-${sluggify(ref)}-${Date.now()}.tar.gz`)
|
|
128
140
|
const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${ref}`
|
|
@@ -132,7 +144,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
132
144
|
|
|
133
145
|
const response = yield* HttpClient.execute(request).pipe(
|
|
134
146
|
Effect.scoped,
|
|
135
|
-
Effect.
|
|
147
|
+
Effect.catch(
|
|
136
148
|
(error) =>
|
|
137
149
|
new NetworkError({
|
|
138
150
|
cause: error,
|
|
@@ -154,10 +166,9 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
154
166
|
const extractDir = nodePath.join(tempDir, `extract-${Date.now()}`)
|
|
155
167
|
yield* fs.makeDirectory(extractDir, { recursive: true })
|
|
156
168
|
|
|
157
|
-
// Extract tarball using
|
|
158
|
-
yield*
|
|
159
|
-
|
|
160
|
-
Effect.catchAll(
|
|
169
|
+
// Extract tarball using a child process
|
|
170
|
+
yield* spawner.exitCode(ChildProcess.make('tar', ['-xzf', tarballPath, '-C', extractDir])).pipe(
|
|
171
|
+
Effect.catch(
|
|
161
172
|
(error) =>
|
|
162
173
|
new NetworkError({
|
|
163
174
|
cause: error,
|
|
@@ -190,10 +201,9 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
190
201
|
})
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
// Copy the example directory contents to the destination using
|
|
194
|
-
yield*
|
|
195
|
-
|
|
196
|
-
Effect.catchAll(
|
|
204
|
+
// Copy the example directory contents to the destination using a child process
|
|
205
|
+
yield* spawner.exitCode(ChildProcess.make('cp', ['-r', `${exampleSourcePath}/.`, destinationPath])).pipe(
|
|
206
|
+
Effect.catch(
|
|
197
207
|
(error) =>
|
|
198
208
|
new NetworkError({
|
|
199
209
|
cause: error,
|
|
@@ -203,10 +213,10 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
203
213
|
)
|
|
204
214
|
|
|
205
215
|
// Clean up extract directory
|
|
206
|
-
yield* fs.remove(extractDir, { recursive: true }).pipe(Effect.
|
|
216
|
+
yield* fs.remove(extractDir, { recursive: true }).pipe(Effect.catch(() => Effect.void))
|
|
207
217
|
|
|
208
218
|
// Clean up tarball
|
|
209
|
-
yield* fs.remove(tarballPath).pipe(Effect.
|
|
219
|
+
yield* fs.remove(tarballPath).pipe(Effect.catch(() => Effect.void))
|
|
210
220
|
|
|
211
221
|
yield* Console.log(`✅ Example "${exampleName}" created successfully at: ${destinationPath}`)
|
|
212
222
|
})
|
|
@@ -214,22 +224,22 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
214
224
|
export const createCommand = Cli.Command.make(
|
|
215
225
|
'create',
|
|
216
226
|
{
|
|
217
|
-
example: Cli.
|
|
218
|
-
Cli.
|
|
219
|
-
Cli.
|
|
227
|
+
example: Cli.Flag.string('example').pipe(
|
|
228
|
+
Cli.Flag.optional,
|
|
229
|
+
Cli.Flag.withDescription('Example name to create (bypasses interactive selection)'),
|
|
220
230
|
),
|
|
221
|
-
ref: Cli.
|
|
222
|
-
Cli.
|
|
223
|
-
Cli.
|
|
224
|
-
Cli.
|
|
225
|
-
Cli.
|
|
226
|
-
Cli.
|
|
231
|
+
ref: Cli.Flag.string('ref').pipe(
|
|
232
|
+
Cli.Flag.withAlias('commit'),
|
|
233
|
+
Cli.Flag.withAlias('branch'),
|
|
234
|
+
Cli.Flag.withAlias('tag'),
|
|
235
|
+
Cli.Flag.withDefault('main'),
|
|
236
|
+
Cli.Flag.withDescription(
|
|
227
237
|
'The name of the commit/branch/tag to fetch examples from. Pull requests refs must be fully-formed (e.g., `refs/pull/123/merge`).',
|
|
228
238
|
),
|
|
229
239
|
),
|
|
230
|
-
path: Cli.
|
|
231
|
-
Cli.
|
|
232
|
-
Cli.
|
|
240
|
+
path: Cli.Argument.string('path').pipe(
|
|
241
|
+
Cli.Argument.optional,
|
|
242
|
+
Cli.Argument.withDescription('Destination path for the new project'),
|
|
233
243
|
),
|
|
234
244
|
},
|
|
235
245
|
Effect.fn(function* ({
|
|
@@ -270,7 +280,8 @@ export const createCommand = Cli.Command.make(
|
|
|
270
280
|
}
|
|
271
281
|
|
|
272
282
|
// Determine destination path
|
|
273
|
-
const destinationPath =
|
|
283
|
+
const destinationPath =
|
|
284
|
+
Option.isSome(path) === true ? nodePath.resolve(path.value) : nodePath.resolve(selectedExample)
|
|
274
285
|
|
|
275
286
|
// Download and extract the example
|
|
276
287
|
yield* downloadExample(selectedExample, ref, destinationPath)
|
|
@@ -281,7 +292,9 @@ export const createCommand = Cli.Command.make(
|
|
|
281
292
|
const fs = yield* FileSystem.FileSystem
|
|
282
293
|
const packageJsonPath = nodePath.join(destinationPath, 'package.json')
|
|
283
294
|
const packageJsonContent = yield* fs.readFileString(packageJsonPath)
|
|
284
|
-
const runScript = yield* Schema.
|
|
295
|
+
const runScript = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PackageJsonScriptsSchema))(
|
|
296
|
+
packageJsonContent,
|
|
297
|
+
).pipe(
|
|
285
298
|
Effect.map((pkg) => ('dev' in pkg.scripts ? ('dev' as const) : ('start' as const))),
|
|
286
299
|
Effect.orElseSucceed(() => undefined),
|
|
287
300
|
)
|
|
@@ -28,9 +28,9 @@ LiveStore implements distributed systems principles from Martin Kleppmann's rese
|
|
|
28
28
|
\`\`\`typescript
|
|
29
29
|
// Materialized views from event log
|
|
30
30
|
const materializers = State.SQLite.materializers(events, {
|
|
31
|
-
'TodoCreated': ({ id, text, createdAt }) =>
|
|
31
|
+
'TodoCreated': ({ id, text, createdAt }) =>
|
|
32
32
|
tables.todos.insert({ id, text, completed: false, createdAt }),
|
|
33
|
-
'TodoCompleted': ({ id }) =>
|
|
33
|
+
'TodoCompleted': ({ id }) =>
|
|
34
34
|
tables.todos.update({ completed: true }).where({ id })
|
|
35
35
|
})
|
|
36
36
|
\`\`\`
|
|
@@ -46,7 +46,7 @@ const materializers = State.SQLite.materializers(events, {
|
|
|
46
46
|
const events = {
|
|
47
47
|
todoCompleted: Events.synced({
|
|
48
48
|
name: 'v1.TodoCompleted',
|
|
49
|
-
schema: Schema.Struct({
|
|
49
|
+
schema: Schema.Struct({
|
|
50
50
|
id: Schema.String,
|
|
51
51
|
completedAt: Schema.Date // For conflict resolution
|
|
52
52
|
})
|
|
@@ -149,7 +149,7 @@ await testStore.replay([
|
|
|
149
149
|
|
|
150
150
|
### Developer Tools
|
|
151
151
|
- **Event Inspector**: Real-time event log visualization
|
|
152
|
-
- **Query Profiler**: Performance analysis for slow queries
|
|
152
|
+
- **Query Profiler**: Performance analysis for slow queries
|
|
153
153
|
- **Sync Monitor**: Network synchronization health dashboard
|
|
154
154
|
- **Schema Explorer**: Interactive database schema browsing
|
|
155
155
|
|
|
@@ -36,13 +36,13 @@ export const tables = {
|
|
|
36
36
|
createdAt: State.SQLite.integer({ schema: Schema.DateFromNumber }),
|
|
37
37
|
},
|
|
38
38
|
}),
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
// Client-only state (not synced)
|
|
41
41
|
uiState: State.SQLite.clientDocument({
|
|
42
42
|
name: 'uiState',
|
|
43
|
-
schema: Schema.Struct({
|
|
44
|
-
newTodoText: Schema.String,
|
|
45
|
-
filter: Schema.Literal('all', 'active', 'completed')
|
|
43
|
+
schema: Schema.Struct({
|
|
44
|
+
newTodoText: Schema.String,
|
|
45
|
+
filter: Schema.Literal('all', 'active', 'completed')
|
|
46
46
|
}),
|
|
47
47
|
default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } },
|
|
48
48
|
}),
|
|
@@ -52,10 +52,10 @@ export const tables = {
|
|
|
52
52
|
export const events = {
|
|
53
53
|
todoCreated: Events.synced({
|
|
54
54
|
name: 'v1.TodoCreated',
|
|
55
|
-
schema: Schema.Struct({
|
|
56
|
-
id: Schema.String,
|
|
55
|
+
schema: Schema.Struct({
|
|
56
|
+
id: Schema.String,
|
|
57
57
|
text: Schema.String,
|
|
58
|
-
createdAt: Schema.Date
|
|
58
|
+
createdAt: Schema.Date
|
|
59
59
|
}),
|
|
60
60
|
}),
|
|
61
61
|
todoCompleted: Events.synced({
|
|
@@ -70,23 +70,23 @@ export const events = {
|
|
|
70
70
|
name: 'v1.TodoDeleted',
|
|
71
71
|
schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
|
|
72
72
|
}),
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
// UI state events (local only)
|
|
75
75
|
uiStateSet: tables.uiState.set,
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Materializers map events to state changes
|
|
79
79
|
const materializers = State.SQLite.materializers(events, {
|
|
80
|
-
'v1.TodoCreated': ({ id, text, createdAt }) =>
|
|
80
|
+
'v1.TodoCreated': ({ id, text, createdAt }) =>
|
|
81
81
|
tables.todos.insert({ id, text, completed: false, createdAt }),
|
|
82
|
-
|
|
83
|
-
'v1.TodoCompleted': ({ id }) =>
|
|
82
|
+
|
|
83
|
+
'v1.TodoCompleted': ({ id }) =>
|
|
84
84
|
tables.todos.update({ completed: true }).where({ id }),
|
|
85
|
-
|
|
86
|
-
'v1.TodoUncompleted': ({ id }) =>
|
|
85
|
+
|
|
86
|
+
'v1.TodoUncompleted': ({ id }) =>
|
|
87
87
|
tables.todos.update({ completed: false }).where({ id }),
|
|
88
|
-
|
|
89
|
-
'v1.TodoDeleted': ({ id, deletedAt }) =>
|
|
88
|
+
|
|
89
|
+
'v1.TodoDeleted': ({ id, deletedAt }) =>
|
|
90
90
|
tables.todos.update({ deletedAt }).where({ id }),
|
|
91
91
|
})
|
|
92
92
|
|
|
@@ -144,28 +144,28 @@ const TodoApp: React.FC = () => {
|
|
|
144
144
|
const activeTodos = useLiveQuery(activeTodos$)
|
|
145
145
|
const completedTodos = useLiveQuery(completedTodos$)
|
|
146
146
|
const uiState = useLiveQuery(uiState$)
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
const createTodo = (text: string) => {
|
|
149
149
|
const id = crypto.randomUUID()
|
|
150
150
|
dispatchEvent(events.todoCreated({ id, text, createdAt: new Date() }))
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
const toggleTodo = (id: string, completed: boolean) => {
|
|
154
154
|
dispatchEvent(
|
|
155
|
-
completed
|
|
155
|
+
completed
|
|
156
156
|
? events.todoCompleted({ id })
|
|
157
157
|
: events.todoUncompleted({ id })
|
|
158
158
|
)
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
const deleteTodo = (id: string) => {
|
|
162
162
|
dispatchEvent(events.todoDeleted({ id, deletedAt: new Date() }))
|
|
163
163
|
}
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
return (
|
|
166
166
|
<div className="todo-app">
|
|
167
167
|
<h1>Local-First Todos</h1>
|
|
168
|
-
|
|
168
|
+
|
|
169
169
|
{/* Add todo form */}
|
|
170
170
|
<form onSubmit={(e) => {
|
|
171
171
|
e.preventDefault()
|
|
@@ -179,13 +179,13 @@ const TodoApp: React.FC = () => {
|
|
|
179
179
|
<input name="todo" placeholder="What needs to be done?" />
|
|
180
180
|
<button type="submit">Add Todo</button>
|
|
181
181
|
</form>
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
{/* Active todos */}
|
|
184
184
|
<div className="todo-list">
|
|
185
185
|
<h2>Active ({activeTodos.length})</h2>
|
|
186
186
|
{activeTodos.map(todo => (
|
|
187
187
|
<div key={todo.id} className="todo-item">
|
|
188
|
-
<input
|
|
188
|
+
<input
|
|
189
189
|
type="checkbox"
|
|
190
190
|
checked={false}
|
|
191
191
|
onChange={() => toggleTodo(todo.id, true)}
|
|
@@ -195,13 +195,13 @@ const TodoApp: React.FC = () => {
|
|
|
195
195
|
</div>
|
|
196
196
|
))}
|
|
197
197
|
</div>
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
{/* Completed todos */}
|
|
200
200
|
<div className="todo-list">
|
|
201
201
|
<h2>Completed ({completedTodos.length})</h2>
|
|
202
202
|
{completedTodos.map(todo => (
|
|
203
203
|
<div key={todo.id} className="todo-item completed">
|
|
204
|
-
<input
|
|
204
|
+
<input
|
|
205
205
|
type="checkbox"
|
|
206
206
|
checked={true}
|
|
207
207
|
onChange={() => toggleTodo(todo.id, false)}
|
|
@@ -225,16 +225,16 @@ export default TodoApp
|
|
|
225
225
|
<template>
|
|
226
226
|
<div class="todo-app">
|
|
227
227
|
<h1>Local-First Todos</h1>
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
<form @submit.prevent="createTodo">
|
|
230
230
|
<input v-model="newTodoText" placeholder="What needs to be done?" />
|
|
231
231
|
<button type="submit">Add Todo</button>
|
|
232
232
|
</form>
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
<div class="todo-list">
|
|
235
235
|
<h2>Active ({{ activeTodos.length }})</h2>
|
|
236
236
|
<div v-for="todo in activeTodos" :key="todo.id" class="todo-item">
|
|
237
|
-
<input
|
|
237
|
+
<input
|
|
238
238
|
type="checkbox"
|
|
239
239
|
:checked="false"
|
|
240
240
|
@change="toggleTodo(todo.id, true)"
|
|
@@ -258,10 +258,10 @@ const newTodoText = ref('')
|
|
|
258
258
|
const createTodo = () => {
|
|
259
259
|
if (newTodoText.value.trim()) {
|
|
260
260
|
const id = crypto.randomUUID()
|
|
261
|
-
dispatchEvent(events.todoCreated({
|
|
262
|
-
id,
|
|
263
|
-
text: newTodoText.value.trim(),
|
|
264
|
-
createdAt: new Date()
|
|
261
|
+
dispatchEvent(events.todoCreated({
|
|
262
|
+
id,
|
|
263
|
+
text: newTodoText.value.trim(),
|
|
264
|
+
createdAt: new Date()
|
|
265
265
|
}))
|
|
266
266
|
newTodoText.value = ''
|
|
267
267
|
}
|
|
@@ -269,7 +269,7 @@ const createTodo = () => {
|
|
|
269
269
|
|
|
270
270
|
const toggleTodo = (id: string, completed: boolean) => {
|
|
271
271
|
dispatchEvent(
|
|
272
|
-
completed
|
|
272
|
+
completed
|
|
273
273
|
? events.todoCompleted({ id })
|
|
274
274
|
: events.todoUncompleted({ id })
|
|
275
275
|
)
|
|
@@ -332,7 +332,7 @@ Multiple users can collaborate in real-time. Conflicts are automatically resolve
|
|
|
332
332
|
\`\`\`typescript
|
|
333
333
|
// Custom conflict resolution
|
|
334
334
|
const materializers = State.SQLite.materializers(events, {
|
|
335
|
-
'v1.TodoTextChanged': ({ id, text, editedAt }) =>
|
|
335
|
+
'v1.TodoTextChanged': ({ id, text, editedAt }) =>
|
|
336
336
|
// Use timestamp for conflict resolution
|
|
337
337
|
tables.todos
|
|
338
338
|
.update({ text, editedAt })
|
|
@@ -350,18 +350,18 @@ import { schema, events } from './schema.js'
|
|
|
350
350
|
|
|
351
351
|
test('creating and completing todos', async () => {
|
|
352
352
|
const store = createTestStore(schema)
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
// Dispatch events
|
|
355
355
|
await store.dispatch([
|
|
356
356
|
events.todoCreated({ id: '1', text: 'Test todo', createdAt: new Date() }),
|
|
357
357
|
events.todoCompleted({ id: '1' })
|
|
358
358
|
])
|
|
359
|
-
|
|
359
|
+
|
|
360
360
|
// Query final state
|
|
361
361
|
const completedTodos = await store.query(
|
|
362
362
|
tables.todos.select().where({ completed: true })
|
|
363
363
|
)
|
|
364
|
-
|
|
364
|
+
|
|
365
365
|
expect(completedTodos).toHaveLength(1)
|
|
366
366
|
expect(completedTodos[0].text).toBe('Test todo')
|
|
367
367
|
})
|
|
@@ -26,7 +26,7 @@ LiveStore is a production-ready local-first data platform that combines the imme
|
|
|
26
26
|
|
|
27
27
|
### 🔐 End-to-End Type Safety
|
|
28
28
|
- **Schema-first development** with Effect-based validation
|
|
29
|
-
- **Compile-time query validation** prevents runtime errors
|
|
29
|
+
- **Compile-time query validation** prevents runtime errors
|
|
30
30
|
- **Automatic TypeScript generation** from schema definitions
|
|
31
31
|
- **Runtime safety** with comprehensive input validation
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ LiveStore is a production-ready local-first data platform that combines the imme
|
|
|
43
43
|
- **Design tools** with multiplayer editing capabilities
|
|
44
44
|
- **Chat applications** with offline message queuing
|
|
45
45
|
|
|
46
|
-
### 📱 Mobile-First Applications
|
|
46
|
+
### 📱 Mobile-First Applications
|
|
47
47
|
- **Field service apps** that work in areas with poor connectivity
|
|
48
48
|
- **Healthcare applications** with sensitive data that must stay local
|
|
49
49
|
- **Educational apps** for students in low-connectivity environments
|
|
@@ -61,16 +61,16 @@ LiveStore is a production-ready local-first data platform that combines the imme
|
|
|
61
61
|
graph TB
|
|
62
62
|
UI[UI Framework] --> Queries[Reactive Queries]
|
|
63
63
|
UI --> Events[Event Dispatch]
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
Events --> EventLog[Event Log]
|
|
66
66
|
EventLog --> Materializers[Materializers]
|
|
67
67
|
Materializers --> SQLite[SQLite Database]
|
|
68
68
|
Queries --> SQLite
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
EventLog --> Sync[Sync Engine]
|
|
71
71
|
Sync --> Network[Network Layer]
|
|
72
72
|
Network --> Server[Sync Server]
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
Server --> OtherClients[Other Clients]
|
|
75
75
|
OtherClients --> Server
|
|
76
76
|
Server --> Network
|
|
@@ -95,7 +95,7 @@ Unlike traditional ORMs that hide change tracking, LiveStore makes all state cha
|
|
|
95
95
|
### Sophisticated Conflict Resolution
|
|
96
96
|
LiveStore handles conflicts intelligently using multiple strategies:
|
|
97
97
|
- **Last-Write-Wins**: Simple timestamp-based resolution
|
|
98
|
-
- **Semantic Merging**: Application-specific conflict resolution logic
|
|
98
|
+
- **Semantic Merging**: Application-specific conflict resolution logic
|
|
99
99
|
- **Operational Transforms**: For real-time collaborative text editing
|
|
100
100
|
- **CRDT Integration**: Conflict-free data types for specific use cases
|
|
101
101
|
|
|
@@ -17,7 +17,7 @@ export const tables = {
|
|
|
17
17
|
version: State.SQLite.integer({ default: 1 }), // For optimistic concurrency
|
|
18
18
|
},
|
|
19
19
|
}),
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
comments: State.SQLite.table({
|
|
22
22
|
name: 'comments',
|
|
23
23
|
columns: {
|
|
@@ -31,7 +31,7 @@ export const tables = {
|
|
|
31
31
|
deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
|
|
32
32
|
},
|
|
33
33
|
}),
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
authors: State.SQLite.table({
|
|
36
36
|
name: 'authors',
|
|
37
37
|
columns: {
|
|
@@ -43,7 +43,7 @@ export const tables = {
|
|
|
43
43
|
createdAt: State.SQLite.integer({ schema: Schema.DateFromNumber }),
|
|
44
44
|
},
|
|
45
45
|
}),
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
// Track collaborative editing sessions
|
|
48
48
|
editingSessions: State.SQLite.clientDocument({
|
|
49
49
|
name: 'editingSessions',
|
|
@@ -53,8 +53,8 @@ export const tables = {
|
|
|
53
53
|
lastActivity: Schema.Date,
|
|
54
54
|
cursorPosition: Schema.Number,
|
|
55
55
|
}),
|
|
56
|
-
default: {
|
|
57
|
-
id: SessionIdSymbol,
|
|
56
|
+
default: {
|
|
57
|
+
id: SessionIdSymbol,
|
|
58
58
|
value: { postId: '', authorId: '', lastActivity: new Date(), cursorPosition: 0 }
|
|
59
59
|
},
|
|
60
60
|
}),
|
|
@@ -72,7 +72,7 @@ export const events = {
|
|
|
72
72
|
createdAt: Schema.Date,
|
|
73
73
|
}),
|
|
74
74
|
}),
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
postTitleChanged: Events.synced({
|
|
77
77
|
name: 'v1.PostTitleChanged',
|
|
78
78
|
schema: Schema.Struct({
|
|
@@ -81,7 +81,7 @@ export const events = {
|
|
|
81
81
|
version: Schema.Number, // Optimistic concurrency control
|
|
82
82
|
}),
|
|
83
83
|
}),
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
postContentChanged: Events.synced({
|
|
86
86
|
name: 'v1.PostContentChanged',
|
|
87
87
|
schema: Schema.Struct({
|
|
@@ -91,7 +91,7 @@ export const events = {
|
|
|
91
91
|
authorId: Schema.String,
|
|
92
92
|
}),
|
|
93
93
|
}),
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
postPublished: Events.synced({
|
|
96
96
|
name: 'v1.PostPublished',
|
|
97
97
|
schema: Schema.Struct({
|
|
@@ -99,12 +99,12 @@ export const events = {
|
|
|
99
99
|
publishedAt: Schema.Date,
|
|
100
100
|
}),
|
|
101
101
|
}),
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
postUnpublished: Events.synced({
|
|
104
104
|
name: 'v1.PostUnpublished',
|
|
105
105
|
schema: Schema.Struct({ id: Schema.String }),
|
|
106
106
|
}),
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
postDeleted: Events.synced({
|
|
109
109
|
name: 'v1.PostDeleted',
|
|
110
110
|
schema: Schema.Struct({
|
|
@@ -112,7 +112,7 @@ export const events = {
|
|
|
112
112
|
deletedAt: Schema.Date,
|
|
113
113
|
}),
|
|
114
114
|
}),
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
// Comment events
|
|
117
117
|
commentCreated: Events.synced({
|
|
118
118
|
name: 'v1.CommentCreated',
|
|
@@ -125,7 +125,7 @@ export const events = {
|
|
|
125
125
|
createdAt: Schema.Date,
|
|
126
126
|
}),
|
|
127
127
|
}),
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
commentEdited: Events.synced({
|
|
130
130
|
name: 'v1.CommentEdited',
|
|
131
131
|
schema: Schema.Struct({
|
|
@@ -134,7 +134,7 @@ export const events = {
|
|
|
134
134
|
editedAt: Schema.Date,
|
|
135
135
|
}),
|
|
136
136
|
}),
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
commentDeleted: Events.synced({
|
|
139
139
|
name: 'v1.CommentDeleted',
|
|
140
140
|
schema: Schema.Struct({
|
|
@@ -142,7 +142,7 @@ export const events = {
|
|
|
142
142
|
deletedAt: Schema.Date,
|
|
143
143
|
}),
|
|
144
144
|
}),
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
// Author events
|
|
147
147
|
authorCreated: Events.synced({
|
|
148
148
|
name: 'v1.AuthorCreated',
|
|
@@ -153,7 +153,7 @@ export const events = {
|
|
|
153
153
|
createdAt: Schema.Date,
|
|
154
154
|
}),
|
|
155
155
|
}),
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
// Local editing session tracking
|
|
158
158
|
editingSessionUpdated: tables.editingSessions.set,
|
|
159
159
|
}
|
|
@@ -163,33 +163,33 @@ const materializers = State.SQLite.materializers(events, {
|
|
|
163
163
|
// Post materializers
|
|
164
164
|
'v1.PostCreated': ({ id, title, slug, authorId, createdAt }) =>
|
|
165
165
|
tables.posts.insert({ id, title, content: '', slug, authorId, createdAt, version: 1 }),
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
'v1.PostTitleChanged': ({ id, title, version }) =>
|
|
168
168
|
// Last-write-wins with version check for optimistic concurrency
|
|
169
169
|
tables.posts.update({ title, version }).where({ id }),
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
'v1.PostContentChanged': ({ id, content, version, authorId }) =>
|
|
172
172
|
tables.posts.update({ content, version }).where({ id }),
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
'v1.PostPublished': ({ id, publishedAt }) =>
|
|
175
175
|
tables.posts.update({ published: true, publishedAt }).where({ id }),
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
'v1.PostUnpublished': ({ id }) =>
|
|
178
178
|
tables.posts.update({ published: false, publishedAt: null }).where({ id }),
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
'v1.PostDeleted': ({ id, deletedAt }) =>
|
|
181
181
|
tables.posts.update({ deletedAt }).where({ id }),
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
// Comment materializers
|
|
184
184
|
'v1.CommentCreated': ({ id, postId, authorId, content, parentId, createdAt }) =>
|
|
185
185
|
tables.comments.insert({ id, postId, authorId, content, parentId, createdAt }),
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
'v1.CommentEdited': ({ id, content, editedAt }) =>
|
|
188
188
|
tables.comments.update({ content, editedAt }).where({ id }),
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
'v1.CommentDeleted': ({ id, deletedAt }) =>
|
|
191
191
|
tables.comments.update({ deletedAt }).where({ id }),
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
// Author materializers
|
|
194
194
|
'v1.AuthorCreated': ({ id, name, email, createdAt }) =>
|
|
195
195
|
tables.authors.insert({ id, name, email, createdAt }),
|
|
@@ -215,7 +215,7 @@ export const schema = makeSchema({ events, state })
|
|
|
215
215
|
// const postComments$ = (postId: string) => queryDb(
|
|
216
216
|
// tables.comments
|
|
217
217
|
// .select()
|
|
218
|
-
// .join(tables.authors, 'authorId', 'id')
|
|
218
|
+
// .join(tables.authors, 'authorId', 'id')
|
|
219
219
|
// .where({ postId, deletedAt: null })
|
|
220
220
|
// .orderBy('createdAt'),
|
|
221
221
|
// { label: \`postComments-\${postId}\` }
|