@livestore/livestore 0.0.12 → 0.0.14
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 +25 -28
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +71 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/__tests__/react/fixture.d.ts +26 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -0
- package/dist/__tests__/react/fixture.js +60 -0
- package/dist/__tests__/react/fixture.js.map +1 -0
- package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
- package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
- package/dist/__tests__/react/useComponentState.test.js +68 -0
- package/dist/__tests__/react/useComponentState.test.js.map +1 -0
- package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLQuery.test.js +38 -0
- package/dist/__tests__/react/useLQuery.test.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +73 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
- package/dist/__tests__/react/useQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useQuery.test.js +33 -0
- package/dist/__tests__/react/useQuery.test.js.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
- package/dist/__tests__/reactive.test.d.ts +2 -0
- package/dist/__tests__/reactive.test.d.ts.map +1 -0
- package/dist/__tests__/reactive.test.js +271 -0
- package/dist/__tests__/reactive.test.js.map +1 -0
- package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
- package/dist/bounded-collections.d.ts +34 -0
- package/dist/bounded-collections.d.ts.map +1 -0
- package/dist/bounded-collections.js +103 -0
- package/dist/bounded-collections.js.map +1 -0
- package/dist/componentKey.d.ts +20 -0
- package/dist/componentKey.d.ts.map +1 -0
- package/dist/componentKey.js +3 -0
- package/dist/componentKey.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +36 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +41 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +56 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +223 -0
- package/dist/inMemoryDatabase.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +16 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +67 -0
- package/dist/migrations.js.map +1 -0
- package/dist/otel.d.ts +4 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +6 -0
- package/dist/otel.js.map +1 -0
- package/dist/react/LiveStoreContext.d.ts +11 -0
- package/dist/react/LiveStoreContext.d.ts.map +1 -0
- package/dist/react/LiveStoreContext.js +10 -0
- package/dist/react/LiveStoreContext.js.map +1 -0
- package/dist/react/LiveStoreProvider.d.ts +20 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +52 -0
- package/dist/react/LiveStoreProvider.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useComponentState.d.ts +50 -0
- package/dist/react/useComponentState.d.ts.map +1 -0
- package/dist/react/useComponentState.js +248 -0
- package/dist/react/useComponentState.js.map +1 -0
- package/dist/react/useGlobalQuery.d.ts +3 -0
- package/dist/react/useGlobalQuery.d.ts.map +1 -0
- package/dist/react/useGlobalQuery.js +26 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +13 -0
- package/dist/react/useGraphQL.d.ts.map +1 -0
- package/dist/react/useGraphQL.js +87 -0
- package/dist/react/useGraphQL.js.map +1 -0
- package/dist/react/useLiveStoreComponent.d.ts +75 -0
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
- package/dist/react/useLiveStoreComponent.js +361 -0
- package/dist/react/useLiveStoreComponent.js.map +1 -0
- package/dist/react/useQuery.d.ts +3 -0
- package/dist/react/useQuery.d.ts.map +1 -0
- package/dist/react/useQuery.js +42 -0
- package/dist/react/useQuery.js.map +1 -0
- package/dist/react/useTemporaryQuery.d.ts +8 -0
- package/dist/react/useTemporaryQuery.d.ts.map +1 -0
- package/dist/react/useTemporaryQuery.js +17 -0
- package/dist/react/useTemporaryQuery.js.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
- package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
- package/dist/reactive.d.ts +134 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +409 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +32 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +30 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graph.d.ts +10 -0
- package/dist/reactiveQueries/graph.d.ts.map +1 -0
- package/dist/reactiveQueries/graph.js +6 -0
- package/dist/reactiveQueries/graph.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +42 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +99 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +23 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +36 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +35 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +97 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -0
- package/dist/storage/in-memory/index.d.ts +15 -0
- package/dist/storage/in-memory/index.d.ts.map +1 -0
- package/dist/storage/in-memory/index.js +14 -0
- package/dist/storage/in-memory/index.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/tauri/index.d.ts +19 -0
- package/dist/storage/tauri/index.d.ts.map +1 -0
- package/dist/storage/tauri/index.js +38 -0
- package/dist/storage/tauri/index.js.map +1 -0
- package/dist/storage/utils/idb.d.ts +10 -0
- package/dist/storage/utils/idb.d.ts.map +1 -0
- package/dist/storage/utils/idb.js +58 -0
- package/dist/storage/utils/idb.js.map +1 -0
- package/dist/storage/web-worker/index.d.ts +27 -0
- package/dist/storage/web-worker/index.d.ts.map +1 -0
- package/dist/storage/web-worker/index.js +74 -0
- package/dist/storage/web-worker/index.js.map +1 -0
- package/dist/storage/web-worker/worker.d.ts +13 -0
- package/dist/storage/web-worker/worker.d.ts.map +1 -0
- package/dist/storage/web-worker/worker.js +110 -0
- package/dist/storage/web-worker/worker.js.map +1 -0
- package/dist/store.d.ts +159 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +626 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +28 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +55 -0
- package/dist/util.js.map +1 -0
- package/package.json +47 -19
- package/src/QueryCache.ts +1 -1
- package/src/__tests__/react/fixture.tsx +35 -39
- package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
- package/src/__tests__/react/useQuery.test.tsx +48 -0
- package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
- package/src/__tests__/reactive.test.ts +194 -142
- package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
- package/src/effect/LiveStore.ts +22 -31
- package/src/events.ts +1 -1
- package/src/inMemoryDatabase.ts +117 -142
- package/src/index.ts +18 -22
- package/src/migrations.ts +119 -0
- package/src/otel.ts +0 -11
- package/src/react/LiveStoreProvider.tsx +24 -23
- package/src/react/index.ts +12 -7
- package/src/react/useComponentState.ts +409 -0
- package/src/react/useQuery.ts +58 -0
- package/src/react/useTemporaryQuery.ts +21 -0
- package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
- package/src/reactive.ts +386 -267
- package/src/reactiveQueries/base-class.ts +61 -39
- package/src/reactiveQueries/graph.ts +15 -0
- package/src/reactiveQueries/graphql.ts +147 -31
- package/src/reactiveQueries/js.ts +54 -21
- package/src/reactiveQueries/sql.ts +128 -37
- package/src/schema.ts +69 -145
- package/src/storage/in-memory/index.ts +21 -0
- package/src/storage/index.ts +27 -0
- package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
- package/src/storage/web-worker/index.ts +116 -0
- package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
- package/src/store.ts +466 -457
- package/src/util.ts +13 -3
- package/tsconfig.json +1 -3
- package/src/backends/base.ts +0 -67
- package/src/backends/index.ts +0 -98
- package/src/backends/noop.ts +0 -32
- package/src/backends/web-in-memory.ts +0 -65
- package/src/backends/web.ts +0 -97
- package/src/react/useGlobalQuery.ts +0 -40
- package/src/react/useGraphQL.ts +0 -112
- package/src/react/useLiveStoreComponent.ts +0 -483
- /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/schema.ts
CHANGED
|
@@ -1,45 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
primaryKey?: boolean
|
|
9
|
-
} & (
|
|
10
|
-
| { type: 'text'; default?: string }
|
|
11
|
-
| { type: 'json'; default?: string }
|
|
12
|
-
| { type: 'integer'; default?: number }
|
|
13
|
-
| { type: 'boolean'; default?: boolean }
|
|
14
|
-
| { type: 'real'; default?: number }
|
|
15
|
-
| { type: 'blob'; default?: any }
|
|
16
|
-
) // sqlite uses numbers for booleans but we fake it
|
|
17
|
-
|
|
18
|
-
// TODO: defaults should be nullable for nullable columns
|
|
19
|
-
type ColumnDefinitionWithDefault = {
|
|
20
|
-
primaryKey?: boolean
|
|
21
|
-
} & (
|
|
22
|
-
| { type: 'text'; nullable?: true; default: string }
|
|
23
|
-
| { type: 'json'; nullable?: true; default: string }
|
|
24
|
-
| { type: 'integer'; nullable?: true; default: number }
|
|
25
|
-
| { type: 'boolean'; nullable?: true; default: boolean }
|
|
26
|
-
| { type: 'real'; nullable: true; default: number | null }
|
|
27
|
-
| { type: 'blob'; nullable: true; default: any | null }
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
export type TableDefinition = {
|
|
31
|
-
columns: {
|
|
32
|
-
[key: string]: ColumnDefinition
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Can be used for various purposes e.g. to provide a foreign key constraint like below:
|
|
36
|
-
* ```ts
|
|
37
|
-
* columnsRaw: (columnsStr) => `${columnsStr}, foreign key (userId) references users(id)`
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
columnsRaw?: (columnsStr: string) => string
|
|
41
|
-
indexes?: Index[]
|
|
42
|
-
}
|
|
1
|
+
import type { PrettifyFlat } from '@livestore/utils'
|
|
2
|
+
import { mapObjectValues } from '@livestore/utils'
|
|
3
|
+
import type { Schema } from '@livestore/utils/effect'
|
|
4
|
+
import type { SqliteAst } from 'effect-db-schema'
|
|
5
|
+
import { SqliteDsl } from 'effect-db-schema'
|
|
6
|
+
|
|
7
|
+
import { DbSchema } from './index.js'
|
|
43
8
|
|
|
44
9
|
export type Index = {
|
|
45
10
|
name: string
|
|
@@ -48,35 +13,61 @@ export type Index = {
|
|
|
48
13
|
isUnique?: boolean
|
|
49
14
|
}
|
|
50
15
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
16
|
+
// A global variable representing component state tables we should create in the database
|
|
17
|
+
export const componentStateTables: { [key: string]: SqliteAst.Table } = {}
|
|
18
|
+
|
|
19
|
+
export type InputSchema = {
|
|
20
|
+
tables: {
|
|
21
|
+
[tableName: string]: SqliteDsl.TableDefinition<any, any>
|
|
55
22
|
}
|
|
23
|
+
materializedViews?: MaterializedViewDefinitions
|
|
24
|
+
actions: ActionDefinitions<any>
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
|
|
27
|
+
export const makeSchema = <TSchema extends InputSchema>(schema: TSchema): Schema =>
|
|
28
|
+
({
|
|
29
|
+
tables: { ...mapObjectValues(schema.tables, (_tableName, table) => table.ast), ...systemTables },
|
|
30
|
+
materializedViews: schema.materializedViews ?? {},
|
|
31
|
+
actions: schema.actions,
|
|
32
|
+
}) satisfies Schema
|
|
33
|
+
|
|
34
|
+
export type ComponentStateSchema = SqliteDsl.TableDefinition<any, any> & {
|
|
35
|
+
// TODO
|
|
36
|
+
register: () => void
|
|
37
|
+
}
|
|
60
38
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
// TODO get rid of "side effect" in this function (via explicit register fn)
|
|
40
|
+
export const defineComponentStateSchema = <TName extends string, TColumns extends SqliteDsl.Columns>(
|
|
41
|
+
// TODO get rid of the `name` param here and use the `componentKey` name instead
|
|
42
|
+
name: TName,
|
|
43
|
+
columns: TColumns,
|
|
44
|
+
): SqliteDsl.TableDefinition<
|
|
45
|
+
`components__${TName}`,
|
|
46
|
+
PrettifyFlat<TColumns & { id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false> }>
|
|
47
|
+
> => {
|
|
48
|
+
const tablePath = `components__${name}` as const
|
|
65
49
|
if (Object.keys(componentStateTables).includes(tablePath)) {
|
|
66
50
|
// throw new Error(`Can't register duplicate component: ${name}`)
|
|
67
51
|
console.error(`Can't register duplicate component: ${tablePath}`)
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
const schemaWithId =
|
|
54
|
+
const schemaWithId = columns as unknown as PrettifyFlat<
|
|
55
|
+
TColumns & {
|
|
56
|
+
id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
schemaWithId.id = DbSchema.text({ primaryKey: true })
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
const tableDef = SqliteDsl.table(tablePath, schemaWithId, [])
|
|
73
63
|
|
|
74
|
-
|
|
64
|
+
// TODO move into register fn
|
|
65
|
+
componentStateTables[tablePath] = tableDef.ast
|
|
75
66
|
|
|
76
|
-
return
|
|
67
|
+
return tableDef
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
type SQLWriteStatement = {
|
|
70
|
+
export type SQLWriteStatement = {
|
|
80
71
|
sql: string
|
|
81
72
|
|
|
82
73
|
/** Tables written by the statement */
|
|
@@ -96,31 +87,31 @@ export type Schema = {
|
|
|
96
87
|
actions: ActionDefinitions<any>
|
|
97
88
|
}
|
|
98
89
|
|
|
99
|
-
export type TableDefinitions = { [key: string]:
|
|
90
|
+
export type TableDefinitions = { [key: string]: SqliteAst.Table }
|
|
100
91
|
export type MaterializedViewDefinitions = { [key: string]: {} }
|
|
101
92
|
export type ActionDefinitions<TArgsMap extends Record<string, any>> = {
|
|
102
93
|
[key in keyof TArgsMap]: ActionDefinition<TArgsMap[key]>
|
|
103
94
|
}
|
|
104
95
|
|
|
105
|
-
export const
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
96
|
+
export const SCHEMA_META_TABLE = '__livestore_schema'
|
|
97
|
+
|
|
98
|
+
const schemaMetaTable = SqliteDsl.table(SCHEMA_META_TABLE, {
|
|
99
|
+
tableName: SqliteDsl.text({ primaryKey: true }),
|
|
100
|
+
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
101
|
+
/** ISO date format */
|
|
102
|
+
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export type SchemaMetaRow = SqliteDsl.FromTable.RowDecoded<typeof schemaMetaTable>
|
|
106
|
+
|
|
107
|
+
export const systemTables = {
|
|
108
|
+
// [EVENTS_TABLE_NAME]: SqliteDsl.table(EVENTS_TABLE_NAME, {
|
|
109
|
+
// id: SqliteDsl.text({ primaryKey: true }),
|
|
110
|
+
// type: SqliteDsl.text({ nullable: false }),
|
|
111
|
+
// args: SqliteDsl.text({ nullable: false }),
|
|
112
|
+
// }).ast,
|
|
113
|
+
[SCHEMA_META_TABLE]: schemaMetaTable.ast,
|
|
114
|
+
} satisfies TableDefinitions
|
|
124
115
|
|
|
125
116
|
export const defineTables = <T extends TableDefinitions>(tables: T) => tables
|
|
126
117
|
|
|
@@ -143,77 +134,10 @@ type RecordValues<T> = T extends Record<string, infer V> ? V : never
|
|
|
143
134
|
|
|
144
135
|
export type GetActionArgs<A> = A extends ActionDefinition<infer TArgs> ? TArgs : never
|
|
145
136
|
|
|
137
|
+
// TODO get rid of this
|
|
146
138
|
declare global {
|
|
147
139
|
// NOTE Can be extended
|
|
148
140
|
interface LiveStoreActionDefinitionsTypes {
|
|
149
141
|
[key: string]: ActionDefinition
|
|
150
142
|
}
|
|
151
143
|
}
|
|
152
|
-
|
|
153
|
-
const mergeSystemSchema = <S extends Schema>(schema: S) => {
|
|
154
|
-
return {
|
|
155
|
-
...schema,
|
|
156
|
-
tables: {
|
|
157
|
-
...schema.tables,
|
|
158
|
-
...systemTables,
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Destructively load a schema into a database,
|
|
165
|
-
* dropping any existing tables and creating new ones.
|
|
166
|
-
*/
|
|
167
|
-
export const loadSchema = async (backend: InMemoryDatabase | Backend, schema: Schema) => {
|
|
168
|
-
const fullSchemaWithComponents = { ...schema, tables: { ...schema.tables, ...componentStateTables } }
|
|
169
|
-
|
|
170
|
-
// Loop through all the tables and create them in the SQLite database
|
|
171
|
-
for (const [tableName, tableDefinition] of Object.entries(fullSchemaWithComponents.tables)) {
|
|
172
|
-
const primaryKeys = Object.entries(tableDefinition.columns)
|
|
173
|
-
.filter(([_, columnDef]) => columnDef.primaryKey)
|
|
174
|
-
.map(([columnName, _]) => columnName)
|
|
175
|
-
const columnDefStrs = Object.entries(tableDefinition.columns).map(([columnName, column]) =>
|
|
176
|
-
toSqliteColumnSpec(columnName, column),
|
|
177
|
-
)
|
|
178
|
-
if (primaryKeys.length > 0) {
|
|
179
|
-
columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
|
|
180
|
-
}
|
|
181
|
-
const mapColumns = tableDefinition.columnsRaw ?? ((_) => _)
|
|
182
|
-
const columnSpec = mapColumns(columnDefStrs.join(', '))
|
|
183
|
-
|
|
184
|
-
backend.execute(sql`drop table if exists ${tableName}`)
|
|
185
|
-
|
|
186
|
-
backend.execute(sql`create table if not exists ${tableName} (${columnSpec});`)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
await createIndexes(backend, schema)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const toSqliteColumnSpec = (columnName: string, column: ColumnDefinition) => {
|
|
193
|
-
const columnType = column.type === 'boolean' ? 'integer' : column.type
|
|
194
|
-
// const primaryKey = column.primaryKey ? 'primary key' : ''
|
|
195
|
-
const nullable = column.nullable === false ? 'not null' : ''
|
|
196
|
-
const defaultValue =
|
|
197
|
-
column.default === undefined
|
|
198
|
-
? ''
|
|
199
|
-
: column.type === 'text'
|
|
200
|
-
? `default '${column.default}'`
|
|
201
|
-
: `default ${column.default}`
|
|
202
|
-
|
|
203
|
-
return `${columnName} ${columnType} ${nullable} ${defaultValue}`
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const createIndexFromDefinition = (tableName: string, index: Index) => {
|
|
207
|
-
const uniqueStr = index.isUnique ? 'UNIQUE' : ''
|
|
208
|
-
return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const createIndexes = async (db: Backend | InMemoryDatabase, schema: Schema) => {
|
|
212
|
-
for (const [tableName, tableDefinition] of Object.entries(schema.tables)) {
|
|
213
|
-
if (tableDefinition.indexes !== undefined) {
|
|
214
|
-
for (const index of tableDefinition.indexes) {
|
|
215
|
-
db.execute(createIndexFromDefinition(tableName, index))
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
3
|
+
import type { PreparedBindValues } from '../../util.js'
|
|
4
|
+
import type { Storage, StorageOtelProps } from '../index.js'
|
|
5
|
+
|
|
6
|
+
export type StorageOptionsWebInMemory = {
|
|
7
|
+
type: 'web-in-memory'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** NOTE: This storage is currently only used for testing */
|
|
11
|
+
export class InMemoryStorage implements Storage {
|
|
12
|
+
constructor(readonly otelTracer: otel.Tracer) {}
|
|
13
|
+
|
|
14
|
+
static load = async (_options?: StorageOptionsWebInMemory) => {
|
|
15
|
+
return ({ otelTracer }: StorageOtelProps) => new InMemoryStorage(otelTracer)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
execute = (_query: string, _bindValues?: PreparedBindValues): void => {}
|
|
19
|
+
|
|
20
|
+
getPersistedData = async (): Promise<Uint8Array> => new Uint8Array()
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// A storage represents a raw SQLite database.
|
|
2
|
+
// Examples include:
|
|
3
|
+
// - A native SQLite process running in a Tauri Rust process
|
|
4
|
+
// - A SQL.js WASM version of SQLite running in a web worker
|
|
5
|
+
//
|
|
6
|
+
// We can send commands to execute various kinds of queries,
|
|
7
|
+
// and respond to various events from the database.
|
|
8
|
+
|
|
9
|
+
import type * as otel from '@opentelemetry/api'
|
|
10
|
+
|
|
11
|
+
import type { PreparedBindValues } from '../util.js'
|
|
12
|
+
|
|
13
|
+
export type StorageInit = (otelProps: StorageOtelProps) => Promise<Storage> | Storage
|
|
14
|
+
|
|
15
|
+
export interface Storage {
|
|
16
|
+
execute(query: string, bindValues?: PreparedBindValues, parentSpan?: otel.Span): void
|
|
17
|
+
|
|
18
|
+
/** Return a snapshot of persisted data from the storage */
|
|
19
|
+
getPersistedData(parentSpan?: otel.Span): Promise<Uint8Array>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type StorageType = 'tauri' | 'web' | 'web-in-memory'
|
|
23
|
+
|
|
24
|
+
export type StorageOtelProps = {
|
|
25
|
+
otelTracer: otel.Tracer
|
|
26
|
+
parentSpan: otel.Span
|
|
27
|
+
}
|
|
@@ -2,38 +2,33 @@ import { getTraceParentHeader } from '@livestore/utils'
|
|
|
2
2
|
import type * as otel from '@opentelemetry/api'
|
|
3
3
|
import { invoke } from '@tauri-apps/api'
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
import { prepareBindValues } from '
|
|
7
|
-
import {
|
|
8
|
-
import type { BackendOtelProps, SelectResponse } from './index.js'
|
|
5
|
+
import type { PreparedBindValues } from '../../util.js'
|
|
6
|
+
import { prepareBindValues } from '../../util.js'
|
|
7
|
+
import type { Storage, StorageOtelProps } from '../index.js'
|
|
9
8
|
|
|
10
|
-
export type
|
|
11
|
-
type: 'tauri'
|
|
9
|
+
export type StorageOptionsTauri = {
|
|
12
10
|
dbDirPath: string
|
|
13
11
|
appDbFileName: string
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
export class
|
|
14
|
+
export class TauriStorage implements Storage {
|
|
17
15
|
constructor(
|
|
18
16
|
readonly dbFilePath: string,
|
|
19
17
|
readonly dbDirPath: string,
|
|
20
18
|
readonly otelTracer: otel.Tracer,
|
|
21
19
|
readonly parentSpan: otel.Span,
|
|
22
|
-
) {
|
|
23
|
-
super()
|
|
24
|
-
}
|
|
20
|
+
) {}
|
|
25
21
|
|
|
26
|
-
static load =
|
|
27
|
-
{ dbDirPath, appDbFileName }:
|
|
28
|
-
{ otelTracer, parentSpan }:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await invoke('initialize_connection', { dbName: dbFilePath, otelData: getOtelData_(parentSpan) })
|
|
22
|
+
static load =
|
|
23
|
+
({ dbDirPath, appDbFileName }: StorageOptionsTauri) =>
|
|
24
|
+
async ({ otelTracer, parentSpan }: StorageOtelProps) => {
|
|
25
|
+
const dbFilePath = `${dbDirPath}/${appDbFileName}`
|
|
26
|
+
await invoke('initialize_connection', { dbName: dbFilePath, otelData: getOtelData_(parentSpan) })
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
return new TauriStorage(dbFilePath, dbDirPath, otelTracer, parentSpan)
|
|
29
|
+
}
|
|
35
30
|
|
|
36
|
-
execute = (query: string, bindValues?:
|
|
31
|
+
execute = (query: string, bindValues?: PreparedBindValues, parentSpan?: otel.Span): void => {
|
|
37
32
|
// console.log({ query, bindValues, prepared: prepareBindValues(bindValues ?? {}, query) })
|
|
38
33
|
void invoke('execute', {
|
|
39
34
|
dbName: this.dbFilePath,
|
|
@@ -43,15 +38,6 @@ export class TauriBackend extends BaseBackend {
|
|
|
43
38
|
})
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
select = async <T>(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): Promise<SelectResponse<T>> => {
|
|
47
|
-
return invoke('select', {
|
|
48
|
-
db: this.dbFilePath,
|
|
49
|
-
query,
|
|
50
|
-
values: bindValues ?? {},
|
|
51
|
-
otelData: this.getOtelData(parentSpan),
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
getPersistedData = async (parentSpan?: otel.Span): Promise<Uint8Array> => {
|
|
56
42
|
const headers = new Headers()
|
|
57
43
|
headers.set('traceparent', getTraceParentHeader(parentSpan ?? this.parentSpan))
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { casesHandled } from '@livestore/utils'
|
|
2
|
+
import type * as otel from '@opentelemetry/api'
|
|
3
|
+
import * as Comlink from 'comlink'
|
|
4
|
+
|
|
5
|
+
import type { PreparedBindValues } from '../../util.js'
|
|
6
|
+
import type { Storage, StorageOtelProps } from '../index.js'
|
|
7
|
+
import { IDB } from '../utils/idb.js'
|
|
8
|
+
import type { WrappedWorker } from './worker.js'
|
|
9
|
+
|
|
10
|
+
export type StorageType = 'opfs' | 'indexeddb'
|
|
11
|
+
|
|
12
|
+
export type StorageOptionsWeb = {
|
|
13
|
+
/** Specifies where to persist data for this storage */
|
|
14
|
+
type: StorageType
|
|
15
|
+
fileName: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class WebWorkerStorage implements Storage {
|
|
19
|
+
worker: Comlink.Remote<WrappedWorker>
|
|
20
|
+
options: StorageOptionsWeb
|
|
21
|
+
otelTracer: otel.Tracer
|
|
22
|
+
|
|
23
|
+
executionBacklog: { query: string; bindValues?: PreparedBindValues }[] = []
|
|
24
|
+
executionPromise: Promise<void> | undefined
|
|
25
|
+
|
|
26
|
+
private constructor({
|
|
27
|
+
worker,
|
|
28
|
+
options,
|
|
29
|
+
otelTracer,
|
|
30
|
+
executionPromise,
|
|
31
|
+
}: {
|
|
32
|
+
worker: Comlink.Remote<WrappedWorker>
|
|
33
|
+
options: StorageOptionsWeb
|
|
34
|
+
otelTracer: otel.Tracer
|
|
35
|
+
executionPromise: Promise<void>
|
|
36
|
+
}) {
|
|
37
|
+
this.worker = worker
|
|
38
|
+
this.options = options
|
|
39
|
+
this.otelTracer = otelTracer
|
|
40
|
+
this.executionPromise = executionPromise
|
|
41
|
+
|
|
42
|
+
executionPromise.then(() => this.executeBacklog())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static load = (options: StorageOptionsWeb) => {
|
|
46
|
+
// TODO: Importing the worker like this only works with Vite;
|
|
47
|
+
// should this really be inside the LiveStore library?
|
|
48
|
+
// Doesn't work with Firefox right now during dev https://bugzilla.mozilla.org/show_bug.cgi?id=1247687
|
|
49
|
+
const worker = new Worker(new URL('./worker.js', import.meta.url), {
|
|
50
|
+
type: 'module',
|
|
51
|
+
})
|
|
52
|
+
const wrappedWorker = Comlink.wrap<WrappedWorker>(worker)
|
|
53
|
+
|
|
54
|
+
return ({ otelTracer }: StorageOtelProps) =>
|
|
55
|
+
new WebWorkerStorage({
|
|
56
|
+
worker: wrappedWorker,
|
|
57
|
+
options,
|
|
58
|
+
otelTracer,
|
|
59
|
+
executionPromise: wrappedWorker.initialize(options),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
execute = (query: string, bindValues?: PreparedBindValues) => {
|
|
64
|
+
this.executionBacklog.push({ query, bindValues })
|
|
65
|
+
|
|
66
|
+
// Instead of sending the queries to the worker immediately, we wait a bit and batch them up (which reduces the number of messages sent to the worker)
|
|
67
|
+
if (this.executionPromise === undefined) {
|
|
68
|
+
this.executionPromise = new Promise((resolve) => {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
this.executeBacklog()
|
|
71
|
+
|
|
72
|
+
resolve()
|
|
73
|
+
}, 10)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private executeBacklog = () => {
|
|
79
|
+
void this.worker.executeBulk(this.executionBacklog)
|
|
80
|
+
this.executionBacklog = []
|
|
81
|
+
this.executionPromise = undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getPersistedData = async (_parentSpan?: otel.Span): Promise<Uint8Array> => getPersistedData(this.options)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const getPersistedData = async (options: StorageOptionsWeb): Promise<Uint8Array> => {
|
|
88
|
+
switch (options.type) {
|
|
89
|
+
case 'opfs': {
|
|
90
|
+
try {
|
|
91
|
+
const rootHandle = await navigator.storage.getDirectory()
|
|
92
|
+
const fileHandle = await rootHandle.getFileHandle(options.fileName + '.db')
|
|
93
|
+
const file = await fileHandle.getFile()
|
|
94
|
+
const buffer = await file.arrayBuffer()
|
|
95
|
+
const data = new Uint8Array(buffer)
|
|
96
|
+
|
|
97
|
+
return data
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
if (error instanceof DOMException && error.name === 'NotFoundError') {
|
|
100
|
+
return new Uint8Array()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'indexeddb': {
|
|
108
|
+
const idb = new IDB(options.fileName)
|
|
109
|
+
|
|
110
|
+
return (await idb.get('db')) ?? new Uint8Array()
|
|
111
|
+
}
|
|
112
|
+
default: {
|
|
113
|
+
casesHandled(options.type)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -8,10 +8,10 @@ import type * as SqliteWasm from 'sqlite-esm'
|
|
|
8
8
|
import sqlite3InitModule from 'sqlite-esm'
|
|
9
9
|
|
|
10
10
|
// import { v4 as uuid } from 'uuid'
|
|
11
|
-
import type { Bindable } from '
|
|
12
|
-
import { casesHandled, sql } from '
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
11
|
+
import type { Bindable } from '../../util.js'
|
|
12
|
+
import { casesHandled, sql } from '../../util.js'
|
|
13
|
+
import { IDB } from '../utils/idb.js'
|
|
14
|
+
import type { StorageOptionsWeb } from './index.js'
|
|
15
15
|
|
|
16
16
|
// A global variable to hold the database connection.
|
|
17
17
|
// let db: SqliteWasm.Database
|
|
@@ -19,11 +19,11 @@ let db: SqliteWasm.DatabaseApi
|
|
|
19
19
|
|
|
20
20
|
let sqlite3: SqliteWasm.Sqlite3Static
|
|
21
21
|
|
|
22
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
22
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
23
23
|
let idb: IDB | undefined
|
|
24
24
|
|
|
25
|
-
/** The location where this database
|
|
26
|
-
let
|
|
25
|
+
/** The location where this database storage persists its data */
|
|
26
|
+
let options_: StorageOptionsWeb
|
|
27
27
|
|
|
28
28
|
const configureConnection = () =>
|
|
29
29
|
db.exec(sql`
|
|
@@ -35,18 +35,18 @@ const configureConnection = () =>
|
|
|
35
35
|
/** A full virtual filename in the IDB FS */
|
|
36
36
|
const fullyQualifiedFilename = (name: string) => `${name}.db`
|
|
37
37
|
|
|
38
|
-
const initialize = async (
|
|
39
|
-
|
|
38
|
+
const initialize = async (options: StorageOptionsWeb) => {
|
|
39
|
+
options_ = options
|
|
40
40
|
|
|
41
41
|
sqlite3 = await sqlite3InitModule({
|
|
42
42
|
print: (message) => console.log(`[sql-client] ${message}`),
|
|
43
43
|
printErr: (message) => console.error(`[sql-client] ${message}`),
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
switch (
|
|
46
|
+
switch (options.type) {
|
|
47
47
|
case 'opfs': {
|
|
48
48
|
try {
|
|
49
|
-
db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(
|
|
49
|
+
db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(options.fileName)) // , 'c'
|
|
50
50
|
} catch (e) {
|
|
51
51
|
debugger
|
|
52
52
|
}
|
|
@@ -55,7 +55,7 @@ const initialize = async ({ persistentDatabaseLocation }: { persistentDatabaseLo
|
|
|
55
55
|
case 'indexeddb': {
|
|
56
56
|
try {
|
|
57
57
|
db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' })
|
|
58
|
-
idb = new IDB(
|
|
58
|
+
idb = new IDB(options.fileName)
|
|
59
59
|
|
|
60
60
|
const bytes = await idb.get('db')
|
|
61
61
|
|
|
@@ -70,21 +70,15 @@ const initialize = async ({ persistentDatabaseLocation }: { persistentDatabaseLo
|
|
|
70
70
|
}
|
|
71
71
|
break
|
|
72
72
|
}
|
|
73
|
-
case 'filesystem': {
|
|
74
|
-
throw new Error('Persisting to native FS is not supported in the web worker backend')
|
|
75
|
-
}
|
|
76
|
-
case 'volatile-in-memory': {
|
|
77
|
-
break
|
|
78
|
-
}
|
|
79
73
|
default: {
|
|
80
|
-
casesHandled(
|
|
74
|
+
casesHandled(options.type)
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
configureConnection()
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
81
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
88
82
|
let idbPersistTimeout: NodeJS.Timeout | undefined
|
|
89
83
|
|
|
90
84
|
type ExecutionQueueItem = { query: string; bindValues?: Bindable }
|
|
@@ -119,8 +113,8 @@ const executeBulk = (executionItems: ExecutionQueueItem[]): void => {
|
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
123
|
-
if (
|
|
116
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
117
|
+
if (options_.type === 'indexeddb') {
|
|
124
118
|
if (idbPersistTimeout !== undefined) {
|
|
125
119
|
clearTimeout(idbPersistTimeout)
|
|
126
120
|
}
|
|
@@ -133,36 +127,7 @@ const executeBulk = (executionItems: ExecutionQueueItem[]): void => {
|
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
129
|
|
|
136
|
-
const
|
|
137
|
-
const resultRows: T[] = []
|
|
138
|
-
|
|
139
|
-
db.exec({
|
|
140
|
-
sql: query,
|
|
141
|
-
bind: bindValues,
|
|
142
|
-
rowMode: 'object',
|
|
143
|
-
resultRows,
|
|
144
|
-
} as TODO)
|
|
145
|
-
|
|
146
|
-
return { results: resultRows }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const getPersistedData = async (): Promise<Uint8Array> => {
|
|
150
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite backend
|
|
151
|
-
if (persistentDatabaseLocation_.type === 'indexeddb') {
|
|
152
|
-
const data = sqlite3.capi.sqlite3_js_db_export(db.pointer)
|
|
153
|
-
return Comlink.transfer(data, [data.buffer])
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const rootHandle = await navigator.storage.getDirectory()
|
|
157
|
-
const fileHandle = await rootHandle.getFileHandle(db.filename)
|
|
158
|
-
const file = await fileHandle.getFile()
|
|
159
|
-
const buffer = await file.arrayBuffer()
|
|
160
|
-
const data = new Uint8Array(buffer)
|
|
161
|
-
|
|
162
|
-
return Comlink.transfer(data, [data.buffer])
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const wrappedWorker = { initialize, executeBulk, select, getPersistedData }
|
|
130
|
+
const wrappedWorker = { initialize, executeBulk }
|
|
166
131
|
|
|
167
132
|
export type WrappedWorker = typeof wrappedWorker
|
|
168
133
|
|