@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.
- package/README.md +7 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +4 -120
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +19 -26
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/reactive.test.js +31 -0
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/backends/base.d.ts +4 -4
- package/dist/backends/{web-in-memory.d.ts → in-memory/index.d.ts} +6 -6
- package/dist/backends/in-memory/index.d.ts.map +1 -0
- package/dist/backends/{web-in-memory.js → in-memory/index.js} +7 -7
- package/dist/backends/in-memory/index.js.map +1 -0
- package/dist/backends/index.d.ts +4 -8
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +0 -22
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/{tauri.d.ts → tauri/index.d.ts} +5 -6
- package/dist/backends/tauri/index.d.ts.map +1 -0
- package/dist/backends/{tauri.js → tauri/index.js} +4 -4
- package/dist/backends/tauri/index.js.map +1 -0
- package/dist/backends/{web.d.ts → web-worker/index.d.ts} +6 -7
- package/dist/backends/web-worker/index.d.ts.map +1 -0
- package/dist/backends/{web.js → web-worker/index.js} +6 -6
- package/dist/backends/web-worker/index.js.map +1 -0
- package/dist/backends/{web-worker.d.ts → web-worker/worker.d.ts} +3 -3
- package/dist/backends/web-worker/worker.d.ts.map +1 -0
- package/dist/backends/{web-worker.js → web-worker/worker.js} +3 -3
- package/dist/backends/web-worker/worker.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +6 -6
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +2 -5
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/events.js.map +1 -1
- package/dist/inMemoryDatabase.d.ts +5 -10
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +78 -89
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +62 -0
- package/dist/migrations.js.map +1 -0
- package/dist/otel.d.ts +0 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +0 -11
- package/dist/otel.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +5 -4
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +6 -5
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/useGlobalQuery.d.ts.map +1 -1
- package/dist/react/useGlobalQuery.js +0 -2
- package/dist/react/useGlobalQuery.js.map +1 -1
- package/dist/react/useLiveStoreComponent.d.ts +22 -17
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
- package/dist/react/useLiveStoreComponent.js +46 -17
- package/dist/react/useLiveStoreComponent.js.map +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +1 -0
- package/dist/reactive.js.map +1 -1
- package/dist/schema.d.ts +32 -112
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +36 -79
- package/dist/schema.js.map +1 -1
- package/dist/storage/base.d.ts +10 -0
- package/dist/storage/base.d.ts.map +1 -0
- package/dist/storage/base.js +14 -0
- package/dist/storage/base.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 +76 -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 +6 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +93 -63
- package/dist/store.js.map +1 -1
- package/dist/util.d.ts +3 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +2 -0
- package/dist/util.js.map +1 -1
- package/package.json +50 -23
- package/src/__tests__/react/fixture.tsx +19 -28
- package/src/__tests__/reactive.test.ts +39 -0
- package/src/effect/LiveStore.ts +8 -13
- package/src/events.ts +1 -1
- package/src/inMemoryDatabase.ts +100 -117
- package/src/index.ts +10 -16
- package/src/migrations.ts +101 -0
- package/src/otel.ts +0 -11
- package/src/react/LiveStoreProvider.tsx +12 -8
- package/src/react/index.ts +9 -0
- package/src/react/useGlobalQuery.ts +0 -3
- package/src/react/useLiveStoreComponent.ts +98 -38
- package/src/reactive.ts +2 -1
- package/src/schema.ts +72 -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} +13 -27
- package/src/storage/web-worker/index.ts +118 -0
- package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
- package/src/store.ts +112 -79
- package/src/util.ts +5 -1
- package/tsconfig.json +1 -3
- package/dist/backends/noop.d.ts +0 -18
- package/dist/backends/noop.d.ts.map +0 -1
- package/dist/backends/noop.js +0 -21
- package/dist/backends/noop.js.map +0 -1
- package/dist/backends/tauri.d.ts.map +0 -1
- package/dist/backends/tauri.js.map +0 -1
- package/dist/backends/web-in-memory.d.ts.map +0 -1
- package/dist/backends/web-in-memory.js.map +0 -1
- package/dist/backends/web-worker.d.ts.map +0 -1
- package/dist/backends/web-worker.js.map +0 -1
- package/dist/backends/web.d.ts.map +0 -1
- package/dist/backends/web.js.map +0 -1
- 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/{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 '
|
|
6
|
-
import { prepareBindValues } from '
|
|
7
|
-
import {
|
|
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
|
|
11
|
-
type: 'tauri'
|
|
9
|
+
export type StorageOptionsTauri = {
|
|
12
10
|
dbDirPath: string
|
|
13
11
|
appDbFileName: string
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
export class
|
|
14
|
+
export class TauriStorage implements Storage {
|
|
17
15
|
constructor(
|
|
18
16
|
readonly dbFilePath: string,
|
|
19
17
|
readonly dbDirPath: string,
|
|
20
18
|
readonly otelTracer: otel.Tracer,
|
|
21
19
|
readonly parentSpan: otel.Span,
|
|
22
|
-
) {
|
|
23
|
-
super()
|
|
24
|
-
}
|
|
20
|
+
) {}
|
|
25
21
|
|
|
26
|
-
static load =
|
|
27
|
-
{ dbDirPath, appDbFileName }:
|
|
28
|
-
{ otelTracer, parentSpan }:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await invoke('initialize_connection', { dbName: dbFilePath, otelData: getOtelData_(parentSpan) })
|
|
22
|
+
static load =
|
|
23
|
+
({ dbDirPath, appDbFileName }: StorageOptionsTauri) =>
|
|
24
|
+
async ({ otelTracer, parentSpan }: StorageOtelProps) => {
|
|
25
|
+
const dbFilePath = `${dbDirPath}/${appDbFileName}`
|
|
26
|
+
await invoke('initialize_connection', { dbName: dbFilePath, otelData: getOtelData_(parentSpan) })
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
return new TauriStorage(dbFilePath, dbDirPath, otelTracer, parentSpan)
|
|
29
|
+
}
|
|
35
30
|
|
|
36
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 '
|
|
12
|
-
import { casesHandled, sql } from '
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
11
|
+
import type { Bindable } from '../../util.js'
|
|
12
|
+
import { casesHandled, sql } from '../../util.js'
|
|
13
|
+
import { IDB } from '../utils/idb.js'
|
|
14
|
+
import type { StorageOptionsWeb } from './index.js'
|
|
15
15
|
|
|
16
16
|
// A global variable to hold the database connection.
|
|
17
17
|
// let db: SqliteWasm.Database
|
|
@@ -19,11 +19,11 @@ let db: SqliteWasm.DatabaseApi
|
|
|
19
19
|
|
|
20
20
|
let sqlite3: SqliteWasm.Sqlite3Static
|
|
21
21
|
|
|
22
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
22
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
23
23
|
let idb: IDB | undefined
|
|
24
24
|
|
|
25
|
-
/** The location where this database
|
|
26
|
-
let
|
|
25
|
+
/** The location where this database storage persists its data */
|
|
26
|
+
let options_: StorageOptionsWeb
|
|
27
27
|
|
|
28
28
|
const configureConnection = () =>
|
|
29
29
|
db.exec(sql`
|
|
@@ -35,18 +35,18 @@ const configureConnection = () =>
|
|
|
35
35
|
/** A full virtual filename in the IDB FS */
|
|
36
36
|
const fullyQualifiedFilename = (name: string) => `${name}.db`
|
|
37
37
|
|
|
38
|
-
const initialize = async (
|
|
39
|
-
|
|
38
|
+
const initialize = async (options: StorageOptionsWeb) => {
|
|
39
|
+
options_ = options
|
|
40
40
|
|
|
41
41
|
sqlite3 = await sqlite3InitModule({
|
|
42
42
|
print: (message) => console.log(`[sql-client] ${message}`),
|
|
43
43
|
printErr: (message) => console.error(`[sql-client] ${message}`),
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
switch (
|
|
46
|
+
switch (options.type) {
|
|
47
47
|
case 'opfs': {
|
|
48
48
|
try {
|
|
49
|
-
db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(
|
|
49
|
+
db = new sqlite3.oo1.OpfsDb(fullyQualifiedFilename(options.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(
|
|
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(
|
|
74
|
+
casesHandled(options.type)
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
configureConnection()
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
81
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
88
82
|
let idbPersistTimeout: NodeJS.Timeout | undefined
|
|
89
83
|
|
|
90
84
|
type ExecutionQueueItem = { query: string; bindValues?: Bindable }
|
|
@@ -119,8 +113,8 @@ const executeBulk = (executionItems: ExecutionQueueItem[]): void => {
|
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite
|
|
123
|
-
if (
|
|
116
|
+
// TODO get rid of this in favour of a "proper" IDB SQLite storage
|
|
117
|
+
if (options_.type === 'indexeddb') {
|
|
124
118
|
if (idbPersistTimeout !== undefined) {
|
|
125
119
|
clearTimeout(idbPersistTimeout)
|
|
126
120
|
}
|
|
@@ -133,36 +127,7 @@ const executeBulk = (executionItems: ExecutionQueueItem[]): void => {
|
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
129
|
|
|
136
|
-
const
|
|
137
|
-
const resultRows: T[] = []
|
|
138
|
-
|
|
139
|
-
db.exec({
|
|
140
|
-
sql: query,
|
|
141
|
-
bind: bindValues,
|
|
142
|
-
rowMode: 'object',
|
|
143
|
-
resultRows,
|
|
144
|
-
} as TODO)
|
|
145
|
-
|
|
146
|
-
return { results: resultRows }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const getPersistedData = async (): Promise<Uint8Array> => {
|
|
150
|
-
// TODO get rid of this in favour of a "proper" IDB SQLite backend
|
|
151
|
-
if (persistentDatabaseLocation_.type === 'indexeddb') {
|
|
152
|
-
const data = sqlite3.capi.sqlite3_js_db_export(db.pointer)
|
|
153
|
-
return Comlink.transfer(data, [data.buffer])
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const rootHandle = await navigator.storage.getDirectory()
|
|
157
|
-
const fileHandle = await rootHandle.getFileHandle(db.filename)
|
|
158
|
-
const file = await fileHandle.getFile()
|
|
159
|
-
const buffer = await file.arrayBuffer()
|
|
160
|
-
const data = new Uint8Array(buffer)
|
|
161
|
-
|
|
162
|
-
return Comlink.transfer(data, [data.buffer])
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const wrappedWorker = { initialize, executeBulk, select, getPersistedData }
|
|
130
|
+
const wrappedWorker = { initialize, executeBulk }
|
|
166
131
|
|
|
167
132
|
export type WrappedWorker = typeof wrappedWorker
|
|
168
133
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
storage?: Storage
|
|
115
117
|
temporaryQueries: Set<LiveStoreQuery> | undefined
|
|
116
118
|
|
|
117
119
|
private constructor({
|
|
118
120
|
db,
|
|
119
121
|
schema,
|
|
120
|
-
|
|
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.
|
|
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
|
|
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
|
|
771
|
-
if (this.
|
|
772
|
-
this.
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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.
|
|
814
|
+
if (this.storage !== undefined) {
|
|
799
815
|
const parentSpan = otel.trace.getSpan(otel.context.active())
|
|
800
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
831
|
+
loadStorage: () => StorageInit | Promise<StorageInit>
|
|
816
832
|
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
817
833
|
otelTracer?: otel.Tracer
|
|
818
834
|
otelRootSpanContext?: otel.Context
|
|
819
|
-
boot?: (
|
|
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
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
856
|
-
shouldResetDB = true
|
|
865
|
+
return { storage, persistedData }
|
|
857
866
|
}
|
|
858
867
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
865
|
-
await boot(backend!, span)
|
|
866
|
-
}
|
|
877
|
+
const [{ storage, persistedData }, sqlite3] = await Promise.all([loadStorageAndPersistedData(), loadSqlite3()])
|
|
867
878
|
|
|
868
|
-
const
|
|
869
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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,
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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'
|