@livestore/livestore 0.0.12 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +71 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/__tests__/react/fixture.d.ts +25 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -0
- package/dist/__tests__/react/fixture.js +60 -0
- package/dist/__tests__/react/fixture.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
- package/dist/__tests__/reactive.test.d.ts +2 -0
- package/dist/__tests__/reactive.test.d.ts.map +1 -0
- package/dist/__tests__/reactive.test.js +197 -0
- package/dist/__tests__/reactive.test.js.map +1 -0
- package/dist/bounded-collections.d.ts +34 -0
- package/dist/bounded-collections.d.ts.map +1 -0
- package/dist/bounded-collections.js +103 -0
- package/dist/bounded-collections.js.map +1 -0
- package/dist/componentKey.d.ts +20 -0
- package/dist/componentKey.d.ts.map +1 -0
- package/dist/componentKey.js +3 -0
- package/dist/componentKey.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +36 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +41 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +56 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +223 -0
- package/dist/inMemoryDatabase.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +16 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +67 -0
- package/dist/migrations.js.map +1 -0
- package/dist/otel.d.ts +4 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +6 -0
- package/dist/otel.js.map +1 -0
- package/dist/react/LiveStoreContext.d.ts +11 -0
- package/dist/react/LiveStoreContext.d.ts.map +1 -0
- package/dist/react/LiveStoreContext.js +10 -0
- package/dist/react/LiveStoreContext.js.map +1 -0
- package/dist/react/LiveStoreProvider.d.ts +20 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +52 -0
- package/dist/react/LiveStoreProvider.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +13 -0
- package/dist/react/useGraphQL.d.ts.map +1 -0
- package/dist/react/useGraphQL.js +85 -0
- package/dist/react/useGraphQL.js.map +1 -0
- package/dist/react/useLiveStoreComponent.d.ts +75 -0
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
- package/dist/react/useLiveStoreComponent.js +317 -0
- package/dist/react/useLiveStoreComponent.js.map +1 -0
- package/dist/react/useQuery.d.ts +3 -0
- package/dist/react/useQuery.d.ts.map +1 -0
- package/dist/react/useQuery.js +38 -0
- package/dist/react/useQuery.js.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
- package/dist/reactive.d.ts +140 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +302 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +27 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +23 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +25 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +18 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +19 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +13 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +31 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +32 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -0
- package/dist/storage/in-memory/index.d.ts +15 -0
- package/dist/storage/in-memory/index.d.ts.map +1 -0
- package/dist/storage/in-memory/index.js +14 -0
- package/dist/storage/in-memory/index.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/tauri/index.d.ts +19 -0
- package/dist/storage/tauri/index.d.ts.map +1 -0
- package/dist/storage/tauri/index.js +38 -0
- package/dist/storage/tauri/index.js.map +1 -0
- package/dist/storage/utils/idb.d.ts +10 -0
- package/dist/storage/utils/idb.d.ts.map +1 -0
- package/dist/storage/utils/idb.js +58 -0
- package/dist/storage/utils/idb.js.map +1 -0
- package/dist/storage/web-worker/index.d.ts +27 -0
- package/dist/storage/web-worker/index.d.ts.map +1 -0
- package/dist/storage/web-worker/index.js +74 -0
- package/dist/storage/web-worker/index.js.map +1 -0
- package/dist/storage/web-worker/worker.d.ts +13 -0
- package/dist/storage/web-worker/worker.d.ts.map +1 -0
- package/dist/storage/web-worker/worker.js +110 -0
- package/dist/storage/web-worker/worker.js.map +1 -0
- package/dist/store.d.ts +199 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +603 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +28 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +55 -0
- package/dist/util.js.map +1 -0
- package/package.json +46 -19
- package/src/__tests__/react/fixture.tsx +23 -32
- package/src/__tests__/reactive.test.ts +3 -4
- package/src/effect/LiveStore.ts +22 -31
- package/src/events.ts +1 -1
- package/src/inMemoryDatabase.ts +115 -140
- package/src/index.ts +20 -20
- package/src/migrations.ts +119 -0
- package/src/otel.ts +0 -11
- package/src/react/LiveStoreProvider.tsx +24 -23
- package/src/react/index.ts +10 -1
- package/src/react/useGraphQL.ts +28 -2
- package/src/react/useLiveStoreComponent.ts +134 -50
- package/src/react/useQuery.ts +56 -0
- package/src/reactive.ts +6 -4
- package/src/reactiveQueries/base-class.ts +9 -3
- package/src/reactiveQueries/graphql.ts +4 -4
- package/src/reactiveQueries/js.ts +2 -2
- package/src/reactiveQueries/sql.ts +6 -6
- package/src/schema.ts +69 -145
- package/src/storage/in-memory/index.ts +21 -0
- package/src/storage/index.ts +27 -0
- package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
- package/src/storage/web-worker/index.ts +116 -0
- package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
- package/src/store.ts +171 -98
- package/src/util.ts +13 -3
- package/tsconfig.json +1 -3
- package/src/backends/base.ts +0 -67
- package/src/backends/index.ts +0 -98
- package/src/backends/noop.ts +0 -32
- package/src/backends/web-in-memory.ts +0 -65
- package/src/backends/web.ts +0 -97
- package/src/react/useGlobalQuery.ts +0 -40
- /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/inMemoryDatabase.ts
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
2
2
|
|
|
3
3
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
4
|
-
import { identity } from '@livestore/utils/effect'
|
|
5
4
|
import type * as otel from '@opentelemetry/api'
|
|
6
|
-
import type * as
|
|
7
|
-
import initSqlJs from 'sqlite-esm'
|
|
5
|
+
import type * as Sqlite from 'sqlite-esm'
|
|
8
6
|
|
|
9
7
|
import BoundMap, { BoundArray } from './bounded-collections.js'
|
|
10
|
-
import type { LiveStoreEvent } from './events.js'
|
|
11
8
|
// import { EVENTS_TABLE_NAME } from './events.js'
|
|
12
9
|
import { sql } from './index.js'
|
|
13
10
|
import { getDurationMsFromSpan, getStartTimeHighResFromSpan } from './otel.js'
|
|
14
11
|
import QueryCache from './QueryCache.js'
|
|
15
|
-
import type {
|
|
16
|
-
import type { Bindable, ParamsObject } from './util.js'
|
|
17
|
-
import { prepareBindValues } from './util.js'
|
|
12
|
+
import type { Bindable, PreparedBindValues } from './util.js'
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
Basic = 'Basic',
|
|
21
|
-
FullText = 'FullText',
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Index {
|
|
25
|
-
indexType: IndexType
|
|
26
|
-
name: string
|
|
27
|
-
columns: string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
|
|
14
|
+
type DatabaseWithCAPI = Sqlite.Database & { capi: Sqlite.CAPI }
|
|
31
15
|
|
|
32
16
|
export interface DebugInfo {
|
|
33
17
|
slowQueries: BoundArray<SlowQueryInfo>
|
|
@@ -38,7 +22,7 @@ export interface DebugInfo {
|
|
|
38
22
|
|
|
39
23
|
export type SlowQueryInfo = [
|
|
40
24
|
queryStr: string,
|
|
41
|
-
bindValues:
|
|
25
|
+
bindValues: PreparedBindValues | undefined,
|
|
42
26
|
durationMs: number,
|
|
43
27
|
rowsCount: number | undefined,
|
|
44
28
|
queriedTables: string[],
|
|
@@ -54,7 +38,7 @@ export const emptyDebugInfo = (): DebugInfo => ({
|
|
|
54
38
|
|
|
55
39
|
export class InMemoryDatabase {
|
|
56
40
|
// TODO: how many unique active statements are expected?
|
|
57
|
-
private cachedStmts = new BoundMap<string,
|
|
41
|
+
private cachedStmts = new BoundMap<string, Sqlite.PreparedStatement>(200)
|
|
58
42
|
private tablesUsedCache = new BoundMap<string, string[]>(200)
|
|
59
43
|
private resultCache = new QueryCache()
|
|
60
44
|
public debugInfo: DebugInfo = emptyDebugInfo()
|
|
@@ -63,21 +47,21 @@ export class InMemoryDatabase {
|
|
|
63
47
|
private db: DatabaseWithCAPI,
|
|
64
48
|
private otelTracer: otel.Tracer,
|
|
65
49
|
private otelRootSpanContext: otel.Context,
|
|
66
|
-
public SQL:
|
|
50
|
+
public SQL: Sqlite.Sqlite3Static,
|
|
67
51
|
) {}
|
|
68
52
|
|
|
69
|
-
static
|
|
70
|
-
data
|
|
71
|
-
otelTracer
|
|
72
|
-
otelRootSpanContext
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
static load({
|
|
54
|
+
data,
|
|
55
|
+
otelTracer,
|
|
56
|
+
otelRootSpanContext,
|
|
57
|
+
sqlite3,
|
|
58
|
+
}: {
|
|
59
|
+
data: Uint8Array | undefined
|
|
60
|
+
otelTracer: otel.Tracer
|
|
61
|
+
otelRootSpanContext: otel.Context
|
|
62
|
+
sqlite3: Sqlite.Sqlite3Static
|
|
63
|
+
}): InMemoryDatabase {
|
|
64
|
+
// TODO move WASM init higher up in the init process (to do some other work while it's loading)
|
|
81
65
|
|
|
82
66
|
const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
|
|
83
67
|
db.capi = sqlite3.capi
|
|
@@ -97,7 +81,11 @@ export class InMemoryDatabase {
|
|
|
97
81
|
)
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
|
|
84
|
+
const inMemoryDatabase = new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
|
|
85
|
+
|
|
86
|
+
configureSQLite(inMemoryDatabase)
|
|
87
|
+
|
|
88
|
+
return inMemoryDatabase
|
|
101
89
|
}
|
|
102
90
|
|
|
103
91
|
txn<TRes>(callback: () => TRes): TRes {
|
|
@@ -141,56 +129,81 @@ export class InMemoryDatabase {
|
|
|
141
129
|
return tablesUsed as string[]
|
|
142
130
|
}
|
|
143
131
|
|
|
144
|
-
/**
|
|
145
|
-
* NOTE `execute` is untraced since it's usually called from `applyEvent` which is traced
|
|
146
|
-
*/
|
|
147
132
|
execute(
|
|
148
133
|
query: string,
|
|
149
|
-
bindValues?:
|
|
134
|
+
bindValues?: PreparedBindValues,
|
|
150
135
|
writeTables?: string[],
|
|
151
|
-
options?: { hasNoEffects?: boolean },
|
|
152
|
-
):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
136
|
+
options?: { hasNoEffects?: boolean; otelContext: otel.Context },
|
|
137
|
+
): { durationMs: number } {
|
|
138
|
+
return this.otelTracer.startActiveSpan(
|
|
139
|
+
'livestore.in-memory-db:execute',
|
|
140
|
+
// TODO truncate query string
|
|
141
|
+
{ attributes: { 'sql.query': query } },
|
|
142
|
+
options?.otelContext ?? this.otelRootSpanContext,
|
|
143
|
+
(span) => {
|
|
144
|
+
try {
|
|
145
|
+
let stmt = this.cachedStmts.get(query)
|
|
146
|
+
if (stmt === undefined) {
|
|
147
|
+
stmt = this.db.prepare(query)
|
|
148
|
+
this.cachedStmts.set(query, stmt)
|
|
149
|
+
}
|
|
159
150
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
|
|
152
|
+
stmt.bind(bindValues)
|
|
153
|
+
}
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
stmt.reset() // Reset is needed for next execution
|
|
168
|
-
}
|
|
169
|
-
} catch (error) {
|
|
170
|
-
shouldNeverHappen(
|
|
171
|
-
`Error executing query: ${error} \n ${JSON.stringify({
|
|
172
|
-
query,
|
|
173
|
-
bindValues,
|
|
174
|
-
})}`,
|
|
175
|
-
)
|
|
176
|
-
}
|
|
155
|
+
if (import.meta.env.DEV) {
|
|
156
|
+
this.debugInfo.events.push([query, bindValues])
|
|
157
|
+
}
|
|
177
158
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
159
|
+
try {
|
|
160
|
+
stmt.step()
|
|
161
|
+
} finally {
|
|
162
|
+
stmt.reset() // Reset is needed for next execution
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
shouldNeverHappen(
|
|
166
|
+
`Error executing query: ${error} \n ${JSON.stringify({
|
|
167
|
+
query,
|
|
168
|
+
bindValues,
|
|
169
|
+
})}`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
183
172
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
|
|
174
|
+
// TODO use write tables instead
|
|
175
|
+
// check what queries actually end up here.
|
|
176
|
+
this.resultCache.invalidate(writeTables ?? this.getTablesUsed(query))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
span.end()
|
|
180
|
+
|
|
181
|
+
const durationMs = getDurationMsFromSpan(span)
|
|
182
|
+
|
|
183
|
+
this.debugInfo.queryFrameDuration += durationMs
|
|
184
|
+
this.debugInfo.queryFrameCount++
|
|
185
|
+
|
|
186
|
+
if (durationMs > 5 && import.meta.env.DEV) {
|
|
187
|
+
this.debugInfo.slowQueries.push([
|
|
188
|
+
query,
|
|
189
|
+
bindValues,
|
|
190
|
+
durationMs,
|
|
191
|
+
undefined,
|
|
192
|
+
[],
|
|
193
|
+
getStartTimeHighResFromSpan(span),
|
|
194
|
+
])
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { durationMs }
|
|
198
|
+
},
|
|
199
|
+
)
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
select<T = any>(
|
|
190
203
|
query: string,
|
|
191
204
|
options?: {
|
|
192
205
|
queriedTables?: string[]
|
|
193
|
-
bindValues?:
|
|
206
|
+
bindValues?: PreparedBindValues
|
|
194
207
|
skipCache?: boolean
|
|
195
208
|
otelContext?: otel.Context
|
|
196
209
|
},
|
|
@@ -218,19 +231,27 @@ export class InMemoryDatabase {
|
|
|
218
231
|
stmt = this.db.prepare(query)
|
|
219
232
|
this.cachedStmts.set(query, stmt)
|
|
220
233
|
}
|
|
221
|
-
if (bindValues) {
|
|
222
|
-
stmt.bind(bindValues
|
|
234
|
+
if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
|
|
235
|
+
stmt.bind(bindValues)
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
const result: T[] = []
|
|
239
|
+
|
|
226
240
|
try {
|
|
227
|
-
|
|
241
|
+
// NOTE `getColumnNames` only works for `SELECT` statements, ignoring other statements for now
|
|
242
|
+
let columns = undefined
|
|
243
|
+
try {
|
|
244
|
+
columns = stmt.getColumnNames()
|
|
245
|
+
} catch (_e) {}
|
|
246
|
+
|
|
228
247
|
while (stmt.step()) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
248
|
+
if (columns !== undefined) {
|
|
249
|
+
const obj: { [key: string]: any } = {}
|
|
250
|
+
for (const [i, c] of columns.entries()) {
|
|
251
|
+
obj[c] = stmt.get(i)
|
|
252
|
+
}
|
|
253
|
+
result.push(obj as unknown as T)
|
|
232
254
|
}
|
|
233
|
-
result.push(obj as unknown as T)
|
|
234
255
|
}
|
|
235
256
|
} finally {
|
|
236
257
|
// we're caching statements in this iteration. do not free.
|
|
@@ -275,67 +296,6 @@ export class InMemoryDatabase {
|
|
|
275
296
|
)
|
|
276
297
|
}
|
|
277
298
|
|
|
278
|
-
// TODO move `applyEvent` logic to Store and only call `execute` here
|
|
279
|
-
applyEvent(
|
|
280
|
-
event: LiveStoreEvent,
|
|
281
|
-
eventDefinition: ActionDefinition,
|
|
282
|
-
otelContext: otel.Context,
|
|
283
|
-
): { durationMs: number } {
|
|
284
|
-
return this.otelTracer.startActiveSpan('livestore.in-memory-db:applyEvent', {}, otelContext, (span) => {
|
|
285
|
-
// TODO: in the future, we'll do more CRDT-style stuff here to decide whether to run effects of the event
|
|
286
|
-
|
|
287
|
-
// NOTE: These two updates should happen transactionally;
|
|
288
|
-
// we don't create a transaction here because that's handled in the caller.
|
|
289
|
-
// The reason for this is that sometimes we want to apply multiple events in a larger transaction.
|
|
290
|
-
|
|
291
|
-
// Insert into the events table
|
|
292
|
-
// this.execute(sql`insert into ${EVENTS_TABLE_NAME} (id, type, args) values ($id, $type, $args)`, {
|
|
293
|
-
// id: event.id,
|
|
294
|
-
// type: event.type,
|
|
295
|
-
// args: JSON.stringify(event.args ?? {}),
|
|
296
|
-
// })
|
|
297
|
-
|
|
298
|
-
const statement =
|
|
299
|
-
typeof eventDefinition.statement === 'function'
|
|
300
|
-
? eventDefinition.statement(event.args)
|
|
301
|
-
: eventDefinition.statement
|
|
302
|
-
|
|
303
|
-
const prepareBindValues = eventDefinition.prepareBindValues ?? identity
|
|
304
|
-
|
|
305
|
-
const bindValues =
|
|
306
|
-
typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound
|
|
307
|
-
? {}
|
|
308
|
-
: prepareBindValues(event.args)
|
|
309
|
-
|
|
310
|
-
if (import.meta.env.DEV) {
|
|
311
|
-
this.debugInfo.events.push([statement.sql, bindValues])
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Run the effects of the event
|
|
315
|
-
this.execute(statement.sql, bindValues, statement.writeTables)
|
|
316
|
-
|
|
317
|
-
span.end()
|
|
318
|
-
|
|
319
|
-
const durationMs = getDurationMsFromSpan(span)
|
|
320
|
-
|
|
321
|
-
this.debugInfo.queryFrameDuration += durationMs
|
|
322
|
-
this.debugInfo.queryFrameCount++
|
|
323
|
-
|
|
324
|
-
if (durationMs > 5 && import.meta.env.DEV) {
|
|
325
|
-
this.debugInfo.slowQueries.push([
|
|
326
|
-
statement.sql,
|
|
327
|
-
bindValues,
|
|
328
|
-
durationMs,
|
|
329
|
-
undefined,
|
|
330
|
-
[],
|
|
331
|
-
getStartTimeHighResFromSpan(span),
|
|
332
|
-
])
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return { durationMs }
|
|
336
|
-
})
|
|
337
|
-
}
|
|
338
|
-
|
|
339
299
|
export() {
|
|
340
300
|
// Clear statement cache because exporting frees statements
|
|
341
301
|
for (const key of this.cachedStmts.keys()) {
|
|
@@ -345,3 +305,18 @@ export class InMemoryDatabase {
|
|
|
345
305
|
return this.db.capi.sqlite3_js_db_export(this.db.pointer)
|
|
346
306
|
}
|
|
347
307
|
}
|
|
308
|
+
|
|
309
|
+
/** Set up SQLite performance; hasn't been super carefully optimized yet. */
|
|
310
|
+
const configureSQLite = (db: InMemoryDatabase) => {
|
|
311
|
+
db.execute(
|
|
312
|
+
// TODO: revisit these tuning parameters for max performance
|
|
313
|
+
sql`
|
|
314
|
+
PRAGMA page_size=32768;
|
|
315
|
+
PRAGMA cache_size=10000;
|
|
316
|
+
PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
|
|
317
|
+
PRAGMA synchronous='OFF';
|
|
318
|
+
PRAGMA temp_store='MEMORY';
|
|
319
|
+
PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
|
|
320
|
+
`,
|
|
321
|
+
)
|
|
322
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
export { Store, createStore
|
|
2
|
-
export type {
|
|
1
|
+
export { Store, createStore } from './store.js'
|
|
2
|
+
export type {
|
|
3
|
+
LiveStoreQuery,
|
|
4
|
+
GetAtomResult,
|
|
5
|
+
BaseGraphQLContext,
|
|
6
|
+
QueryResult,
|
|
7
|
+
QueryDebugInfo,
|
|
8
|
+
RefreshReason,
|
|
9
|
+
} from './store.js'
|
|
3
10
|
|
|
4
11
|
export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
|
|
5
12
|
|
|
6
13
|
export {
|
|
7
14
|
defineComponentStateSchema,
|
|
8
|
-
EVENT_CURSOR_TABLE,
|
|
9
|
-
defineSchema,
|
|
10
15
|
defineAction,
|
|
11
16
|
defineActions,
|
|
12
17
|
defineTables,
|
|
13
18
|
defineMaterializedViews,
|
|
19
|
+
makeSchema,
|
|
14
20
|
} from './schema.js'
|
|
15
21
|
export { InMemoryDatabase, type DebugInfo, emptyDebugInfo } from './inMemoryDatabase.js'
|
|
16
|
-
export {
|
|
17
|
-
export type { BackendOptions, Backend, BackendType } from './backends/index.js'
|
|
18
|
-
export { isBackendType } from './backends/index.js'
|
|
19
|
-
export type { SelectResponse } from './backends/index.js'
|
|
20
|
-
export { WebWorkerBackend } from './backends/web.js'
|
|
22
|
+
export type { Storage, StorageType, StorageInit } from './storage/index.js'
|
|
21
23
|
export type {
|
|
22
24
|
GetAtom,
|
|
23
25
|
AtomDebugInfo,
|
|
@@ -32,16 +34,14 @@ export type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
|
|
|
32
34
|
|
|
33
35
|
export { labelForKey } from './componentKey.js'
|
|
34
36
|
export type { ComponentKey } from './componentKey.js'
|
|
35
|
-
export type {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ActionDefinitions,
|
|
44
|
-
} from './schema.js'
|
|
37
|
+
export type { Schema, GetActionArgs, GetApplyEventArgs, Index, ActionDefinition, ActionDefinitions } from './schema.js'
|
|
38
|
+
|
|
39
|
+
export { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
40
|
+
|
|
41
|
+
import type { SqliteAst } from 'effect-db-schema'
|
|
42
|
+
export type TableDefinition = SqliteAst.Table
|
|
43
|
+
|
|
44
|
+
export { SqliteDsl as DbSchema } from 'effect-db-schema'
|
|
45
45
|
|
|
46
|
-
export { sql, type Bindable } from './util.js'
|
|
46
|
+
export { prepareBindValues, sql, type Bindable, type PreparedBindValues } from './util.js'
|
|
47
47
|
export { isEqual } from 'lodash-es'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
import { SqliteAst } from 'effect-db-schema'
|
|
3
|
+
import { memoize, omit } from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
import type { InMemoryDatabase } from './index.js'
|
|
6
|
+
import type { Schema, SchemaMetaRow } from './schema.js'
|
|
7
|
+
import { componentStateTables, SCHEMA_META_TABLE, systemTables } from './schema.js'
|
|
8
|
+
import type { PreparedBindValues } from './util.js'
|
|
9
|
+
import { sql } from './util.js'
|
|
10
|
+
|
|
11
|
+
const getMemoizedTimestamp = memoize(() => new Date().toISOString())
|
|
12
|
+
|
|
13
|
+
// TODO more graceful DB migration (e.g. backup DB before destructive migrations)
|
|
14
|
+
export const migrateDb = ({
|
|
15
|
+
db,
|
|
16
|
+
otelContext,
|
|
17
|
+
schema,
|
|
18
|
+
}: {
|
|
19
|
+
db: InMemoryDatabase
|
|
20
|
+
otelContext: otel.Context
|
|
21
|
+
schema: Schema
|
|
22
|
+
}) => {
|
|
23
|
+
db.execute(
|
|
24
|
+
// TODO use schema migration definition from schema.ts instead
|
|
25
|
+
sql`create table if not exists ${SCHEMA_META_TABLE} (tableName text primary key, schemaHash text, updatedAt text);`,
|
|
26
|
+
undefined,
|
|
27
|
+
[],
|
|
28
|
+
{ otelContext },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const schemaMetaRows = db.select<SchemaMetaRow>(sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
|
|
32
|
+
|
|
33
|
+
const dbSchemaHashByTable = Object.fromEntries(
|
|
34
|
+
schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const tableDefs = {
|
|
38
|
+
// NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
|
|
39
|
+
[SCHEMA_META_TABLE]: systemTables[SCHEMA_META_TABLE],
|
|
40
|
+
...omit(schema.tables, [SCHEMA_META_TABLE]),
|
|
41
|
+
...componentStateTables,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [tableName, tableDef] of Object.entries(tableDefs)) {
|
|
45
|
+
const dbSchemaHash = dbSchemaHashByTable[tableName]
|
|
46
|
+
const schemaHash = SqliteAst.hash(tableDef)
|
|
47
|
+
if (schemaHash !== dbSchemaHash) {
|
|
48
|
+
console.log(
|
|
49
|
+
`Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
migrateTable({ db, tableDef, otelContext, schemaHash })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const migrateTable = ({
|
|
58
|
+
db,
|
|
59
|
+
tableDef,
|
|
60
|
+
otelContext,
|
|
61
|
+
schemaHash,
|
|
62
|
+
}: {
|
|
63
|
+
db: InMemoryDatabase
|
|
64
|
+
tableDef: SqliteAst.Table
|
|
65
|
+
otelContext: otel.Context
|
|
66
|
+
schemaHash: number
|
|
67
|
+
}) => {
|
|
68
|
+
console.log(`Migrating table '${tableDef.name}'...`)
|
|
69
|
+
const tableName = tableDef.name
|
|
70
|
+
const columnSpec = makeColumnSpec(tableDef)
|
|
71
|
+
|
|
72
|
+
// TODO need to possibly handle cascading deletes due to foreign keys
|
|
73
|
+
db.execute(sql`drop table if exists ${tableName}`, undefined, [], { otelContext })
|
|
74
|
+
db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, [], { otelContext })
|
|
75
|
+
|
|
76
|
+
for (const index of tableDef.indexes) {
|
|
77
|
+
db.execute(createIndexFromDefinition(tableName, index), undefined, [], { otelContext })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const updatedAt = getMemoizedTimestamp()
|
|
81
|
+
db.execute(
|
|
82
|
+
sql`
|
|
83
|
+
INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
|
|
84
|
+
ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
|
|
85
|
+
`,
|
|
86
|
+
{ $tableName: tableName, $schemaHash: schemaHash, $updatedAt: updatedAt } as unknown as PreparedBindValues,
|
|
87
|
+
[],
|
|
88
|
+
{ otelContext },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
|
|
93
|
+
const uniqueStr = index.unique ? 'UNIQUE' : ''
|
|
94
|
+
return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const makeColumnSpec = (tableDef: SqliteAst.Table) => {
|
|
98
|
+
const primaryKeys = tableDef.columns.filter((_) => _.primaryKey).map((_) => _.name)
|
|
99
|
+
const columnDefStrs = tableDef.columns.map(toSqliteColumnSpec)
|
|
100
|
+
if (primaryKeys.length > 0) {
|
|
101
|
+
columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return columnDefStrs.join(', ')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const toSqliteColumnSpec = (column: SqliteAst.Column) => {
|
|
108
|
+
const columnType = column.type._tag
|
|
109
|
+
// const primaryKey = column.primaryKey ? 'primary key' : ''
|
|
110
|
+
const nullable = column.nullable === false ? 'not null' : ''
|
|
111
|
+
const defaultValue =
|
|
112
|
+
column.default === undefined
|
|
113
|
+
? ''
|
|
114
|
+
: columnType === 'text'
|
|
115
|
+
? `default '${column.default}'`
|
|
116
|
+
: `default ${column.default}`
|
|
117
|
+
|
|
118
|
+
return `${column.name} ${columnType} ${nullable} ${defaultValue}`
|
|
119
|
+
}
|
package/src/otel.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import type * as otel from '@opentelemetry/api'
|
|
2
2
|
|
|
3
|
-
// TODO improve - see https://www.notion.so/schickling/Better-solution-for-globalThis-inProgressSpans-503cd7a5f4fc4fb8bdec2e60bde1be1f
|
|
4
|
-
export const TODO_REMOVE_trackLongRunningSpan = (span: otel.Span): void => {
|
|
5
|
-
// @ts-expect-error TODO get rid of this coupling
|
|
6
|
-
if (window.inProgressSpans !== undefined && window.inProgressSpans instanceof Set) {
|
|
7
|
-
// @ts-expect-error TODO get rid of this coupling
|
|
8
|
-
window.inProgressSpans.add(span)
|
|
9
|
-
} else {
|
|
10
|
-
// debugger
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
3
|
export const getDurationMsFromSpan = (span: otel.Span): number => {
|
|
15
4
|
const durationHr: [seconds: number, nanos: number] = (span as any)._duration
|
|
16
5
|
return durationHr[0] * 1000 + durationHr[1] / 1_000_000
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import type * as otel from '@opentelemetry/api'
|
|
2
|
-
import { mapValues } from 'lodash-es'
|
|
3
2
|
import type { ReactElement, ReactNode } from 'react'
|
|
4
3
|
import React from 'react'
|
|
4
|
+
import initSqlite3Wasm from 'sqlite-esm'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
LiveStoreContext as StoreContext_,
|
|
10
|
-
LiveStoreCreateStoreOptions,
|
|
11
|
-
} from '../effect/LiveStore.js'
|
|
6
|
+
// TODO refactor so the `react` module doesn't depend on `effect` module
|
|
7
|
+
import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
|
|
8
|
+
import type { InMemoryDatabase } from '../inMemoryDatabase.js'
|
|
12
9
|
import type { Schema } from '../schema.js'
|
|
10
|
+
import type { StorageInit } from '../storage/index.js'
|
|
13
11
|
import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
|
|
14
12
|
import { createStore } from '../store.js'
|
|
15
13
|
import { LiveStoreContext } from './LiveStoreContext.js'
|
|
16
14
|
|
|
15
|
+
// NOTE we're starting to initialize the sqlite wasm binary here (already before calling `createStore`),
|
|
16
|
+
// so that it's ready when we need it
|
|
17
|
+
const sqlite3Promise = initSqlite3Wasm({
|
|
18
|
+
print: (message) => console.log(`[livestore sqlite] ${message}`),
|
|
19
|
+
printErr: (message) => console.error(`[livestore sqlite] ${message}`),
|
|
20
|
+
})
|
|
21
|
+
|
|
17
22
|
interface LiveStoreProviderProps<GraphQLContext> {
|
|
18
23
|
schema: Schema
|
|
19
|
-
|
|
20
|
-
boot?: (
|
|
21
|
-
globalQueryDefs: GlobalQueryDefs
|
|
24
|
+
loadStorage: () => StorageInit | Promise<StorageInit>
|
|
25
|
+
boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
|
|
22
26
|
graphQLOptions?: GraphQLOptions<GraphQLContext>
|
|
23
27
|
otelTracer?: otel.Tracer
|
|
24
28
|
otelRootSpanContext?: otel.Context
|
|
@@ -27,8 +31,7 @@ interface LiveStoreProviderProps<GraphQLContext> {
|
|
|
27
31
|
|
|
28
32
|
export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
29
33
|
fallback,
|
|
30
|
-
|
|
31
|
-
backendOptions,
|
|
34
|
+
loadStorage,
|
|
32
35
|
graphQLOptions,
|
|
33
36
|
otelTracer,
|
|
34
37
|
otelRootSpanContext,
|
|
@@ -38,8 +41,7 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
38
41
|
}: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
|
|
39
42
|
const store = useCreateStore({
|
|
40
43
|
schema,
|
|
41
|
-
|
|
42
|
-
backendOptions,
|
|
44
|
+
loadStorage,
|
|
43
45
|
graphQLOptions,
|
|
44
46
|
otelTracer,
|
|
45
47
|
otelRootSpanContext,
|
|
@@ -50,13 +52,14 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
50
52
|
return fallback
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
window.__debugLiveStore = store.store
|
|
56
|
+
|
|
53
57
|
return <LiveStoreContext.Provider value={store}>{children}</LiveStoreContext.Provider>
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
57
61
|
schema,
|
|
58
|
-
|
|
59
|
-
backendOptions,
|
|
62
|
+
loadStorage,
|
|
60
63
|
graphQLOptions,
|
|
61
64
|
otelTracer,
|
|
62
65
|
otelRootSpanContext,
|
|
@@ -67,19 +70,17 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
67
70
|
React.useEffect(() => {
|
|
68
71
|
void (async () => {
|
|
69
72
|
try {
|
|
73
|
+
const sqlite3 = await sqlite3Promise
|
|
70
74
|
const store = await createStore({
|
|
71
75
|
schema,
|
|
72
|
-
|
|
76
|
+
loadStorage,
|
|
73
77
|
graphQLOptions,
|
|
74
78
|
otelTracer,
|
|
75
79
|
otelRootSpanContext,
|
|
76
80
|
boot,
|
|
81
|
+
sqlite3,
|
|
77
82
|
})
|
|
78
|
-
|
|
79
|
-
const globalQueries = mapValues(globalQueryDefs, (queryDef) => queryDef(store))
|
|
80
|
-
setCtxValue({ store, globalQueries })
|
|
81
|
-
span.end()
|
|
82
|
-
})
|
|
83
|
+
setCtxValue({ store })
|
|
83
84
|
} catch (e) {
|
|
84
85
|
console.error(`Error creating LiveStore store:`, e)
|
|
85
86
|
throw e
|
|
@@ -87,7 +88,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
87
88
|
})()
|
|
88
89
|
|
|
89
90
|
// TODO: do we need to return any cleanup function here?
|
|
90
|
-
}, [schema,
|
|
91
|
+
}, [schema, loadStorage, graphQLOptions, otelTracer, otelRootSpanContext, boot])
|
|
91
92
|
|
|
92
93
|
return ctxValue
|
|
93
94
|
}
|
package/src/react/index.ts
CHANGED
|
@@ -3,9 +3,18 @@ export type {
|
|
|
3
3
|
ReactiveGraphQL,
|
|
4
4
|
ReactiveSQL,
|
|
5
5
|
Setters,
|
|
6
|
+
ComponentKeyConfig,
|
|
7
|
+
QueryResults,
|
|
8
|
+
QueryDefinitions,
|
|
9
|
+
ComponentColumns,
|
|
10
|
+
GetStateType,
|
|
11
|
+
GetStateTypeEncoded,
|
|
6
12
|
} from './useLiveStoreComponent.js'
|
|
7
13
|
export { LiveStoreContext, useStore } from './LiveStoreContext.js'
|
|
8
14
|
export { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
9
15
|
export { useLiveStoreComponent } from './useLiveStoreComponent.js'
|
|
10
16
|
export { useGraphQL } from './useGraphQL.js'
|
|
11
|
-
export {
|
|
17
|
+
export { useQuery } from './useQuery.js'
|
|
18
|
+
|
|
19
|
+
// Needed to make TS happy
|
|
20
|
+
export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
|