@livestore/livestore 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +7 -7
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/QueryCache.d.ts +20 -0
  4. package/dist/QueryCache.d.ts.map +1 -0
  5. package/dist/QueryCache.js +71 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/__tests__/react/fixture.d.ts +25 -0
  8. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  9. package/dist/__tests__/react/fixture.js +61 -0
  10. package/dist/__tests__/react/fixture.js.map +1 -0
  11. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  12. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  14. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  15. package/dist/__tests__/reactive.test.d.ts +2 -0
  16. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  17. package/dist/__tests__/reactive.test.js +198 -0
  18. package/dist/__tests__/reactive.test.js.map +1 -0
  19. package/dist/backends/base.d.ts +13 -0
  20. package/dist/backends/base.d.ts.map +1 -0
  21. package/dist/backends/base.js +53 -0
  22. package/dist/backends/base.js.map +1 -0
  23. package/dist/backends/in-memory/index.d.ts +22 -0
  24. package/dist/backends/in-memory/index.d.ts.map +1 -0
  25. package/dist/backends/in-memory/index.js +45 -0
  26. package/dist/backends/in-memory/index.js.map +1 -0
  27. package/dist/backends/index.d.ts +41 -0
  28. package/dist/backends/index.d.ts.map +1 -0
  29. package/dist/backends/index.js +16 -0
  30. package/dist/backends/index.js.map +1 -0
  31. package/dist/backends/tauri/index.d.ts +21 -0
  32. package/dist/backends/tauri/index.d.ts.map +1 -0
  33. package/dist/backends/tauri/index.js +48 -0
  34. package/dist/backends/tauri/index.js.map +1 -0
  35. package/dist/backends/utils/idb.d.ts +10 -0
  36. package/dist/backends/utils/idb.d.ts.map +1 -0
  37. package/dist/backends/utils/idb.js +58 -0
  38. package/dist/backends/utils/idb.js.map +1 -0
  39. package/dist/backends/web-worker/index.d.ts +26 -0
  40. package/dist/backends/web-worker/index.d.ts.map +1 -0
  41. package/dist/backends/web-worker/index.js +63 -0
  42. package/dist/backends/web-worker/index.js.map +1 -0
  43. package/dist/backends/web-worker/worker.d.ts +17 -0
  44. package/dist/backends/web-worker/worker.d.ts.map +1 -0
  45. package/dist/backends/web-worker/worker.js +139 -0
  46. package/dist/backends/web-worker/worker.js.map +1 -0
  47. package/dist/bounded-collections.d.ts +34 -0
  48. package/dist/bounded-collections.d.ts.map +1 -0
  49. package/dist/bounded-collections.js +103 -0
  50. package/dist/bounded-collections.js.map +1 -0
  51. package/dist/componentKey.d.ts +20 -0
  52. package/dist/componentKey.d.ts.map +1 -0
  53. package/dist/componentKey.js +3 -0
  54. package/dist/componentKey.js.map +1 -0
  55. package/dist/effect/LiveStore.d.ts +42 -0
  56. package/dist/effect/LiveStore.d.ts.map +1 -0
  57. package/dist/effect/LiveStore.js +37 -0
  58. package/dist/effect/LiveStore.js.map +1 -0
  59. package/dist/effect/index.d.ts +2 -0
  60. package/dist/effect/index.d.ts.map +1 -0
  61. package/dist/effect/index.js +2 -0
  62. package/dist/effect/index.js.map +1 -0
  63. package/dist/events.d.ts +7 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/inMemoryDatabase.d.ts +60 -0
  68. package/dist/inMemoryDatabase.d.ts.map +1 -0
  69. package/dist/inMemoryDatabase.js +230 -0
  70. package/dist/inMemoryDatabase.js.map +1 -0
  71. package/dist/index.d.ts +20 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +9 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/migrations.d.ts +9 -0
  76. package/dist/migrations.d.ts.map +1 -0
  77. package/dist/migrations.js +62 -0
  78. package/dist/migrations.js.map +1 -0
  79. package/dist/otel.d.ts +4 -0
  80. package/dist/otel.d.ts.map +1 -0
  81. package/dist/otel.js +6 -0
  82. package/dist/otel.js.map +1 -0
  83. package/dist/react/LiveStoreContext.d.ts +11 -0
  84. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  85. package/dist/react/LiveStoreContext.js +10 -0
  86. package/dist/react/LiveStoreContext.js.map +1 -0
  87. package/dist/react/LiveStoreProvider.d.ts +22 -0
  88. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  89. package/dist/react/LiveStoreProvider.js +49 -0
  90. package/dist/react/LiveStoreProvider.js.map +1 -0
  91. package/dist/react/index.d.ts +8 -0
  92. package/dist/react/index.d.ts.map +1 -0
  93. package/dist/react/index.js +6 -0
  94. package/dist/react/index.js.map +1 -0
  95. package/dist/react/useGlobalQuery.d.ts +3 -0
  96. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  97. package/dist/react/useGlobalQuery.js +23 -0
  98. package/dist/react/useGlobalQuery.js.map +1 -0
  99. package/dist/react/useGraphQL.d.ts +11 -0
  100. package/dist/react/useGraphQL.d.ts.map +1 -0
  101. package/dist/react/useGraphQL.js +67 -0
  102. package/dist/react/useGraphQL.js.map +1 -0
  103. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  104. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  105. package/dist/react/useLiveStoreComponent.js +301 -0
  106. package/dist/react/useLiveStoreComponent.js.map +1 -0
  107. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  108. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  109. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  110. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  111. package/dist/reactive.d.ts +140 -0
  112. package/dist/reactive.d.ts.map +1 -0
  113. package/dist/reactive.js +302 -0
  114. package/dist/reactive.js.map +1 -0
  115. package/dist/reactiveQueries/base-class.d.ts +24 -0
  116. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  117. package/dist/reactiveQueries/base-class.js +22 -0
  118. package/dist/reactiveQueries/base-class.js.map +1 -0
  119. package/dist/reactiveQueries/graphql.d.ts +25 -0
  120. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  121. package/dist/reactiveQueries/graphql.js +18 -0
  122. package/dist/reactiveQueries/graphql.js.map +1 -0
  123. package/dist/reactiveQueries/js.d.ts +19 -0
  124. package/dist/reactiveQueries/js.d.ts.map +1 -0
  125. package/dist/reactiveQueries/js.js +13 -0
  126. package/dist/reactiveQueries/js.js.map +1 -0
  127. package/dist/reactiveQueries/sql.d.ts +31 -0
  128. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  129. package/dist/reactiveQueries/sql.js +32 -0
  130. package/dist/reactiveQueries/sql.js.map +1 -0
  131. package/dist/schema.d.ts +83 -0
  132. package/dist/schema.d.ts.map +1 -0
  133. package/dist/schema.js +49 -0
  134. package/dist/schema.js.map +1 -0
  135. package/dist/storage/base.d.ts +10 -0
  136. package/dist/storage/base.d.ts.map +1 -0
  137. package/dist/storage/base.js +14 -0
  138. package/dist/storage/base.js.map +1 -0
  139. package/dist/storage/in-memory/index.d.ts +15 -0
  140. package/dist/storage/in-memory/index.d.ts.map +1 -0
  141. package/dist/storage/in-memory/index.js +14 -0
  142. package/dist/storage/in-memory/index.js.map +1 -0
  143. package/dist/storage/index.d.ts +14 -0
  144. package/dist/storage/index.d.ts.map +1 -0
  145. package/dist/storage/index.js +9 -0
  146. package/dist/storage/index.js.map +1 -0
  147. package/dist/storage/tauri/index.d.ts +19 -0
  148. package/dist/storage/tauri/index.d.ts.map +1 -0
  149. package/dist/storage/tauri/index.js +38 -0
  150. package/dist/storage/tauri/index.js.map +1 -0
  151. package/dist/storage/utils/idb.d.ts +10 -0
  152. package/dist/storage/utils/idb.d.ts.map +1 -0
  153. package/dist/storage/utils/idb.js +58 -0
  154. package/dist/storage/utils/idb.js.map +1 -0
  155. package/dist/storage/web-worker/index.d.ts +27 -0
  156. package/dist/storage/web-worker/index.d.ts.map +1 -0
  157. package/dist/storage/web-worker/index.js +76 -0
  158. package/dist/storage/web-worker/index.js.map +1 -0
  159. package/dist/storage/web-worker/worker.d.ts +13 -0
  160. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  161. package/dist/storage/web-worker/worker.js +110 -0
  162. package/dist/storage/web-worker/worker.js.map +1 -0
  163. package/dist/store.d.ts +192 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +569 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/util.d.ts +26 -0
  168. package/dist/util.d.ts.map +1 -0
  169. package/dist/util.js +53 -0
  170. package/dist/util.js.map +1 -0
  171. package/package.json +46 -19
  172. package/src/__tests__/react/fixture.tsx +19 -28
  173. package/src/effect/LiveStore.ts +8 -13
  174. package/src/events.ts +1 -1
  175. package/src/inMemoryDatabase.ts +100 -117
  176. package/src/index.ts +10 -16
  177. package/src/migrations.ts +101 -0
  178. package/src/otel.ts +0 -11
  179. package/src/react/LiveStoreProvider.tsx +12 -8
  180. package/src/react/index.ts +9 -0
  181. package/src/react/useGlobalQuery.ts +0 -3
  182. package/src/react/useLiveStoreComponent.ts +95 -37
  183. package/src/schema.ts +72 -145
  184. package/src/storage/in-memory/index.ts +21 -0
  185. package/src/storage/index.ts +27 -0
  186. package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
  187. package/src/storage/web-worker/index.ts +118 -0
  188. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  189. package/src/store.ts +112 -79
  190. package/src/util.ts +5 -1
  191. package/tsconfig.json +1 -3
  192. package/src/backends/base.ts +0 -67
  193. package/src/backends/index.ts +0 -98
  194. package/src/backends/noop.ts +0 -32
  195. package/src/backends/web-in-memory.ts +0 -65
  196. package/src/backends/web.ts +0 -97
  197. /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/schema.ts CHANGED
@@ -1,45 +1,10 @@
1
- import type { Backend } from './backends/index.js'
2
- import { EVENTS_TABLE_NAME } from './events.js'
3
- import type { InMemoryDatabase } from './inMemoryDatabase.js'
4
- import { sql } from './util.js'
5
-
6
- export type ColumnDefinition = {
7
- nullable?: boolean
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,60 @@ export type Index = {
48
13
  isUnique?: boolean
49
14
  }
50
15
 
51
- export type ComponentStateSchema<T> = {
52
- componentType: string
53
- columns: {
54
- [k in keyof T]: ColumnDefinitionWithDefault
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
- // A global variable representing component state tables we should create in the database
59
- export const componentStateTables: { [key: string]: TableDefinition } = {}
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
60
33
 
61
- export const defineComponentStateSchema = <T>(
62
- schema: ComponentStateSchema<T>,
63
- ): ComponentStateSchema<T & { id: string }> => {
64
- const tablePath = `components__${schema.componentType}`
34
+ export type ComponentStateSchema = SqliteDsl.TableDefinition<any, any> & {
35
+ // TODO
36
+ register: () => void
37
+ }
38
+
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
+ name: TName,
42
+ columns: TColumns,
43
+ ): SqliteDsl.TableDefinition<
44
+ `components__${TName}`,
45
+ PrettifyFlat<TColumns & { id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false> }>
46
+ > => {
47
+ const tablePath = `components__${name}` as const
65
48
  if (Object.keys(componentStateTables).includes(tablePath)) {
66
49
  // throw new Error(`Can't register duplicate component: ${name}`)
67
50
  console.error(`Can't register duplicate component: ${tablePath}`)
68
51
  }
69
52
 
70
- const schemaWithId = schema as ComponentStateSchema<T & { id: string }>
53
+ const schemaWithId = columns as unknown as PrettifyFlat<
54
+ TColumns & {
55
+ id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
56
+ }
57
+ >
58
+
59
+ schemaWithId.id = DbSchema.text({ primaryKey: true })
71
60
 
72
- schemaWithId.columns.id = { type: 'text', primaryKey: true } as any
61
+ const tableDef = SqliteDsl.table(tablePath, schemaWithId, [])
73
62
 
74
- componentStateTables[tablePath] = schemaWithId as any
63
+ // TODO move into register fn
64
+ componentStateTables[tablePath] = tableDef.ast
75
65
 
76
- return schemaWithId
66
+ return tableDef
77
67
  }
78
68
 
79
- type SQLWriteStatement = {
69
+ export type SQLWriteStatement = {
80
70
  sql: string
81
71
 
82
72
  /** Tables written by the statement */
@@ -96,31 +86,36 @@ export type Schema = {
96
86
  actions: ActionDefinitions<any>
97
87
  }
98
88
 
99
- export type TableDefinitions = { [key: string]: TableDefinition }
89
+ export type TableDefinitions = { [key: string]: SqliteAst.Table }
100
90
  export type MaterializedViewDefinitions = { [key: string]: {} }
101
91
  export type ActionDefinitions<TArgsMap extends Record<string, any>> = {
102
92
  [key in keyof TArgsMap]: ActionDefinition<TArgsMap[key]>
103
93
  }
104
94
 
105
- export const EVENT_CURSOR_TABLE = 'livestore__event_cursor'
106
-
107
- const systemTables = {
108
- [EVENTS_TABLE_NAME]: {
109
- columns: {
110
- id: { type: 'text', primaryKey: true },
111
- type: { type: 'text', nullable: false },
112
- args: { type: 'text', nullable: false },
113
- },
114
- },
115
- [EVENT_CURSOR_TABLE]: {
116
- columns: {
117
- id: { type: 'text', primaryKey: true },
118
- cursor: { type: 'text', nullable: false },
119
- },
120
- },
121
- } as const
122
-
123
- export const defineSchema = <S extends Schema>(schema: S) => mergeSystemSchema(schema)
95
+ export const EVENT_CURSOR_TABLE = '__livestore_event_cursor'
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
+ [EVENT_CURSOR_TABLE]: SqliteDsl.table(EVENT_CURSOR_TABLE, {
114
+ id: SqliteDsl.text({ primaryKey: true }),
115
+ cursor: SqliteDsl.text({ nullable: false }),
116
+ }).ast,
117
+ [SCHEMA_META_TABLE]: schemaMetaTable.ast,
118
+ } satisfies TableDefinitions
124
119
 
125
120
  export const defineTables = <T extends TableDefinitions>(tables: T) => tables
126
121
 
@@ -149,71 +144,3 @@ declare global {
149
144
  [key: string]: ActionDefinition
150
145
  }
151
146
  }
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 { ParamsObject } 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?: ParamsObject): 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 { ParamsObject } from '../util.js'
12
+
13
+ export type StorageInit = (otelProps: StorageOtelProps) => Promise<Storage> | Storage
14
+
15
+ export interface Storage {
16
+ execute(query: string, bindValues?: ParamsObject, 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,36 +2,31 @@ 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 { ParamsObject } from '../util.js'
6
- import { prepareBindValues } from '../util.js'
7
- import { BaseBackend } from './base.js'
8
- import type { BackendOtelProps, SelectResponse } from './index.js'
5
+ import type { ParamsObject } from '../../util.js'
6
+ import { prepareBindValues } from '../../util.js'
7
+ import type { Storage, StorageOtelProps } from '../index.js'
9
8
 
10
- export type BackendOptionsTauri = {
11
- type: 'tauri'
9
+ export type StorageOptionsTauri = {
12
10
  dbDirPath: string
13
11
  appDbFileName: string
14
12
  }
15
13
 
16
- export class TauriBackend extends BaseBackend {
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 = async (
27
- { dbDirPath, appDbFileName }: BackendOptionsTauri,
28
- { otelTracer, parentSpan }: BackendOtelProps,
29
- ): Promise<TauriBackend> => {
30
- const dbFilePath = `${dbDirPath}/${appDbFileName}`
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
- return new TauriBackend(dbFilePath, dbDirPath, otelTracer, parentSpan)
34
- }
28
+ return new TauriStorage(dbFilePath, dbDirPath, otelTracer, parentSpan)
29
+ }
35
30
 
36
31
  execute = (query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void => {
37
32
  // console.log({ query, bindValues, prepared: prepareBindValues(bindValues ?? {}, query) })
@@ -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,118 @@
1
+ import { casesHandled } from '@livestore/utils'
2
+ import type * as otel from '@opentelemetry/api'
3
+ import * as Comlink from 'comlink'
4
+
5
+ import type { ParamsObject } from '../../util.js'
6
+ import { prepareBindValues } from '../../util.js'
7
+ import type { Storage, StorageOtelProps } from '../index.js'
8
+ import { IDB } from '../utils/idb.js'
9
+ import type { WrappedWorker } from './worker.js'
10
+
11
+ export type StorageType = 'opfs' | 'indexeddb'
12
+
13
+ export type StorageOptionsWeb = {
14
+ /** Specifies where to persist data for this storage */
15
+ type: StorageType
16
+ virtualFilename: string
17
+ }
18
+
19
+ export class WebWorkerStorage implements Storage {
20
+ worker: Comlink.Remote<WrappedWorker>
21
+ options: StorageOptionsWeb
22
+ otelTracer: otel.Tracer
23
+
24
+ executionBacklog: { query: string; bindValues?: ParamsObject }[] = []
25
+ executionPromise: Promise<void> | undefined
26
+
27
+ private constructor({
28
+ worker,
29
+ options,
30
+ otelTracer,
31
+ executionPromise,
32
+ }: {
33
+ worker: Comlink.Remote<WrappedWorker>
34
+ options: StorageOptionsWeb
35
+ otelTracer: otel.Tracer
36
+ executionPromise: Promise<void>
37
+ }) {
38
+ this.worker = worker
39
+ this.options = options
40
+ this.otelTracer = otelTracer
41
+ this.executionPromise = executionPromise
42
+
43
+ executionPromise.then(() => this.executeBacklog())
44
+ }
45
+
46
+ static load = (options: StorageOptionsWeb) => {
47
+ // TODO: Importing the worker like this only works with Vite;
48
+ // should this really be inside the LiveStore library?
49
+ // Doesn't work with Firefox right now during dev https://bugzilla.mozilla.org/show_bug.cgi?id=1247687
50
+ const worker = new Worker(new URL('./worker.js', import.meta.url), {
51
+ type: 'module',
52
+ })
53
+ const wrappedWorker = Comlink.wrap<WrappedWorker>(worker)
54
+
55
+ return ({ otelTracer }: StorageOtelProps) =>
56
+ new WebWorkerStorage({
57
+ worker: wrappedWorker,
58
+ options,
59
+ otelTracer,
60
+ executionPromise: wrappedWorker.initialize(options),
61
+ })
62
+ }
63
+
64
+ execute = (query: string, bindValues_?: ParamsObject) => {
65
+ const bindValues = prepareBindValues(bindValues_ ?? {}, query)
66
+ this.executionBacklog.push({ query, bindValues })
67
+
68
+ // 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)
69
+ if (this.executionPromise === undefined) {
70
+ this.executionPromise = new Promise((resolve) => {
71
+ setTimeout(() => {
72
+ this.executeBacklog()
73
+
74
+ resolve()
75
+ }, 10)
76
+ })
77
+ }
78
+ }
79
+
80
+ private executeBacklog = () => {
81
+ void this.worker.executeBulk(this.executionBacklog)
82
+ this.executionBacklog = []
83
+ this.executionPromise = undefined
84
+ }
85
+
86
+ getPersistedData = async (_parentSpan?: otel.Span): Promise<Uint8Array> => getPersistedData(this.options)
87
+ }
88
+
89
+ const getPersistedData = async (options: StorageOptionsWeb): Promise<Uint8Array> => {
90
+ switch (options.type) {
91
+ case 'opfs': {
92
+ try {
93
+ const rootHandle = await navigator.storage.getDirectory()
94
+ const fileHandle = await rootHandle.getFileHandle(options.virtualFilename + '.db')
95
+ const file = await fileHandle.getFile()
96
+ const buffer = await file.arrayBuffer()
97
+ const data = new Uint8Array(buffer)
98
+
99
+ return data
100
+ } catch (error: any) {
101
+ if (error instanceof DOMException && error.name === 'NotFoundError') {
102
+ return new Uint8Array()
103
+ }
104
+
105
+ throw error
106
+ }
107
+ }
108
+
109
+ case 'indexeddb': {
110
+ const idb = new IDB(options.virtualFilename)
111
+
112
+ return (await idb.get('db')) ?? new Uint8Array()
113
+ }
114
+ default: {
115
+ casesHandled(options.type)
116
+ }
117
+ }
118
+ }
@@ -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 '../util.js'
12
- import { casesHandled, sql } from '../util.js'
13
- import type { SelectResponse, WritableDatabaseLocation } from './index.js'
14
- import { IDB } from './utils/idb.js'
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 backend
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 backend persists its data */
26
- let persistentDatabaseLocation_: WritableDatabaseLocation
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 ({ persistentDatabaseLocation }: { persistentDatabaseLocation: WritableDatabaseLocation }) => {
39
- persistentDatabaseLocation_ = persistentDatabaseLocation
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 (persistentDatabaseLocation.type) {
46
+ switch (options.type) {
47
47
  case 'opfs': {
48
48
  try {
49
- db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(persistentDatabaseLocation.virtualFilename)) // , 'c'
49
+ db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(options.virtualFilename)) // , '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(persistentDatabaseLocation.virtualFilename)
58
+ idb = new IDB(options.virtualFilename)
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(persistentDatabaseLocation)
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 backend
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 backend
123
- if (persistentDatabaseLocation_.type === 'indexeddb') {
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 select = <T = any>(query: string, bindValues?: Bindable): SelectResponse<T> => {
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