@livestore/livestore 0.0.10 → 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 (151) hide show
  1. package/README.md +7 -7
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts +4 -120
  4. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  5. package/dist/__tests__/react/fixture.js +19 -26
  6. package/dist/__tests__/react/fixture.js.map +1 -1
  7. package/dist/__tests__/reactive.test.js +31 -0
  8. package/dist/__tests__/reactive.test.js.map +1 -1
  9. package/dist/backends/base.d.ts +4 -4
  10. package/dist/backends/{web-in-memory.d.ts → in-memory/index.d.ts} +6 -6
  11. package/dist/backends/in-memory/index.d.ts.map +1 -0
  12. package/dist/backends/{web-in-memory.js → in-memory/index.js} +7 -7
  13. package/dist/backends/in-memory/index.js.map +1 -0
  14. package/dist/backends/index.d.ts +4 -8
  15. package/dist/backends/index.d.ts.map +1 -1
  16. package/dist/backends/index.js +0 -22
  17. package/dist/backends/index.js.map +1 -1
  18. package/dist/backends/{tauri.d.ts → tauri/index.d.ts} +5 -6
  19. package/dist/backends/tauri/index.d.ts.map +1 -0
  20. package/dist/backends/{tauri.js → tauri/index.js} +4 -4
  21. package/dist/backends/tauri/index.js.map +1 -0
  22. package/dist/backends/{web.d.ts → web-worker/index.d.ts} +6 -7
  23. package/dist/backends/web-worker/index.d.ts.map +1 -0
  24. package/dist/backends/{web.js → web-worker/index.js} +6 -6
  25. package/dist/backends/web-worker/index.js.map +1 -0
  26. package/dist/backends/{web-worker.d.ts → web-worker/worker.d.ts} +3 -3
  27. package/dist/backends/web-worker/worker.d.ts.map +1 -0
  28. package/dist/backends/{web-worker.js → web-worker/worker.js} +3 -3
  29. package/dist/backends/web-worker/worker.js.map +1 -0
  30. package/dist/effect/LiveStore.d.ts +6 -6
  31. package/dist/effect/LiveStore.d.ts.map +1 -1
  32. package/dist/effect/LiveStore.js +2 -5
  33. package/dist/effect/LiveStore.js.map +1 -1
  34. package/dist/events.d.ts +1 -1
  35. package/dist/events.d.ts.map +1 -1
  36. package/dist/events.js +1 -1
  37. package/dist/events.js.map +1 -1
  38. package/dist/inMemoryDatabase.d.ts +5 -10
  39. package/dist/inMemoryDatabase.d.ts.map +1 -1
  40. package/dist/inMemoryDatabase.js +78 -89
  41. package/dist/inMemoryDatabase.js.map +1 -1
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/migrations.d.ts +9 -0
  47. package/dist/migrations.d.ts.map +1 -0
  48. package/dist/migrations.js +62 -0
  49. package/dist/migrations.js.map +1 -0
  50. package/dist/otel.d.ts +0 -1
  51. package/dist/otel.d.ts.map +1 -1
  52. package/dist/otel.js +0 -11
  53. package/dist/otel.js.map +1 -1
  54. package/dist/react/LiveStoreProvider.d.ts +5 -4
  55. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  56. package/dist/react/LiveStoreProvider.js +6 -5
  57. package/dist/react/LiveStoreProvider.js.map +1 -1
  58. package/dist/react/index.d.ts +2 -1
  59. package/dist/react/index.d.ts.map +1 -1
  60. package/dist/react/index.js.map +1 -1
  61. package/dist/react/useGlobalQuery.d.ts.map +1 -1
  62. package/dist/react/useGlobalQuery.js +0 -2
  63. package/dist/react/useGlobalQuery.js.map +1 -1
  64. package/dist/react/useLiveStoreComponent.d.ts +22 -17
  65. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  66. package/dist/react/useLiveStoreComponent.js +46 -17
  67. package/dist/react/useLiveStoreComponent.js.map +1 -1
  68. package/dist/reactive.d.ts.map +1 -1
  69. package/dist/reactive.js +1 -0
  70. package/dist/reactive.js.map +1 -1
  71. package/dist/schema.d.ts +32 -112
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +36 -79
  74. package/dist/schema.js.map +1 -1
  75. package/dist/storage/base.d.ts +10 -0
  76. package/dist/storage/base.d.ts.map +1 -0
  77. package/dist/storage/base.js +14 -0
  78. package/dist/storage/base.js.map +1 -0
  79. package/dist/storage/in-memory/index.d.ts +15 -0
  80. package/dist/storage/in-memory/index.d.ts.map +1 -0
  81. package/dist/storage/in-memory/index.js +14 -0
  82. package/dist/storage/in-memory/index.js.map +1 -0
  83. package/dist/storage/index.d.ts +14 -0
  84. package/dist/storage/index.d.ts.map +1 -0
  85. package/dist/storage/index.js +9 -0
  86. package/dist/storage/index.js.map +1 -0
  87. package/dist/storage/tauri/index.d.ts +19 -0
  88. package/dist/storage/tauri/index.d.ts.map +1 -0
  89. package/dist/storage/tauri/index.js +38 -0
  90. package/dist/storage/tauri/index.js.map +1 -0
  91. package/dist/storage/utils/idb.d.ts +10 -0
  92. package/dist/storage/utils/idb.d.ts.map +1 -0
  93. package/dist/storage/utils/idb.js +58 -0
  94. package/dist/storage/utils/idb.js.map +1 -0
  95. package/dist/storage/web-worker/index.d.ts +27 -0
  96. package/dist/storage/web-worker/index.d.ts.map +1 -0
  97. package/dist/storage/web-worker/index.js +76 -0
  98. package/dist/storage/web-worker/index.js.map +1 -0
  99. package/dist/storage/web-worker/worker.d.ts +13 -0
  100. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  101. package/dist/storage/web-worker/worker.js +110 -0
  102. package/dist/storage/web-worker/worker.js.map +1 -0
  103. package/dist/store.d.ts +6 -6
  104. package/dist/store.d.ts.map +1 -1
  105. package/dist/store.js +93 -63
  106. package/dist/store.js.map +1 -1
  107. package/dist/util.d.ts +3 -1
  108. package/dist/util.d.ts.map +1 -1
  109. package/dist/util.js +2 -0
  110. package/dist/util.js.map +1 -1
  111. package/package.json +50 -23
  112. package/src/__tests__/react/fixture.tsx +19 -28
  113. package/src/__tests__/reactive.test.ts +39 -0
  114. package/src/effect/LiveStore.ts +8 -13
  115. package/src/events.ts +1 -1
  116. package/src/inMemoryDatabase.ts +100 -117
  117. package/src/index.ts +10 -16
  118. package/src/migrations.ts +101 -0
  119. package/src/otel.ts +0 -11
  120. package/src/react/LiveStoreProvider.tsx +12 -8
  121. package/src/react/index.ts +9 -0
  122. package/src/react/useGlobalQuery.ts +0 -3
  123. package/src/react/useLiveStoreComponent.ts +98 -38
  124. package/src/reactive.ts +2 -1
  125. package/src/schema.ts +72 -145
  126. package/src/storage/in-memory/index.ts +21 -0
  127. package/src/storage/index.ts +27 -0
  128. package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
  129. package/src/storage/web-worker/index.ts +118 -0
  130. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  131. package/src/store.ts +112 -79
  132. package/src/util.ts +5 -1
  133. package/tsconfig.json +1 -3
  134. package/dist/backends/noop.d.ts +0 -18
  135. package/dist/backends/noop.d.ts.map +0 -1
  136. package/dist/backends/noop.js +0 -21
  137. package/dist/backends/noop.js.map +0 -1
  138. package/dist/backends/tauri.d.ts.map +0 -1
  139. package/dist/backends/tauri.js.map +0 -1
  140. package/dist/backends/web-in-memory.d.ts.map +0 -1
  141. package/dist/backends/web-in-memory.js.map +0 -1
  142. package/dist/backends/web-worker.d.ts.map +0 -1
  143. package/dist/backends/web-worker.js.map +0 -1
  144. package/dist/backends/web.d.ts.map +0 -1
  145. package/dist/backends/web.js.map +0 -1
  146. package/src/backends/base.ts +0 -67
  147. package/src/backends/index.ts +0 -98
  148. package/src/backends/noop.ts +0 -32
  149. package/src/backends/web-in-memory.ts +0 -65
  150. package/src/backends/web.ts +0 -97
  151. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -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
 
package/src/store.ts CHANGED
@@ -1,28 +1,30 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
2
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
3
+ import { identity } from '@livestore/utils/effect'
3
4
  import * as otel from '@opentelemetry/api'
4
5
  import type { GraphQLSchema } from 'graphql'
5
6
  import * as graphql from 'graphql'
6
7
  import { uniqueId } from 'lodash-es'
7
8
  import * as ReactDOM from 'react-dom'
9
+ import initSqlite3Wasm from 'sqlite-esm'
8
10
  import { v4 as uuid } from 'uuid'
9
11
 
10
- import type { Backend, BackendOptions } from './backends/index.js'
11
- import { createBackend } from './backends/index.js'
12
12
  import type { ComponentKey } from './componentKey.js'
13
13
  import { tableNameForComponentKey } from './componentKey.js'
14
14
  import type { LiveStoreEvent } from './events.js'
15
15
  import { InMemoryDatabase } from './inMemoryDatabase.js'
16
+ import { migrateDb } from './migrations.js'
16
17
  import { getDurationMsFromSpan } from './otel.js'
17
18
  import type { GetAtom, Ref } from './reactive.js'
18
19
  import { ReactiveGraph } from './reactive.js'
19
20
  import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
20
21
  import { LiveStoreJSQuery } from './reactiveQueries/js.js'
21
22
  import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
22
- import type { ActionDefinition, GetActionArgs, Schema } from './schema.js'
23
- import { componentStateTables, loadSchema } from './schema.js'
23
+ import type { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
24
+ import { componentStateTables } from './schema.js'
25
+ import type { Storage, StorageInit } from './storage/index.js'
24
26
  import type { Bindable, ParamsObject } from './util.js'
25
- import { sql } from './util.js'
27
+ import { isPromise, sql } from './util.js'
26
28
 
27
29
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
28
30
  | LiveStoreSQLQuery<TResult>
@@ -55,7 +57,7 @@ export type GraphQLOptions<TContext> = {
55
57
  export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
56
58
  db: InMemoryDatabase
57
59
  schema: Schema
58
- backend?: Backend
60
+ storage?: Storage
59
61
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
60
62
  otelTracer: otel.Tracer
61
63
  otelRootSpanContext: otel.Context
@@ -111,13 +113,13 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
111
113
  */
112
114
  tableRefs: { [key: string]: Ref<null> }
113
115
  activeQueries: Set<LiveStoreQuery>
114
- backend?: Backend
116
+ storage?: Storage
115
117
  temporaryQueries: Set<LiveStoreQuery> | undefined
116
118
 
117
119
  private constructor({
118
120
  db,
119
121
  schema,
120
- backend,
122
+ storage,
121
123
  graphQLOptions,
122
124
  otelTracer,
123
125
  otelRootSpanContext,
@@ -133,7 +135,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
133
135
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
134
136
  this.tableRefs = {}
135
137
  this.activeQueries = new Set()
136
- this.backend = backend
138
+ this.storage = storage
137
139
 
138
140
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
139
141
  const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan)
@@ -642,7 +644,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
642
644
  try {
643
645
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
644
646
 
645
- // TODO: what to do about backend transaction here?
647
+ // TODO: what to do about storage transaction here?
646
648
  this.inMemoryDB.txn(() => {
647
649
  for (const event of events) {
648
650
  try {
@@ -757,6 +759,15 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
757
759
  }
758
760
  },
759
761
  },
762
+
763
+ RawSql: {
764
+ statement: ({ sql, writeTables }: { sql: string; writeTables: string[] }) => ({
765
+ sql,
766
+ writeTables,
767
+ argsAlreadyBound: false,
768
+ }),
769
+ prepareBindValues: ({ bindValues }) => bindValues,
770
+ },
760
771
  }
761
772
 
762
773
  const actionDefinition = actionDefinitions[eventType] ?? shouldNeverHappen(`Unknown event type: ${eventType}`)
@@ -765,20 +776,25 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
765
776
  const eventWithId: LiveStoreEvent = { id: uuid(), type: eventType, args }
766
777
 
767
778
  // Synchronously apply the event to the in-memory database
768
- const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
779
+ // const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
780
+ const { statement, bindValues } = eventToSql(eventWithId, actionDefinition)
781
+ const { durationMs } = this.inMemoryDB.execute(statement.sql, bindValues, statement.writeTables, {
782
+ otelContext,
783
+ })
769
784
 
770
- // Asynchronously apply the event to a persistent backend (we're not awaiting this promise here)
771
- if (this.backend !== undefined) {
772
- this.backend.applyEvent(eventWithId, actionDefinition, span)
785
+ // Asynchronously apply the event to a persistent storage (we're not awaiting this promise here)
786
+ if (this.storage !== undefined) {
787
+ // this.storage.applyEvent(eventWithId, actionDefinition, span)
788
+ this.storage.execute(statement.sql, bindValues, span)
773
789
  }
774
790
 
775
791
  // Uncomment to print a list of queries currently registered on the store
776
792
  // console.log(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
777
793
 
778
- const statement =
779
- typeof actionDefinition.statement === 'function'
780
- ? actionDefinition.statement(args)
781
- : actionDefinition.statement
794
+ // const statement =
795
+ // typeof actionDefinition.statement === 'function'
796
+ // ? actionDefinition.statement(args)
797
+ // : actionDefinition.statement
782
798
 
783
799
  span.end()
784
800
 
@@ -795,9 +811,9 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
795
811
  execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
796
812
  this.inMemoryDB.execute(query, params, writeTables)
797
813
 
798
- if (this.backend !== undefined) {
814
+ if (this.storage !== undefined) {
799
815
  const parentSpan = otel.trace.getSpan(otel.context.active())
800
- this.backend.execute(query, params, parentSpan)
816
+ this.storage.execute(query, params, parentSpan)
801
817
  }
802
818
  }
803
819
  }
@@ -805,83 +821,100 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
805
821
  /** Create a new LiveStore Store */
806
822
  export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
807
823
  schema,
808
- backendOptions,
824
+ loadStorage,
809
825
  graphQLOptions,
810
826
  otelTracer = makeNoopTracer(),
811
827
  otelRootSpanContext = otel.context.active(),
812
828
  boot,
813
829
  }: {
814
830
  schema: Schema
815
- backendOptions: BackendOptions
831
+ loadStorage: () => StorageInit | Promise<StorageInit>
816
832
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
817
833
  otelTracer?: otel.Tracer
818
834
  otelRootSpanContext?: otel.Context
819
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
835
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
820
836
  }): Promise<Store<TGraphQLContext>> => {
821
837
  return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
822
838
  try {
823
- let persistedData: Uint8Array | undefined
824
- const backend = await createBackend(backendOptions, {
825
- otelTracer: otelTracer ?? makeNoopTracer(),
826
- parentSpan: otel.trace.getSpan(otelRootSpanContext ?? otel.context.active()) ?? makeNoopSpan(),
827
- })
828
- // if we're resetting the database, run boot here.
839
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
829
840
 
830
- let shouldResetDB = false
831
- // Uncomment this line if you want to reset the database contents.
832
- // let shouldResetDB = true
841
+ const loadStorageAndPersistedData = async () => {
842
+ const storage = await otelTracer.startActiveSpan('storage:load', {}, otelContext, async (span) => {
843
+ try {
844
+ const init = await loadStorage()
845
+ const parentSpan = otel.trace.getSpan(otel.context.active()) ?? makeNoopSpan()
846
+ return init({ otelTracer, parentSpan })
847
+ } finally {
848
+ span.end()
849
+ }
850
+ })
833
851
 
834
- const existingTablesRaw = await backend.select(
835
- sql`SELECT * FROM sqlite_master WHERE type='table';`,
836
- undefined,
837
- span,
838
- )
839
- const existingTables = existingTablesRaw.results.map((t: { name: string }) => t.name)
840
- const missingTables = Object.keys(schema.tables).filter((tableName) => !existingTables.includes(tableName))
841
- if (existingTables.length === 0) {
842
- console.log('No existing tables found, loading from schema')
843
- shouldResetDB = true
844
- } else if (
845
- missingTables.length > 0 &&
846
- window.confirm(
847
- `Existing DB is missing ${missingTables.length} tables: ${missingTables.join(
848
- ', ',
849
- )}\n\nReset DB? This will reset all of the following tables to empty: ${Object.keys(schema).join(', ')}`,
852
+ const persistedData = await otelTracer.startActiveSpan(
853
+ 'storage:getPersistedData',
854
+ {},
855
+ otelContext,
856
+ async (span) => {
857
+ try {
858
+ return await storage.getPersistedData(span)
859
+ } finally {
860
+ span.end()
861
+ }
862
+ },
850
863
  )
851
- ) {
852
- shouldResetDB = true
853
- }
854
864
 
855
- if (localStorage.getItem(RESET_DB_LOCAL_STORAGE_KEY) !== null) {
856
- shouldResetDB = true
865
+ return { storage, persistedData }
857
866
  }
858
867
 
859
- if (shouldResetDB) {
860
- await loadSchema(backend, schema)
861
- localStorage.removeItem(RESET_DB_LOCAL_STORAGE_KEY)
862
- }
868
+ const loadSqlite3 = () =>
869
+ initSqlite3Wasm({
870
+ // Required to load the wasm binary asynchronously. Of course, you can host it wherever you want
871
+ // You can omit locateFile completely when running in node
872
+ // locateFile: () => `/sql-wasm.wasm`,
873
+ print: (message) => console.log(`[livestore sqlite] ${message}`),
874
+ printErr: (message) => console.error(`[livestore sqlite] ${message}`),
875
+ })
863
876
 
864
- if (boot) {
865
- await boot(backend!, span)
866
- }
877
+ const [{ storage, persistedData }, sqlite3] = await Promise.all([loadStorageAndPersistedData(), loadSqlite3()])
867
878
 
868
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
869
- await otelTracer.startActiveSpan('backend-getPersistedData', {}, otelContext, async (span) => {
879
+ const db = InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext, sqlite3)
880
+
881
+ // Proxy to `db` that also mirrors `execute` calls to `storage`
882
+ const dbProxy = new Proxy(db, {
883
+ get: (db, prop, receiver) => {
884
+ if (prop === 'execute') {
885
+ const execute: InMemoryDatabase['execute'] = (query, bindValues, writeTables, options) => {
886
+ storage.execute(query, bindValues, span)
887
+ return db.execute(query, bindValues, writeTables, options)
888
+ }
889
+ return execute
890
+ } else {
891
+ return Reflect.get(db, prop, receiver)
892
+ }
893
+ },
894
+ })
895
+
896
+ otelTracer.startActiveSpan('migrateDb', {}, otelContext, async (span) => {
870
897
  try {
871
- persistedData = await backend!.getPersistedData(span)
898
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
899
+ migrateDb({ db: dbProxy, schema, otelContext })
872
900
  } finally {
873
901
  span.end()
874
902
  }
875
903
  })
876
904
 
877
- const db: InMemoryDatabase = await InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext)
878
- configureSQLite(db)
905
+ if (boot !== undefined) {
906
+ const booting = boot(dbProxy, span)
907
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
908
+ if (isPromise(booting)) {
909
+ await booting
910
+ }
911
+ }
879
912
 
880
913
  // TODO: we can't apply the schema at this point, we've already loaded persisted data!
881
914
  // Think about what to do about this case.
882
915
  // await applySchema(db, schema)
883
916
  return Store.createStore<TGraphQLContext>(
884
- { db, schema, backend, graphQLOptions, otelTracer, otelRootSpanContext },
917
+ { db, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext },
885
918
  span,
886
919
  )
887
920
  } finally {
@@ -890,17 +923,17 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
890
923
  })
891
924
  }
892
925
 
893
- /** Set up SQLite performance; hasn't been super carefully optimized yet. */
894
- const configureSQLite = (db: InMemoryDatabase) => {
895
- db.execute(
896
- // TODO: revisit these tuning parameters for max performance
897
- sql`
898
- PRAGMA page_size=32768;
899
- PRAGMA cache_size=10000;
900
- PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
901
- PRAGMA synchronous='OFF';
902
- PRAGMA temp_store='MEMORY';
903
- PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
904
- `,
905
- )
926
+ const eventToSql = (
927
+ event: LiveStoreEvent,
928
+ eventDefinition: ActionDefinition,
929
+ ): { statement: SQLWriteStatement; bindValues: ParamsObject } => {
930
+ const statement =
931
+ typeof eventDefinition.statement === 'function' ? eventDefinition.statement(event.args) : eventDefinition.statement
932
+
933
+ const prepareBindValues = eventDefinition.prepareBindValues ?? identity
934
+
935
+ const bindValues =
936
+ typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound ? {} : prepareBindValues(event.args)
937
+
938
+ return { statement, bindValues }
906
939
  }
package/src/util.ts CHANGED
@@ -1,5 +1,7 @@
1
+ /// <reference lib="es2022" />
2
+
1
3
  export type ParamsObject = Record<string, SqlValue>
2
- export type SqlValue = string | number | Uint8Array
4
+ export type SqlValue = string | number | Uint8Array | null
3
5
 
4
6
  export type Bindable = SqlValue[] | ParamsObject
5
7
 
@@ -57,3 +59,5 @@ export const objectToString = (error: any): string => {
57
59
  return 'Error while printing error: ' + e
58
60
  }
59
61
  }
62
+
63
+ export const isPromise = (value: any): value is Promise<unknown> => typeof value?.then === 'function'
package/tsconfig.json CHANGED
@@ -9,7 +9,5 @@
9
9
  "tsBuildInfoFile": "./dist/.tsbuildinfo"
10
10
  },
11
11
  "include": ["./src"],
12
- "references": [
13
- { "path": "../utils" },
14
- ]
12
+ "references": [{ "path": "../../effect-db-schema" }, { "path": "../utils" }]
15
13
  }