@livestore/livestore 0.0.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.
Files changed (205) hide show
  1. package/README.md +108 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/LiveRiffleStore.d.ts +42 -0
  4. package/dist/LiveRiffleStore.d.ts.map +1 -0
  5. package/dist/LiveRiffleStore.js +36 -0
  6. package/dist/LiveRiffleStore.js.map +1 -0
  7. package/dist/QueryCache.d.ts +20 -0
  8. package/dist/QueryCache.d.ts.map +1 -0
  9. package/dist/QueryCache.js +71 -0
  10. package/dist/QueryCache.js.map +1 -0
  11. package/dist/__tests__/react/fixture.d.ts +141 -0
  12. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  13. package/dist/__tests__/react/fixture.js +72 -0
  14. package/dist/__tests__/react/fixture.js.map +1 -0
  15. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  19. package/dist/__tests__/react/useRiffleComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useRiffleComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useRiffleComponent.test.js +78 -0
  22. package/dist/__tests__/react/useRiffleComponent.test.js.map +1 -0
  23. package/dist/__tests__/reactive.test.d.ts +2 -0
  24. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  25. package/dist/__tests__/reactive.test.js +167 -0
  26. package/dist/__tests__/reactive.test.js.map +1 -0
  27. package/dist/backends/base.d.ts +13 -0
  28. package/dist/backends/base.d.ts.map +1 -0
  29. package/dist/backends/base.js +53 -0
  30. package/dist/backends/base.js.map +1 -0
  31. package/dist/backends/index.d.ts +41 -0
  32. package/dist/backends/index.d.ts.map +1 -0
  33. package/dist/backends/index.js +38 -0
  34. package/dist/backends/index.js.map +1 -0
  35. package/dist/backends/noop.d.ts +18 -0
  36. package/dist/backends/noop.d.ts.map +1 -0
  37. package/dist/backends/noop.js +21 -0
  38. package/dist/backends/noop.js.map +1 -0
  39. package/dist/backends/tauri.d.ts +24 -0
  40. package/dist/backends/tauri.d.ts.map +1 -0
  41. package/dist/backends/tauri.js +48 -0
  42. package/dist/backends/tauri.js.map +1 -0
  43. package/dist/backends/utils/idb.d.ts +10 -0
  44. package/dist/backends/utils/idb.d.ts.map +1 -0
  45. package/dist/backends/utils/idb.js +58 -0
  46. package/dist/backends/utils/idb.js.map +1 -0
  47. package/dist/backends/web-in-memory.d.ts +24 -0
  48. package/dist/backends/web-in-memory.d.ts.map +1 -0
  49. package/dist/backends/web-in-memory.js +46 -0
  50. package/dist/backends/web-in-memory.js.map +1 -0
  51. package/dist/backends/web-worker.d.ts +17 -0
  52. package/dist/backends/web-worker.d.ts.map +1 -0
  53. package/dist/backends/web-worker.js +139 -0
  54. package/dist/backends/web-worker.js.map +1 -0
  55. package/dist/backends/web.d.ts +28 -0
  56. package/dist/backends/web.d.ts.map +1 -0
  57. package/dist/backends/web.js +64 -0
  58. package/dist/backends/web.js.map +1 -0
  59. package/dist/bounded-collections.d.ts +34 -0
  60. package/dist/bounded-collections.d.ts.map +1 -0
  61. package/dist/bounded-collections.js +103 -0
  62. package/dist/bounded-collections.js.map +1 -0
  63. package/dist/componentKey.d.ts +20 -0
  64. package/dist/componentKey.d.ts.map +1 -0
  65. package/dist/componentKey.js +3 -0
  66. package/dist/componentKey.js.map +1 -0
  67. package/dist/effect/LiveStore.d.ts +42 -0
  68. package/dist/effect/LiveStore.d.ts.map +1 -0
  69. package/dist/effect/LiveStore.js +36 -0
  70. package/dist/effect/LiveStore.js.map +1 -0
  71. package/dist/effect/index.d.ts +2 -0
  72. package/dist/effect/index.d.ts.map +1 -0
  73. package/dist/effect/index.js +2 -0
  74. package/dist/effect/index.js.map +1 -0
  75. package/dist/events.d.ts +7 -0
  76. package/dist/events.d.ts.map +1 -0
  77. package/dist/events.js +2 -0
  78. package/dist/events.js.map +1 -0
  79. package/dist/inMemoryDatabase.d.ts +65 -0
  80. package/dist/inMemoryDatabase.d.ts.map +1 -0
  81. package/dist/inMemoryDatabase.js +241 -0
  82. package/dist/inMemoryDatabase.js.map +1 -0
  83. package/dist/index.d.ts +20 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +10 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/otel.d.ts +5 -0
  88. package/dist/otel.d.ts.map +1 -0
  89. package/dist/otel.js +17 -0
  90. package/dist/otel.js.map +1 -0
  91. package/dist/react/LiveStoreContext.d.ts +11 -0
  92. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  93. package/dist/react/LiveStoreContext.js +10 -0
  94. package/dist/react/LiveStoreContext.js.map +1 -0
  95. package/dist/react/LiveStoreProvider.d.ts +21 -0
  96. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  97. package/dist/react/LiveStoreProvider.js +48 -0
  98. package/dist/react/LiveStoreProvider.js.map +1 -0
  99. package/dist/react/RiffleProvider.d.ts +21 -0
  100. package/dist/react/RiffleProvider.d.ts.map +1 -0
  101. package/dist/react/RiffleProvider.js +48 -0
  102. package/dist/react/RiffleProvider.js.map +1 -0
  103. package/dist/react/StoreContext.d.ts +11 -0
  104. package/dist/react/StoreContext.d.ts.map +1 -0
  105. package/dist/react/StoreContext.js +10 -0
  106. package/dist/react/StoreContext.js.map +1 -0
  107. package/dist/react/index.d.ts +7 -0
  108. package/dist/react/index.d.ts.map +1 -0
  109. package/dist/react/index.js +6 -0
  110. package/dist/react/index.js.map +1 -0
  111. package/dist/react/useGlobalQuery.d.ts +3 -0
  112. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  113. package/dist/react/useGlobalQuery.js +25 -0
  114. package/dist/react/useGlobalQuery.js.map +1 -0
  115. package/dist/react/useGraphQL.d.ts +11 -0
  116. package/dist/react/useGraphQL.d.ts.map +1 -0
  117. package/dist/react/useGraphQL.js +68 -0
  118. package/dist/react/useGraphQL.js.map +1 -0
  119. package/dist/react/useLiveStoreComponent.d.ts +70 -0
  120. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  121. package/dist/react/useLiveStoreComponent.js +261 -0
  122. package/dist/react/useLiveStoreComponent.js.map +1 -0
  123. package/dist/react/useRiffleComponent.d.ts +70 -0
  124. package/dist/react/useRiffleComponent.d.ts.map +1 -0
  125. package/dist/react/useRiffleComponent.js +261 -0
  126. package/dist/react/useRiffleComponent.js.map +1 -0
  127. package/dist/react/useRiffleJsonHook.d.ts +4 -0
  128. package/dist/react/useRiffleJsonHook.d.ts.map +1 -0
  129. package/dist/react/useRiffleJsonHook.js +21 -0
  130. package/dist/react/useRiffleJsonHook.js.map +1 -0
  131. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  132. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  133. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  134. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  135. package/dist/reactive.d.ts +140 -0
  136. package/dist/reactive.d.ts.map +1 -0
  137. package/dist/reactive.js +301 -0
  138. package/dist/reactive.js.map +1 -0
  139. package/dist/reactiveQueries/base-class.d.ts +24 -0
  140. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  141. package/dist/reactiveQueries/base-class.js +22 -0
  142. package/dist/reactiveQueries/base-class.js.map +1 -0
  143. package/dist/reactiveQueries/graphql.d.ts +25 -0
  144. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/graphql.js +14 -0
  146. package/dist/reactiveQueries/graphql.js.map +1 -0
  147. package/dist/reactiveQueries/js.d.ts +19 -0
  148. package/dist/reactiveQueries/js.d.ts.map +1 -0
  149. package/dist/reactiveQueries/js.js +13 -0
  150. package/dist/reactiveQueries/js.js.map +1 -0
  151. package/dist/reactiveQueries/sql.d.ts +31 -0
  152. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  153. package/dist/reactiveQueries/sql.js +28 -0
  154. package/dist/reactiveQueries/sql.js.map +1 -0
  155. package/dist/schema.d.ts +163 -0
  156. package/dist/schema.d.ts.map +1 -0
  157. package/dist/schema.js +92 -0
  158. package/dist/schema.js.map +1 -0
  159. package/dist/store.d.ts +175 -0
  160. package/dist/store.d.ts.map +1 -0
  161. package/dist/store.js +546 -0
  162. package/dist/store.js.map +1 -0
  163. package/dist/util.d.ts +24 -0
  164. package/dist/util.d.ts.map +1 -0
  165. package/dist/util.js +51 -0
  166. package/dist/util.js.map +1 -0
  167. package/package.json +52 -0
  168. package/src/QueryCache.ts +81 -0
  169. package/src/__tests__/react/fixture.tsx +106 -0
  170. package/src/__tests__/react/useLiveStoreComponent.test.tsx +111 -0
  171. package/src/__tests__/reactive.test.ts +227 -0
  172. package/src/ambient.d.ts +7 -0
  173. package/src/backends/base.ts +67 -0
  174. package/src/backends/index.ts +94 -0
  175. package/src/backends/noop.ts +32 -0
  176. package/src/backends/tauri.ts +74 -0
  177. package/src/backends/utils/idb.ts +71 -0
  178. package/src/backends/web-in-memory.ts +65 -0
  179. package/src/backends/web-worker.ts +176 -0
  180. package/src/backends/web.ts +96 -0
  181. package/src/bounded-collections.ts +112 -0
  182. package/src/componentKey.ts +9 -0
  183. package/src/effect/LiveStore.ts +123 -0
  184. package/src/effect/index.ts +7 -0
  185. package/src/events.ts +8 -0
  186. package/src/inMemoryDatabase.ts +347 -0
  187. package/src/index.ts +47 -0
  188. package/src/otel.ts +20 -0
  189. package/src/react/LiveStoreContext.ts +23 -0
  190. package/src/react/LiveStoreProvider.tsx +93 -0
  191. package/src/react/index.ts +11 -0
  192. package/src/react/useGlobalQuery.ts +40 -0
  193. package/src/react/useGraphQL.ts +113 -0
  194. package/src/react/useLiveStoreComponent.ts +493 -0
  195. package/src/react/utils/useStateRefWithReactiveInput.ts +51 -0
  196. package/src/reactive.ts +538 -0
  197. package/src/reactiveQueries/base-class.ts +49 -0
  198. package/src/reactiveQueries/graphql.ts +52 -0
  199. package/src/reactiveQueries/js.ts +38 -0
  200. package/src/reactiveQueries/sql.ts +65 -0
  201. package/src/schema.ts +219 -0
  202. package/src/store.ts +889 -0
  203. package/src/util.ts +59 -0
  204. package/tsconfig.json +15 -0
  205. package/vitest.config.js +13 -0
@@ -0,0 +1,67 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { errorToString } from '@livestore/utils'
3
+ import { identity } from '@livestore/utils/effect'
4
+ import * as otel from '@opentelemetry/api'
5
+
6
+ import type { LiveStoreEvent } from '../events.js'
7
+ // import { EVENTS_TABLE_NAME } from '../events.js'
8
+ import type { ActionDefinition } from '../schema.js'
9
+ import type { ParamsObject } from '../util.js'
10
+ import type { Backend, SelectResponse } from './index.js'
11
+
12
+ export abstract class BaseBackend implements Backend {
13
+ abstract otelTracer: otel.Tracer
14
+
15
+ select = async <T = any>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
16
+ throw new Error('Method not implemented.')
17
+ }
18
+
19
+ execute = (query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void => {
20
+ throw new Error('Method not implemented.')
21
+ }
22
+
23
+ getPersistedData = async (parentSpan?: otel.Span): Promise<Uint8Array> => {
24
+ throw new Error('Method not implemented.')
25
+ }
26
+
27
+ // TODO move `applyEvent` logic to Store and only call `execute` here
28
+ applyEvent = (event: LiveStoreEvent, eventDefinition: ActionDefinition, parentSpan?: otel.Span): void => {
29
+ const ctx = parentSpan ? otel.trace.setSpan(otel.context.active(), parentSpan) : otel.context.active()
30
+ this.otelTracer.startActiveSpan('LiveStore:backend:applyEvent', {}, ctx, (span) => {
31
+ try {
32
+ // Careful: this SQL statement is duplicated in the backend.
33
+ // Remember to update it in src-tauri/src/store.rs:apply_event as well.
34
+ // await this.execute(sql`insert into ${EVENTS_TABLE_NAME} (id, type, args) values ($id, $type, $args)`, {
35
+ // id: event.id,
36
+ // type: event.type,
37
+ // args: JSON.stringify(event.args ?? {}),
38
+ // })
39
+
40
+ const statement =
41
+ typeof eventDefinition.statement === 'function'
42
+ ? eventDefinition.statement(event.args)
43
+ : eventDefinition.statement
44
+
45
+ const prepareBindValues = eventDefinition.prepareBindValues ?? identity
46
+
47
+ const bindValues =
48
+ typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound
49
+ ? {}
50
+ : prepareBindValues(event.args)
51
+
52
+ span.setAttributes({
53
+ 'livestore.statement.sql': statement.sql,
54
+ 'livestore.statement.writeTables': statement.writeTables,
55
+ 'livestore.statement.bindVales': JSON.stringify(bindValues),
56
+ })
57
+
58
+ this.execute(statement.sql, bindValues, span)
59
+ } catch (e: any) {
60
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: errorToString(e) })
61
+ throw e
62
+ } finally {
63
+ span.end()
64
+ }
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,94 @@
1
+ // A backend 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 { LiveStoreEvent } from '../events.js'
12
+ import type { ActionDefinition } from '../schema.js'
13
+ import type { ParamsObject } from '../util.js'
14
+ import { casesHandled } from '../util.js'
15
+ import type { BackendOptionsTauri } from './tauri.js'
16
+ import type { BackendOptionsWeb } from './web.js'
17
+ import { WebWorkerBackend } from './web.js'
18
+ import type { BackendOptionsWebInMemory } from './web-in-memory.js'
19
+ import { WebInMemoryBackend } from './web-in-memory.js'
20
+
21
+ /* A location of a persistent writable SQLite file */
22
+ export type WritableDatabaseLocation =
23
+ | {
24
+ type: 'opfs'
25
+ virtualFilename: string
26
+ }
27
+ | {
28
+ type: 'indexeddb'
29
+ virtualFilename: string
30
+ }
31
+ | {
32
+ type: 'filesystem'
33
+ directory: string
34
+ filename: string
35
+ }
36
+ | {
37
+ type: 'volatile-in-memory'
38
+ }
39
+
40
+ export interface Backend {
41
+ // Select some data from the DB.
42
+ // This should only do reads, not writes, but we don't strongly enforce that.
43
+ select<T = any>(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): Promise<SelectResponse<T>>
44
+
45
+ // Execute a query where you don't care about the result.
46
+ // Used for writes and configuration changes.
47
+ execute(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void
48
+
49
+ /** Apply an event to the backend */
50
+ applyEvent(event: LiveStoreEvent, eventDefiniton: ActionDefinition, parentSpan?: otel.Span): void
51
+
52
+ /** Return a snapshot of persisted data from the backend */
53
+ getPersistedData(parentSpan?: otel.Span): Promise<Uint8Array>
54
+ }
55
+
56
+ export type BackendType = 'tauri' | 'web' | 'web-in-memory'
57
+
58
+ export const isBackendType = (type: string): type is BackendType => {
59
+ return type === 'tauri' || type === 'web' || type === 'web-in-memory'
60
+ }
61
+
62
+ export type SelectResponse<T = any> = {
63
+ results: T[]
64
+
65
+ // other perf stats metadata about how long the query took
66
+ [key: string]: any
67
+ }
68
+
69
+ export enum IndexType {
70
+ Basic = 'Basic',
71
+ FullText = 'FullText',
72
+ }
73
+
74
+ export type BackendOptions = BackendOptionsTauri | BackendOptionsWeb | BackendOptionsWebInMemory
75
+
76
+ export const createBackend = async (options: BackendOptions): Promise<Backend> => {
77
+ switch (options.type) {
78
+ case 'tauri': {
79
+ // NOTE Dynamic import is needed to avoid Tauri is a dependency of LiveStore (e.g. when used in the web)
80
+ const { TauriBackend } = await import('./tauri.js')
81
+ return await TauriBackend.load(options)
82
+ }
83
+ case 'web': {
84
+ return WebWorkerBackend.load(options)
85
+ }
86
+ // NOTE currently only needed for testing
87
+ case 'web-in-memory': {
88
+ return WebInMemoryBackend.load(options)
89
+ }
90
+ default: {
91
+ casesHandled(options)
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,32 @@
1
+ import { makeNoopTracer } from '@livestore/utils'
2
+ import type * as otel from '@opentelemetry/api'
3
+
4
+ import type { ParamsObject } from '../util.js'
5
+ import { BaseBackend } from './base.js'
6
+ import type { SelectResponse } from './index.js'
7
+
8
+ export type BackendOptionsNoop = {
9
+ type: 'noop'
10
+ /** Specifies where to persist data for this backend */
11
+ otelTracer?: otel.Tracer
12
+ }
13
+
14
+ export class NoopBackend extends BaseBackend {
15
+ constructor(readonly otelTracer: otel.Tracer) {
16
+ super()
17
+ }
18
+
19
+ static load = async (options: BackendOptionsNoop): Promise<NoopBackend> => {
20
+ return new NoopBackend(options.otelTracer ?? makeNoopTracer())
21
+ }
22
+
23
+ execute = (_query: string, _bindValues?: ParamsObject): void => {}
24
+
25
+ select = async <T>(_query: string, _bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
26
+ return { results: [] }
27
+ }
28
+
29
+ getPersistedData = async (): Promise<Uint8Array> => {
30
+ return new Uint8Array()
31
+ }
32
+ }
@@ -0,0 +1,74 @@
1
+ import { getTraceParentHeader, makeNoopTracer } from '@livestore/utils'
2
+ import type * as otel from '@opentelemetry/api'
3
+ import { invoke } from '@tauri-apps/api/tauri'
4
+
5
+ import type { ParamsObject } from '../util.js'
6
+ import { prepareBindValues } from '../util.js'
7
+ import { BaseBackend } from './base.js'
8
+ import type { SelectResponse } from './index.js'
9
+
10
+ export type BackendOptionsTauri = {
11
+ type: 'tauri'
12
+ dbDirPath: string
13
+ appDbFileName: string
14
+ otelTracer?: otel.Tracer
15
+ parentSpan: otel.Span
16
+ }
17
+
18
+ export class TauriBackend extends BaseBackend {
19
+ constructor(
20
+ readonly dbFilePath: string,
21
+ readonly dbDirPath: string,
22
+ readonly otelTracer: otel.Tracer,
23
+ readonly parentSpan: otel.Span,
24
+ ) {
25
+ super()
26
+ }
27
+
28
+ static load = async ({
29
+ dbDirPath,
30
+ appDbFileName,
31
+ otelTracer,
32
+ parentSpan,
33
+ }: BackendOptionsTauri): Promise<TauriBackend> => {
34
+ const dbFilePath = `${dbDirPath}/${appDbFileName}`
35
+ await invoke('initialize_connection', { dbName: dbFilePath, otelData: getOtelData_(parentSpan) })
36
+
37
+ return new TauriBackend(dbFilePath, dbDirPath, otelTracer ?? makeNoopTracer(), parentSpan)
38
+ }
39
+
40
+ execute = (query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void => {
41
+ // console.log({ query, bindValues, prepared: prepareBindValues(bindValues ?? {}, query) })
42
+ void invoke('execute', {
43
+ dbName: this.dbFilePath,
44
+ query,
45
+ values: prepareBindValues(bindValues ?? {}, query),
46
+ otelData: this.getOtelData(parentSpan),
47
+ })
48
+ }
49
+
50
+ select = async <T>(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): Promise<SelectResponse<T>> => {
51
+ return invoke('select', {
52
+ db: this.dbFilePath,
53
+ query,
54
+ values: bindValues ?? {},
55
+ otelData: this.getOtelData(parentSpan),
56
+ })
57
+ }
58
+
59
+ getPersistedData = async (parentSpan?: otel.Span): Promise<Uint8Array> => {
60
+ const headers = new Headers()
61
+ headers.set('traceparent', getTraceParentHeader(parentSpan ?? this.parentSpan))
62
+
63
+ return fetch(`http://localhost:38787/get-persisted-data?file_path=${this.dbFilePath}`, { headers }).then(
64
+ (response) => response.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
65
+ )
66
+ }
67
+
68
+ private getOtelData = (parentSpan?: otel.Span) => getOtelData_(parentSpan ?? this.parentSpan)!
69
+ }
70
+
71
+ const getOtelData_ = (parentSpan: otel.Span | undefined) => {
72
+ const spanContext = parentSpan?.spanContext()
73
+ return spanContext ? { trace_id: spanContext.traceId, span_id: spanContext.spanId } : undefined
74
+ }
@@ -0,0 +1,71 @@
1
+ /* eslint-disable unicorn/prefer-add-event-listener */
2
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
3
+
4
+ export class IDB {
5
+ private db: IDBDatabase | null = null
6
+
7
+ constructor(
8
+ private dbName: string,
9
+ private storeName: string = 'binary_store',
10
+ ) {}
11
+
12
+ private async open(): Promise<IDBDatabase> {
13
+ if (this.db) return this.db
14
+
15
+ return new Promise((resolve, reject) => {
16
+ const openRequest = indexedDB.open(this.dbName, 1)
17
+
18
+ openRequest.onupgradeneeded = () => {
19
+ const db = openRequest.result
20
+ if (!db.objectStoreNames.contains(this.storeName)) {
21
+ db.createObjectStore(this.storeName)
22
+ }
23
+ }
24
+
25
+ openRequest.onsuccess = () => {
26
+ this.db = openRequest.result
27
+ resolve(this.db)
28
+ }
29
+
30
+ openRequest.onerror = () => {
31
+ reject(new Error('Failed to open database.'))
32
+ }
33
+ })
34
+ }
35
+
36
+ public async get(key: string): Promise<Uint8Array | undefined> {
37
+ const db = await this.open()
38
+
39
+ return new Promise((resolve, reject) => {
40
+ const transaction = db.transaction(this.storeName, 'readonly')
41
+ const store = transaction.objectStore(this.storeName)
42
+ const getRequest = store.get(key)
43
+
44
+ getRequest.onsuccess = () => {
45
+ resolve(getRequest.result)
46
+ }
47
+
48
+ getRequest.onerror = () => {
49
+ reject(new Error('Failed to get data.'))
50
+ }
51
+ })
52
+ }
53
+
54
+ public async put(key: string, value: Uint8Array): Promise<void> {
55
+ const db = await this.open()
56
+
57
+ return new Promise((resolve, reject) => {
58
+ const transaction = db.transaction(this.storeName, 'readwrite')
59
+ const store = transaction.objectStore(this.storeName)
60
+ const putRequest = store.put(value, key)
61
+
62
+ putRequest.onsuccess = () => {
63
+ resolve()
64
+ }
65
+
66
+ putRequest.onerror = () => {
67
+ reject(new Error('Failed to write data.'))
68
+ }
69
+ })
70
+ }
71
+ }
@@ -0,0 +1,65 @@
1
+ import { makeNoopTracer } from '@livestore/utils'
2
+ import type * as otel from '@opentelemetry/api'
3
+ import type * as SqliteWasm from 'sqlite-esm'
4
+ import sqlite3InitModule from 'sqlite-esm'
5
+
6
+ import type { ParamsObject } from '../util.js'
7
+ import { prepareBindValues } from '../util.js'
8
+ import { BaseBackend } from './base.js'
9
+ import type { SelectResponse } from './index.js'
10
+
11
+ export type BackendOptionsWebInMemory = {
12
+ type: 'web-in-memory'
13
+ /** Specifies where to persist data for this backend */
14
+ otelTracer?: otel.Tracer
15
+ }
16
+
17
+ declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
18
+
19
+ // NOTE: This backend is currently only used for testing
20
+ export class WebInMemoryBackend extends BaseBackend {
21
+ constructor(
22
+ readonly otelTracer: otel.Tracer,
23
+ readonly db: DatabaseWithCAPI,
24
+ ) {
25
+ super()
26
+ }
27
+
28
+ static load = async (options: BackendOptionsWebInMemory): Promise<WebInMemoryBackend> => {
29
+ const sqlite3 = await sqlite3InitModule({
30
+ print: (message) => console.log(`[sql-client] ${message}`),
31
+ printErr: (message) => console.error(`[sql-client] ${message}`),
32
+ })
33
+ const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
34
+ db.capi = sqlite3.capi
35
+
36
+ return new WebInMemoryBackend(options.otelTracer ?? makeNoopTracer(), db)
37
+ }
38
+
39
+ execute = (query: string, bindValues?: ParamsObject): void => {
40
+ this.db.exec({
41
+ sql: query,
42
+ bind: prepareBindValues(bindValues ?? {}, query) as TODO,
43
+ returnValue: 'resultRows',
44
+ rowMode: 'object',
45
+ })
46
+ }
47
+
48
+ select = async <T>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
49
+ const resultRows: T[] = []
50
+
51
+ this.db.exec({
52
+ sql: query,
53
+ bind: prepareBindValues(bindValues ?? {}, query) as TODO,
54
+ rowMode: 'object',
55
+ resultRows,
56
+ // callback: (row: any) => console.log('select result', db.filename, query, row),
57
+ } as TODO)
58
+
59
+ return { results: resultRows }
60
+ }
61
+
62
+ getPersistedData = async (): Promise<Uint8Array> => {
63
+ return this.db.capi.sqlite3_js_db_export(this.db.pointer)
64
+ }
65
+ }
@@ -0,0 +1,176 @@
1
+ // Web Worker file for running SQLite in a web worker.
2
+
3
+ // TODO: create types for these libraries? SQL.js already should have types;
4
+ // we just need the types to apply to the fork.
5
+ import { shouldNeverHappen } from '@livestore/utils'
6
+ import * as Comlink from 'comlink'
7
+ import type * as SqliteWasm from 'sqlite-esm'
8
+ import sqlite3InitModule from 'sqlite-esm'
9
+
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'
15
+
16
+ // A global variable to hold the database connection.
17
+ // let db: SqliteWasm.Database
18
+ let db: SqliteWasm.DatabaseApi
19
+
20
+ let sqlite3: SqliteWasm.Sqlite3Static
21
+
22
+ // TODO get rid of this in favour of a "proper" IDB SQLite backend
23
+ let idb: IDB | undefined
24
+
25
+ /** The location where this database backend persists its data */
26
+ let persistentDatabaseLocation_: WritableDatabaseLocation
27
+
28
+ const configureConnection = () =>
29
+ db.exec(sql`
30
+ PRAGMA page_size=8192;
31
+ PRAGMA journal_mode=MEMORY;
32
+ PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
33
+ `)
34
+
35
+ /** A full virtual filename in the IDB FS */
36
+ const fullyQualifiedFilename = (name: string) => `${name}.db`
37
+
38
+ const initialize = async ({ persistentDatabaseLocation }: { persistentDatabaseLocation: WritableDatabaseLocation }) => {
39
+ persistentDatabaseLocation_ = persistentDatabaseLocation
40
+
41
+ sqlite3 = await sqlite3InitModule({
42
+ print: (message) => console.log(`[sql-client] ${message}`),
43
+ printErr: (message) => console.error(`[sql-client] ${message}`),
44
+ })
45
+
46
+ switch (persistentDatabaseLocation.type) {
47
+ case 'opfs': {
48
+ try {
49
+ db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(persistentDatabaseLocation.virtualFilename)) // , 'c'
50
+ } catch (e) {
51
+ debugger
52
+ }
53
+ break
54
+ }
55
+ case 'indexeddb': {
56
+ try {
57
+ db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' })
58
+ idb = new IDB(persistentDatabaseLocation.virtualFilename)
59
+
60
+ const bytes = await idb.get('db')
61
+
62
+ if (bytes !== undefined) {
63
+ // Based on https://sqlite.org/forum/forumpost/2119230da8ac5357a13b731f462dc76e08621a4a29724f7906d5f35bb8508465
64
+ // TODO find cleaner way to do this once possible in sqlite3-wasm
65
+ const p = sqlite3.wasm.allocFromTypedArray(bytes)
66
+ const _rc = sqlite3.capi.sqlite3_deserialize(db.pointer, 'main', p, bytes.length, bytes.length, 0)
67
+ }
68
+ } catch (e) {
69
+ debugger
70
+ }
71
+ break
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
+ default: {
80
+ casesHandled(persistentDatabaseLocation)
81
+ }
82
+ }
83
+
84
+ configureConnection()
85
+ }
86
+
87
+ // TODO get rid of this in favour of a "proper" IDB SQLite backend
88
+ let idbPersistTimeout: NodeJS.Timeout | undefined
89
+
90
+ type ExecutionQueueItem = { query: string; bindValues?: Bindable }
91
+
92
+ const executeBulk = (executionItems: ExecutionQueueItem[]): void => {
93
+ let batchItems: ExecutionQueueItem[] = []
94
+
95
+ while (executionItems.length > 0) {
96
+ try {
97
+ db.exec('BEGIN TRANSACTION') // Start the transaction
98
+
99
+ batchItems = executionItems.splice(0, 50)
100
+
101
+ for (const { query, bindValues } of batchItems) {
102
+ db.exec({
103
+ sql: query,
104
+ bind: bindValues as TODO,
105
+ returnValue: 'resultRows',
106
+ rowMode: 'object',
107
+ })
108
+ }
109
+
110
+ db.exec('COMMIT') // Commit the transaction
111
+ } catch (error) {
112
+ try {
113
+ db.exec('ROLLBACK') // Rollback in case of an error
114
+ } catch (e) {
115
+ console.error('Error rolling back transaction', e)
116
+ }
117
+
118
+ shouldNeverHappen(`Error executing query: ${error} \n ${JSON.stringify(batchItems)}`)
119
+ }
120
+ }
121
+
122
+ // TODO get rid of this in favour of a "proper" IDB SQLite backend
123
+ if (persistentDatabaseLocation_.type === 'indexeddb') {
124
+ if (idbPersistTimeout !== undefined) {
125
+ clearTimeout(idbPersistTimeout)
126
+ }
127
+
128
+ idbPersistTimeout = setTimeout(() => {
129
+ const data = sqlite3.capi.sqlite3_js_db_export(db.pointer) as Uint8Array
130
+
131
+ void idb!.put('db', data)
132
+ }, 1000)
133
+ }
134
+ }
135
+
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 }
166
+
167
+ export type WrappedWorker = typeof wrappedWorker
168
+
169
+ Comlink.expose(wrappedWorker)
170
+
171
+ // NOTE keep this around for debugging
172
+ // db.exec({
173
+ // sql: `select * from sqlite_master where name = 'library_tracks'`,
174
+ // callback: (_: TODO) => console.log(_),
175
+ // rowMode: 'object',
176
+ // } as TODO)
@@ -0,0 +1,96 @@
1
+ import { makeNoopTracer } 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 { BaseBackend } from './base.js'
8
+ import type { SelectResponse, WritableDatabaseLocation } from './index.js'
9
+ import type { WrappedWorker } from './web-worker.js'
10
+
11
+ export type BackendOptionsWeb = {
12
+ type: 'web'
13
+ /** Specifies where to persist data for this backend */
14
+ persistentDatabaseLocation: WritableDatabaseLocation
15
+ otelTracer?: otel.Tracer
16
+ }
17
+
18
+ export class WebWorkerBackend extends BaseBackend {
19
+ worker: Comlink.Remote<WrappedWorker>
20
+ persistentDatabaseLocation: WritableDatabaseLocation
21
+ otelTracer: otel.Tracer
22
+
23
+ executionBacklog: { query: string; bindValues?: ParamsObject }[] = []
24
+ executionPromise: Promise<void> | undefined = undefined
25
+
26
+ private constructor({
27
+ worker,
28
+ persistentDatabaseLocation,
29
+ otelTracer,
30
+ }: {
31
+ worker: Comlink.Remote<WrappedWorker>
32
+ persistentDatabaseLocation: WritableDatabaseLocation
33
+ otelTracer: otel.Tracer
34
+ }) {
35
+ super()
36
+ this.worker = worker
37
+ this.persistentDatabaseLocation = persistentDatabaseLocation
38
+ this.otelTracer = otelTracer
39
+ }
40
+
41
+ static load = async ({ persistentDatabaseLocation, otelTracer }: BackendOptionsWeb): Promise<WebWorkerBackend> => {
42
+ // TODO: Importing the worker like this only works with Vite;
43
+ // should this really be inside the LiveStore library?
44
+ // Doesn't work with Firefox right now during dev https://bugzilla.mozilla.org/show_bug.cgi?id=1247687
45
+ const worker = new Worker(new URL('./web-worker.js', import.meta.url), {
46
+ type: 'module',
47
+ })
48
+ const wrappedWorker = Comlink.wrap<WrappedWorker>(worker)
49
+
50
+ await wrappedWorker.initialize({ persistentDatabaseLocation })
51
+
52
+ return new WebWorkerBackend({
53
+ worker: wrappedWorker,
54
+ persistentDatabaseLocation,
55
+ otelTracer: otelTracer ?? makeNoopTracer(),
56
+ })
57
+ }
58
+
59
+ execute = (query: string, bindValues_?: ParamsObject) => {
60
+ const bindValues = prepareBindValues(bindValues_ ?? {}, query)
61
+ this.executionBacklog.push({ query, bindValues })
62
+
63
+ // 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)
64
+ if (this.executionPromise === undefined) {
65
+ this.executionPromise = new Promise((resolve) => {
66
+ setTimeout(() => {
67
+ void this.worker.executeBulk(this.executionBacklog)
68
+ this.executionBacklog = []
69
+ this.executionPromise = undefined
70
+
71
+ resolve()
72
+ }, 10)
73
+ })
74
+ }
75
+ }
76
+
77
+ select = async <T>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
78
+ // NOTE we need to wait for the executionBacklog to be worked off, before we run the select query (as it might depend on the previous execution queries)
79
+ await this.executionPromise
80
+
81
+ try {
82
+ const response = (await this.worker.select(query, bindValues)) as SelectResponse<T>
83
+ return response
84
+ } catch (e) {
85
+ console.error(`Error while executing query via "select": ${query}`)
86
+ throw e
87
+ }
88
+ }
89
+
90
+ getPersistedData = async (_parentSpan?: otel.Span): Promise<Uint8Array> => {
91
+ // NOTE we need to wait for the executionBacklog to be worked off
92
+ await this.executionPromise
93
+
94
+ return this.worker.getPersistedData()
95
+ }
96
+ }