@livestore/livestore 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +61 -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 +198 -0
- package/dist/__tests__/reactive.test.js.map +1 -0
- package/dist/backends/base.d.ts +13 -0
- package/dist/backends/base.d.ts.map +1 -0
- package/dist/backends/base.js +53 -0
- package/dist/backends/base.js.map +1 -0
- package/dist/backends/in-memory/index.d.ts +22 -0
- package/dist/backends/in-memory/index.d.ts.map +1 -0
- package/dist/backends/in-memory/index.js +45 -0
- package/dist/backends/in-memory/index.js.map +1 -0
- package/dist/backends/index.d.ts +41 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +16 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/tauri/index.d.ts +21 -0
- package/dist/backends/tauri/index.d.ts.map +1 -0
- package/dist/backends/tauri/index.js +48 -0
- package/dist/backends/tauri/index.js.map +1 -0
- package/dist/backends/utils/idb.d.ts +10 -0
- package/dist/backends/utils/idb.d.ts.map +1 -0
- package/dist/backends/utils/idb.js +58 -0
- package/dist/backends/utils/idb.js.map +1 -0
- package/dist/backends/web-worker/index.d.ts +26 -0
- package/dist/backends/web-worker/index.d.ts.map +1 -0
- package/dist/backends/web-worker/index.js +63 -0
- package/dist/backends/web-worker/index.js.map +1 -0
- package/dist/backends/web-worker/worker.d.ts +17 -0
- package/dist/backends/web-worker/worker.d.ts.map +1 -0
- package/dist/backends/web-worker/worker.js +139 -0
- package/dist/backends/web-worker/worker.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 +42 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +37 -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 +60 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +230 -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 +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 +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 +22 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +49 -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/useGlobalQuery.d.ts +3 -0
- package/dist/react/useGlobalQuery.d.ts.map +1 -0
- package/dist/react/useGlobalQuery.js +23 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +11 -0
- package/dist/react/useGraphQL.d.ts.map +1 -0
- package/dist/react/useGraphQL.js +67 -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 +301 -0
- package/dist/react/useLiveStoreComponent.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 +24 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +22 -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 +83 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +49 -0
- package/dist/schema.js.map +1 -0
- 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 +192 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +569 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +53 -0
- package/dist/util.js.map +1 -0
- package/package.json +46 -19
- package/src/__tests__/react/fixture.tsx +19 -28
- 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 +95 -37
- 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/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
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'
|
package/tsconfig.json
CHANGED
package/src/backends/base.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
-
import { errorToString } from '@livestore/utils'
|
|
3
|
-
import { identity } from '@livestore/utils/effect'
|
|
4
|
-
import * as otel from '@opentelemetry/api'
|
|
5
|
-
|
|
6
|
-
import type { LiveStoreEvent } from '../events.js'
|
|
7
|
-
// import { EVENTS_TABLE_NAME } from '../events.js'
|
|
8
|
-
import type { ActionDefinition } from '../schema.js'
|
|
9
|
-
import type { ParamsObject } from '../util.js'
|
|
10
|
-
import type { Backend, SelectResponse } from './index.js'
|
|
11
|
-
|
|
12
|
-
export abstract class BaseBackend implements Backend {
|
|
13
|
-
abstract otelTracer: otel.Tracer
|
|
14
|
-
|
|
15
|
-
select = async <T = any>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
|
|
16
|
-
throw new Error('Method not implemented.')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
execute = (query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void => {
|
|
20
|
-
throw new Error('Method not implemented.')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
getPersistedData = async (parentSpan?: otel.Span): Promise<Uint8Array> => {
|
|
24
|
-
throw new Error('Method not implemented.')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// TODO move `applyEvent` logic to Store and only call `execute` here
|
|
28
|
-
applyEvent = (event: LiveStoreEvent, eventDefinition: ActionDefinition, parentSpan?: otel.Span): void => {
|
|
29
|
-
const ctx = parentSpan ? otel.trace.setSpan(otel.context.active(), parentSpan) : otel.context.active()
|
|
30
|
-
this.otelTracer.startActiveSpan('LiveStore:backend:applyEvent', {}, ctx, (span) => {
|
|
31
|
-
try {
|
|
32
|
-
// Careful: this SQL statement is duplicated in the backend.
|
|
33
|
-
// Remember to update it in src-tauri/src/store.rs:apply_event as well.
|
|
34
|
-
// await this.execute(sql`insert into ${EVENTS_TABLE_NAME} (id, type, args) values ($id, $type, $args)`, {
|
|
35
|
-
// id: event.id,
|
|
36
|
-
// type: event.type,
|
|
37
|
-
// args: JSON.stringify(event.args ?? {}),
|
|
38
|
-
// })
|
|
39
|
-
|
|
40
|
-
const statement =
|
|
41
|
-
typeof eventDefinition.statement === 'function'
|
|
42
|
-
? eventDefinition.statement(event.args)
|
|
43
|
-
: eventDefinition.statement
|
|
44
|
-
|
|
45
|
-
const prepareBindValues = eventDefinition.prepareBindValues ?? identity
|
|
46
|
-
|
|
47
|
-
const bindValues =
|
|
48
|
-
typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound
|
|
49
|
-
? {}
|
|
50
|
-
: prepareBindValues(event.args)
|
|
51
|
-
|
|
52
|
-
span.setAttributes({
|
|
53
|
-
'livestore.statement.sql': statement.sql,
|
|
54
|
-
'livestore.statement.writeTables': statement.writeTables,
|
|
55
|
-
'livestore.statement.bindVales': JSON.stringify(bindValues),
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
this.execute(statement.sql, bindValues, span)
|
|
59
|
-
} catch (e: any) {
|
|
60
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: errorToString(e) })
|
|
61
|
-
throw e
|
|
62
|
-
} finally {
|
|
63
|
-
span.end()
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
}
|
package/src/backends/index.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// A backend represents a raw SQLite database.
|
|
2
|
-
// Examples include:
|
|
3
|
-
// - A native SQLite process running in a Tauri Rust process
|
|
4
|
-
// - A SQL.js WASM version of SQLite running in a web worker
|
|
5
|
-
//
|
|
6
|
-
// We can send commands to execute various kinds of queries,
|
|
7
|
-
// and respond to various events from the database.
|
|
8
|
-
|
|
9
|
-
import type * as otel from '@opentelemetry/api'
|
|
10
|
-
|
|
11
|
-
import type { LiveStoreEvent } from '../events.js'
|
|
12
|
-
import type { ActionDefinition } from '../schema.js'
|
|
13
|
-
import type { ParamsObject } from '../util.js'
|
|
14
|
-
import { casesHandled } from '../util.js'
|
|
15
|
-
import type { BackendOptionsTauri } from './tauri.js'
|
|
16
|
-
import type { BackendOptionsWeb } from './web.js'
|
|
17
|
-
import { WebWorkerBackend } from './web.js'
|
|
18
|
-
import type { BackendOptionsWebInMemory } from './web-in-memory.js'
|
|
19
|
-
import { WebInMemoryBackend } from './web-in-memory.js'
|
|
20
|
-
|
|
21
|
-
/* A location of a persistent writable SQLite file */
|
|
22
|
-
export type WritableDatabaseLocation =
|
|
23
|
-
| {
|
|
24
|
-
type: 'opfs'
|
|
25
|
-
virtualFilename: string
|
|
26
|
-
}
|
|
27
|
-
| {
|
|
28
|
-
type: 'indexeddb'
|
|
29
|
-
virtualFilename: string
|
|
30
|
-
}
|
|
31
|
-
| {
|
|
32
|
-
type: 'filesystem'
|
|
33
|
-
directory: string
|
|
34
|
-
filename: string
|
|
35
|
-
}
|
|
36
|
-
| {
|
|
37
|
-
type: 'volatile-in-memory'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface Backend {
|
|
41
|
-
// Select some data from the DB.
|
|
42
|
-
// This should only do reads, not writes, but we don't strongly enforce that.
|
|
43
|
-
select<T = any>(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): Promise<SelectResponse<T>>
|
|
44
|
-
|
|
45
|
-
// Execute a query where you don't care about the result.
|
|
46
|
-
// Used for writes and configuration changes.
|
|
47
|
-
execute(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void
|
|
48
|
-
|
|
49
|
-
/** Apply an event to the backend */
|
|
50
|
-
applyEvent(event: LiveStoreEvent, eventDefiniton: ActionDefinition, parentSpan?: otel.Span): void
|
|
51
|
-
|
|
52
|
-
/** Return a snapshot of persisted data from the backend */
|
|
53
|
-
getPersistedData(parentSpan?: otel.Span): Promise<Uint8Array>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type BackendType = 'tauri' | 'web' | 'web-in-memory'
|
|
57
|
-
|
|
58
|
-
export const isBackendType = (type: string): type is BackendType => {
|
|
59
|
-
return type === 'tauri' || type === 'web' || type === 'web-in-memory'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type SelectResponse<T = any> = {
|
|
63
|
-
results: T[]
|
|
64
|
-
|
|
65
|
-
// other perf stats metadata about how long the query took
|
|
66
|
-
[key: string]: any
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export enum IndexType {
|
|
70
|
-
Basic = 'Basic',
|
|
71
|
-
FullText = 'FullText',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export type BackendOptions = BackendOptionsTauri | BackendOptionsWeb | BackendOptionsWebInMemory
|
|
75
|
-
export type BackendOtelProps = {
|
|
76
|
-
otelTracer: otel.Tracer
|
|
77
|
-
parentSpan: otel.Span
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const createBackend = async (options: BackendOptions, otelProps: BackendOtelProps): Promise<Backend> => {
|
|
81
|
-
switch (options.type) {
|
|
82
|
-
case 'tauri': {
|
|
83
|
-
// NOTE Dynamic import is needed to avoid Tauri is a dependency of LiveStore (e.g. when used in the web)
|
|
84
|
-
const { TauriBackend } = await import('./tauri.js')
|
|
85
|
-
return await TauriBackend.load(options, otelProps)
|
|
86
|
-
}
|
|
87
|
-
case 'web': {
|
|
88
|
-
return WebWorkerBackend.load(options, otelProps)
|
|
89
|
-
}
|
|
90
|
-
// NOTE currently only needed for testing
|
|
91
|
-
case 'web-in-memory': {
|
|
92
|
-
return WebInMemoryBackend.load(options, otelProps)
|
|
93
|
-
}
|
|
94
|
-
default: {
|
|
95
|
-
casesHandled(options)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
package/src/backends/noop.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { makeNoopTracer } from '@livestore/utils'
|
|
2
|
-
import type * as otel from '@opentelemetry/api'
|
|
3
|
-
|
|
4
|
-
import type { ParamsObject } from '../util.js'
|
|
5
|
-
import { BaseBackend } from './base.js'
|
|
6
|
-
import type { SelectResponse } from './index.js'
|
|
7
|
-
|
|
8
|
-
export type BackendOptionsNoop = {
|
|
9
|
-
type: 'noop'
|
|
10
|
-
/** Specifies where to persist data for this backend */
|
|
11
|
-
otelTracer?: otel.Tracer
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class NoopBackend extends BaseBackend {
|
|
15
|
-
constructor(readonly otelTracer: otel.Tracer) {
|
|
16
|
-
super()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
static load = async (options: BackendOptionsNoop): Promise<NoopBackend> => {
|
|
20
|
-
return new NoopBackend(options.otelTracer ?? makeNoopTracer())
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
execute = (_query: string, _bindValues?: ParamsObject): void => {}
|
|
24
|
-
|
|
25
|
-
select = async <T>(_query: string, _bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
|
|
26
|
-
return { results: [] }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
getPersistedData = async (): Promise<Uint8Array> => {
|
|
30
|
-
return new Uint8Array()
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type * as otel from '@opentelemetry/api'
|
|
2
|
-
import type * as SqliteWasm from 'sqlite-esm'
|
|
3
|
-
import sqlite3InitModule from 'sqlite-esm'
|
|
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'
|
|
9
|
-
|
|
10
|
-
export type BackendOptionsWebInMemory = {
|
|
11
|
-
type: 'web-in-memory'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
|
|
15
|
-
|
|
16
|
-
// NOTE: This backend is currently only used for testing
|
|
17
|
-
export class WebInMemoryBackend extends BaseBackend {
|
|
18
|
-
constructor(
|
|
19
|
-
readonly otelTracer: otel.Tracer,
|
|
20
|
-
readonly db: DatabaseWithCAPI,
|
|
21
|
-
) {
|
|
22
|
-
super()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
static load = async (
|
|
26
|
-
_options: BackendOptionsWebInMemory,
|
|
27
|
-
{ otelTracer }: BackendOtelProps,
|
|
28
|
-
): Promise<WebInMemoryBackend> => {
|
|
29
|
-
const sqlite3 = await sqlite3InitModule({
|
|
30
|
-
print: (message) => console.log(`[sql-client] ${message}`),
|
|
31
|
-
printErr: (message) => console.error(`[sql-client] ${message}`),
|
|
32
|
-
})
|
|
33
|
-
const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
|
|
34
|
-
db.capi = sqlite3.capi
|
|
35
|
-
|
|
36
|
-
return new WebInMemoryBackend(otelTracer, db)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
execute = (query: string, bindValues?: ParamsObject): void => {
|
|
40
|
-
this.db.exec({
|
|
41
|
-
sql: query,
|
|
42
|
-
bind: prepareBindValues(bindValues ?? {}, query) as TODO,
|
|
43
|
-
returnValue: 'resultRows',
|
|
44
|
-
rowMode: 'object',
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
select = async <T>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
|
|
49
|
-
const resultRows: T[] = []
|
|
50
|
-
|
|
51
|
-
this.db.exec({
|
|
52
|
-
sql: query,
|
|
53
|
-
bind: prepareBindValues(bindValues ?? {}, query) as TODO,
|
|
54
|
-
rowMode: 'object',
|
|
55
|
-
resultRows,
|
|
56
|
-
// callback: (row: any) => console.log('select result', db.filename, query, row),
|
|
57
|
-
} as TODO)
|
|
58
|
-
|
|
59
|
-
return { results: resultRows }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
getPersistedData = async (): Promise<Uint8Array> => {
|
|
63
|
-
return this.db.capi.sqlite3_js_db_export(this.db.pointer)
|
|
64
|
-
}
|
|
65
|
-
}
|