@livestore/livestore 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/LiveRiffleStore.d.ts +42 -0
- package/dist/LiveRiffleStore.d.ts.map +1 -0
- package/dist/LiveRiffleStore.js +36 -0
- package/dist/LiveRiffleStore.js.map +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 +141 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -0
- package/dist/__tests__/react/fixture.js +72 -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__/react/useRiffleComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useRiffleComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useRiffleComponent.test.js +78 -0
- package/dist/__tests__/react/useRiffleComponent.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 +167 -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/index.d.ts +41 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +38 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/noop.d.ts +18 -0
- package/dist/backends/noop.d.ts.map +1 -0
- package/dist/backends/noop.js +21 -0
- package/dist/backends/noop.js.map +1 -0
- package/dist/backends/tauri.d.ts +24 -0
- package/dist/backends/tauri.d.ts.map +1 -0
- package/dist/backends/tauri.js +48 -0
- package/dist/backends/tauri.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-in-memory.d.ts +24 -0
- package/dist/backends/web-in-memory.d.ts.map +1 -0
- package/dist/backends/web-in-memory.js +46 -0
- package/dist/backends/web-in-memory.js.map +1 -0
- package/dist/backends/web-worker.d.ts +17 -0
- package/dist/backends/web-worker.d.ts.map +1 -0
- package/dist/backends/web-worker.js +139 -0
- package/dist/backends/web-worker.js.map +1 -0
- package/dist/backends/web.d.ts +28 -0
- package/dist/backends/web.d.ts.map +1 -0
- package/dist/backends/web.js +64 -0
- package/dist/backends/web.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 +36 -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 +65 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +241 -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 +10 -0
- package/dist/index.js.map +1 -0
- package/dist/otel.d.ts +5 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +17 -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 +21 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +48 -0
- package/dist/react/LiveStoreProvider.js.map +1 -0
- package/dist/react/RiffleProvider.d.ts +21 -0
- package/dist/react/RiffleProvider.d.ts.map +1 -0
- package/dist/react/RiffleProvider.js +48 -0
- package/dist/react/RiffleProvider.js.map +1 -0
- package/dist/react/StoreContext.d.ts +11 -0
- package/dist/react/StoreContext.d.ts.map +1 -0
- package/dist/react/StoreContext.js +10 -0
- package/dist/react/StoreContext.js.map +1 -0
- package/dist/react/index.d.ts +7 -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 +25 -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 +68 -0
- package/dist/react/useGraphQL.js.map +1 -0
- package/dist/react/useLiveStoreComponent.d.ts +70 -0
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
- package/dist/react/useLiveStoreComponent.js +261 -0
- package/dist/react/useLiveStoreComponent.js.map +1 -0
- package/dist/react/useRiffleComponent.d.ts +70 -0
- package/dist/react/useRiffleComponent.d.ts.map +1 -0
- package/dist/react/useRiffleComponent.js +261 -0
- package/dist/react/useRiffleComponent.js.map +1 -0
- package/dist/react/useRiffleJsonHook.d.ts +4 -0
- package/dist/react/useRiffleJsonHook.d.ts.map +1 -0
- package/dist/react/useRiffleJsonHook.js +21 -0
- package/dist/react/useRiffleJsonHook.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 +301 -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 +14 -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 +28 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/schema.d.ts +163 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +92 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +546 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +24 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +51 -0
- package/dist/util.js.map +1 -0
- package/package.json +52 -0
- package/src/QueryCache.ts +81 -0
- package/src/__tests__/react/fixture.tsx +106 -0
- package/src/__tests__/react/useLiveStoreComponent.test.tsx +111 -0
- package/src/__tests__/reactive.test.ts +227 -0
- package/src/ambient.d.ts +7 -0
- package/src/backends/base.ts +67 -0
- package/src/backends/index.ts +94 -0
- package/src/backends/noop.ts +32 -0
- package/src/backends/tauri.ts +74 -0
- package/src/backends/utils/idb.ts +71 -0
- package/src/backends/web-in-memory.ts +65 -0
- package/src/backends/web-worker.ts +176 -0
- package/src/backends/web.ts +96 -0
- package/src/bounded-collections.ts +112 -0
- package/src/componentKey.ts +9 -0
- package/src/effect/LiveStore.ts +123 -0
- package/src/effect/index.ts +7 -0
- package/src/events.ts +8 -0
- package/src/inMemoryDatabase.ts +347 -0
- package/src/index.ts +47 -0
- package/src/otel.ts +20 -0
- package/src/react/LiveStoreContext.ts +23 -0
- package/src/react/LiveStoreProvider.tsx +93 -0
- package/src/react/index.ts +11 -0
- package/src/react/useGlobalQuery.ts +40 -0
- package/src/react/useGraphQL.ts +113 -0
- package/src/react/useLiveStoreComponent.ts +493 -0
- package/src/react/utils/useStateRefWithReactiveInput.ts +51 -0
- package/src/reactive.ts +538 -0
- package/src/reactiveQueries/base-class.ts +49 -0
- package/src/reactiveQueries/graphql.ts +52 -0
- package/src/reactiveQueries/js.ts +38 -0
- package/src/reactiveQueries/sql.ts +65 -0
- package/src/schema.ts +219 -0
- package/src/store.ts +889 -0
- package/src/util.ts +59 -0
- package/tsconfig.json +15 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a map that has a fixed number of entries.
|
|
3
|
+
* Once hitting the bound, earliest insertions are removed
|
|
4
|
+
*/
|
|
5
|
+
export default class BoundMap<K, V> {
|
|
6
|
+
#map = new Map<K, V>()
|
|
7
|
+
#sizeLimit: number
|
|
8
|
+
|
|
9
|
+
constructor(sizeLimit: number) {
|
|
10
|
+
this.#sizeLimit = sizeLimit
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onEvict: ((key: K) => void) | undefined
|
|
14
|
+
|
|
15
|
+
set = (key: K, value: V) => {
|
|
16
|
+
this.#map.set(key, value)
|
|
17
|
+
// console.log(this.#map.size, this.#sizeLimit);
|
|
18
|
+
if (this.#map.size > this.#sizeLimit) {
|
|
19
|
+
const firstKey = this.#map.keys().next().value
|
|
20
|
+
this.#map.delete(firstKey)
|
|
21
|
+
if (this.onEvict) {
|
|
22
|
+
this.onEvict(firstKey)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get = (key: K): V | undefined => {
|
|
28
|
+
return this.#map.get(key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete = (key: K) => {
|
|
32
|
+
this.#map.delete(key)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
keys = () => {
|
|
36
|
+
return this.#map.keys()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class BoundSet<V> {
|
|
41
|
+
#map: BoundMap<V, V>
|
|
42
|
+
|
|
43
|
+
constructor(sizeLimit: number) {
|
|
44
|
+
this.#map = new BoundMap(sizeLimit)
|
|
45
|
+
this.#map.onEvict = this.#onEvict
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#onEvict = (v: V) => {
|
|
49
|
+
if (this.onEvict) {
|
|
50
|
+
this.onEvict(v)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onEvict: ((key: V) => void) | undefined
|
|
55
|
+
|
|
56
|
+
add = (v: V) => {
|
|
57
|
+
this.#map.set(v, v)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
[Symbol.iterator] = () => {
|
|
61
|
+
return this.#map.keys()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class BoundArray<V> {
|
|
66
|
+
#array: V[] = []
|
|
67
|
+
#sizeLimit: number
|
|
68
|
+
|
|
69
|
+
constructor(sizeLimit: number) {
|
|
70
|
+
this.#sizeLimit = sizeLimit
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onEvict: ((key: V) => void) | undefined
|
|
74
|
+
|
|
75
|
+
push = (v: V) => {
|
|
76
|
+
this.#array.push(v)
|
|
77
|
+
if (this.#array.length > this.#sizeLimit) {
|
|
78
|
+
const first = this.#array.shift()
|
|
79
|
+
if (first && this.onEvict) {
|
|
80
|
+
this.onEvict(first)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get = (index: number): V | undefined => {
|
|
86
|
+
return this.#array[index]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
delete = (index: number) => {
|
|
90
|
+
this.#array.splice(index, 1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get length() {
|
|
94
|
+
return this.#array.length
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[Symbol.iterator] = () => {
|
|
98
|
+
return this.#array[Symbol.iterator]()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
map = <T>(fn: (v: V) => T): T[] => {
|
|
102
|
+
return this.#array.map(fn)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clear = () => {
|
|
106
|
+
this.#array = []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sort = (fn?: (a: V, b: V) => number) => {
|
|
110
|
+
return this.#array.sort(fn)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type SingletonKey = { _tag: 'singleton'; componentName: string; id: 'singleton' }
|
|
2
|
+
type EphemeralKey = { _tag: 'ephemeral'; componentName: string; id: string }
|
|
3
|
+
type CustomKey = { _tag: 'custom'; componentName: string; id: string }
|
|
4
|
+
|
|
5
|
+
export type ComponentKey = SingletonKey | EphemeralKey | CustomKey
|
|
6
|
+
|
|
7
|
+
export const labelForKey = (key: ComponentKey): string => `${key.componentName}/${key.id}`
|
|
8
|
+
|
|
9
|
+
export const tableNameForComponentKey = (componentKey: ComponentKey) => `components__${componentKey.componentName}`
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Scope } from '@livestore/utils/effect'
|
|
2
|
+
import { Context, Deferred, Duration, Effect, Layer, Otel, pipe } from '@livestore/utils/effect'
|
|
3
|
+
import type * as otel from '@opentelemetry/api'
|
|
4
|
+
import type { GraphQLSchema } from 'graphql'
|
|
5
|
+
import { mapValues } from 'lodash-es'
|
|
6
|
+
|
|
7
|
+
import type { Backend, BackendOptions } from '../backends/index.js'
|
|
8
|
+
import type { InMemoryDatabase } from '../inMemoryDatabase.js'
|
|
9
|
+
import type { Schema } from '../schema.js'
|
|
10
|
+
import type { BaseGraphQLContext, GraphQLOptions, LiveStoreQuery, Store } from '../store.js'
|
|
11
|
+
import { createStore } from '../store.js'
|
|
12
|
+
|
|
13
|
+
export type LiveStoreContext = {
|
|
14
|
+
store: Store<any>
|
|
15
|
+
globalQueries: LiveStoreQueryTypes
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type QueryDefinition = (store: Store<any>) => LiveStoreQuery
|
|
19
|
+
export type GlobalQueryDefs = { [key: string]: QueryDefinition }
|
|
20
|
+
|
|
21
|
+
export type LiveStoreCreateStoreOptions<GraphQLContext extends BaseGraphQLContext> = {
|
|
22
|
+
schema: Schema
|
|
23
|
+
globalQueryDefs: GlobalQueryDefs
|
|
24
|
+
backendOptions: BackendOptions
|
|
25
|
+
graphQLOptions?: GraphQLOptions<GraphQLContext>
|
|
26
|
+
otelTracer?: otel.Tracer
|
|
27
|
+
otelRootSpanContext?: otel.Context
|
|
28
|
+
boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const LiveStoreContext = Context.Tag<LiveStoreContext>('@livestore/livestore/LiveStoreContext')
|
|
32
|
+
|
|
33
|
+
export type DeferredStoreContext = Deferred.Deferred<never, LiveStoreContext>
|
|
34
|
+
export const DeferredStoreContext = Context.Tag<DeferredStoreContext>(
|
|
35
|
+
Symbol.for('@livestore/livestore/DeferredStoreContext'),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// export const DeferredStoreContext = Effect.cached(Effect.flatMap(StoreContext, (_) => Effect.succeed(_)))
|
|
39
|
+
|
|
40
|
+
export type LiveStoreContextProps<GraphQLContext extends BaseGraphQLContext> = {
|
|
41
|
+
schema: Schema
|
|
42
|
+
globalQueryDefs?: Effect.Effect<Otel.Tracer | Otel.Span, never, GlobalQueryDefs>
|
|
43
|
+
backendOptions: Effect.Effect<Otel.Tracer | Otel.Span, never, BackendOptions>
|
|
44
|
+
graphQLOptions?: {
|
|
45
|
+
schema: Effect.Effect<Otel.Tracer, never, GraphQLSchema>
|
|
46
|
+
makeContext: (db: InMemoryDatabase) => GraphQLContext
|
|
47
|
+
}
|
|
48
|
+
boot?: (backend: Backend) => Effect.Effect<Otel.Tracer, never, void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LiveStoreContextLayer = <GraphQLContext extends BaseGraphQLContext>(
|
|
52
|
+
props: LiveStoreContextProps<GraphQLContext>,
|
|
53
|
+
): Layer.Layer<Otel.Tracer, never, LiveStoreContext> =>
|
|
54
|
+
Layer.provide(
|
|
55
|
+
LiveStoreContextDeferred,
|
|
56
|
+
pipe(Layer.scoped(LiveStoreContext, makeLiveStoreContext(props)), Otel.withSpanLayer('LiveStoreContext')),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export const LiveStoreContextDeferred = Layer.effect(DeferredStoreContext, Deferred.make<never, LiveStoreContext>())
|
|
60
|
+
|
|
61
|
+
export const makeLiveStoreContext = <GraphQLContext extends BaseGraphQLContext>({
|
|
62
|
+
globalQueryDefs,
|
|
63
|
+
schema,
|
|
64
|
+
backendOptions: backendOptions_,
|
|
65
|
+
graphQLOptions: graphQLOptions_,
|
|
66
|
+
boot: boot_,
|
|
67
|
+
}: LiveStoreContextProps<GraphQLContext>): Effect.Effect<
|
|
68
|
+
Otel.Tracer | Otel.Span | DeferredStoreContext | Scope.Scope,
|
|
69
|
+
never,
|
|
70
|
+
LiveStoreContext
|
|
71
|
+
> =>
|
|
72
|
+
pipe(
|
|
73
|
+
Effect.gen(function* ($) {
|
|
74
|
+
const ctx = yield* $(Effect.context<Otel.Tracer>())
|
|
75
|
+
const otelRootSpanContext = yield* $(Otel.activeContext)
|
|
76
|
+
const { tracer: otelTracer } = yield* $(Otel.Tracer)
|
|
77
|
+
|
|
78
|
+
const graphQLOptions = yield* $(
|
|
79
|
+
graphQLOptions_
|
|
80
|
+
? Effect.all({ schema: graphQLOptions_.schema, makeContext: Effect.succeed(graphQLOptions_.makeContext) })
|
|
81
|
+
: Effect.succeed(undefined),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const backendOptions = yield* $(backendOptions_ ?? Effect.succeed(undefined))
|
|
85
|
+
|
|
86
|
+
const boot = boot_
|
|
87
|
+
? (db: Backend) => pipe(boot_(db), Effect.provideContext(ctx), Effect.tapCauseLogPretty, Effect.runPromise)
|
|
88
|
+
: undefined
|
|
89
|
+
|
|
90
|
+
const store = yield* $(
|
|
91
|
+
Effect.tryPromise(() =>
|
|
92
|
+
createStore({
|
|
93
|
+
schema,
|
|
94
|
+
backendOptions,
|
|
95
|
+
graphQLOptions,
|
|
96
|
+
otelTracer,
|
|
97
|
+
otelRootSpanContext,
|
|
98
|
+
boot,
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
Effect.acquireRelease((store) => Effect.sync(() => store.destroy())),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
window.__debugLiveStore = store
|
|
105
|
+
|
|
106
|
+
const globalQueries = yield* $(
|
|
107
|
+
globalQueryDefs ?? Effect.succeed({}),
|
|
108
|
+
Effect.map((defs) => mapValues(defs, (queryDef) => queryDef(store))),
|
|
109
|
+
Otel.withSpan('LiveStore:makeGlobalQueries', {}, store.otel.queriesSpanContext),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// NOTE give main thread a chance to render
|
|
113
|
+
yield* $(Effect.yieldNow())
|
|
114
|
+
|
|
115
|
+
return { store, globalQueries }
|
|
116
|
+
}),
|
|
117
|
+
Effect.tap((storeCtx) => Effect.flatMap(DeferredStoreContext, (def) => Deferred.succeed(def, storeCtx))),
|
|
118
|
+
Effect.timeoutFail({
|
|
119
|
+
onTimeout: () => new Error('Timed out while creating LiveStore store after 10sec'),
|
|
120
|
+
duration: Duration.seconds(10),
|
|
121
|
+
}),
|
|
122
|
+
Effect.orDie,
|
|
123
|
+
)
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
2
|
+
|
|
3
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
4
|
+
import { identity } from '@livestore/utils/effect'
|
|
5
|
+
import type * as otel from '@opentelemetry/api'
|
|
6
|
+
import type * as SqliteWasm from 'sqlite-esm'
|
|
7
|
+
import initSqlJs from 'sqlite-esm'
|
|
8
|
+
|
|
9
|
+
import BoundMap, { BoundArray } from './bounded-collections.js'
|
|
10
|
+
import type { LiveStoreEvent } from './events.js'
|
|
11
|
+
// import { EVENTS_TABLE_NAME } from './events.js'
|
|
12
|
+
import { sql } from './index.js'
|
|
13
|
+
import { getDurationMsFromSpan, getStartTimeHighResFromSpan } from './otel.js'
|
|
14
|
+
import QueryCache from './QueryCache.js'
|
|
15
|
+
import type { ActionDefinition } from './schema.js'
|
|
16
|
+
import type { Bindable, ParamsObject } from './util.js'
|
|
17
|
+
import { prepareBindValues } from './util.js'
|
|
18
|
+
|
|
19
|
+
export enum IndexType {
|
|
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 }
|
|
31
|
+
|
|
32
|
+
export interface DebugInfo {
|
|
33
|
+
slowQueries: BoundArray<SlowQueryInfo>
|
|
34
|
+
queryFrameDuration: number
|
|
35
|
+
queryFrameCount: number
|
|
36
|
+
events: BoundArray<[queryStr: string, bindValues: Bindable | undefined]>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SlowQueryInfo = [
|
|
40
|
+
queryStr: string,
|
|
41
|
+
bindValues: Bindable | undefined,
|
|
42
|
+
durationMs: number,
|
|
43
|
+
rowsCount: number | undefined,
|
|
44
|
+
queriedTables: string[],
|
|
45
|
+
startTimePerfNow: DOMHighResTimeStamp,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export const emptyDebugInfo = (): DebugInfo => ({
|
|
49
|
+
slowQueries: new BoundArray(200),
|
|
50
|
+
queryFrameDuration: 0,
|
|
51
|
+
queryFrameCount: 0,
|
|
52
|
+
events: new BoundArray(1000),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export class InMemoryDatabase {
|
|
56
|
+
// TODO: how many unique active statements are expected?
|
|
57
|
+
private cachedStmts = new BoundMap<string, SqliteWasm.PreparedStatement>(200)
|
|
58
|
+
private tablesUsedCache = new BoundMap<string, string[]>(200)
|
|
59
|
+
private resultCache = new QueryCache()
|
|
60
|
+
public debugInfo: DebugInfo = emptyDebugInfo()
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private db: DatabaseWithCAPI,
|
|
64
|
+
private otelTracer: otel.Tracer,
|
|
65
|
+
private otelRootSpanContext: otel.Context,
|
|
66
|
+
public SQL: SqliteWasm.Sqlite3Static,
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
static async load(
|
|
70
|
+
data: Uint8Array | undefined,
|
|
71
|
+
otelTracer: otel.Tracer,
|
|
72
|
+
otelRootSpanContext: otel.Context,
|
|
73
|
+
): Promise<InMemoryDatabase> {
|
|
74
|
+
const sqlite3 = await initSqlJs({
|
|
75
|
+
// Required to load the wasm binary asynchronously. Of course, you can host it wherever you want
|
|
76
|
+
// You can omit locateFile completely when running in node
|
|
77
|
+
// locateFile: () => `/sql-wasm.wasm`,
|
|
78
|
+
print: (message) => console.log(`[sql-client] ${message}`),
|
|
79
|
+
printErr: (message) => console.error(`[sql-client] ${message}`),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
|
|
83
|
+
db.capi = sqlite3.capi
|
|
84
|
+
|
|
85
|
+
if (data !== undefined) {
|
|
86
|
+
// Based on https://sqlite.org/forum/forumpost/2119230da8ac5357a13b731f462dc76e08621a4a29724f7906d5f35bb8508465
|
|
87
|
+
// TODO find cleaner way to do this once possible in sqlite3-wasm
|
|
88
|
+
const bytes = data
|
|
89
|
+
const p = sqlite3.wasm.allocFromTypedArray(bytes)
|
|
90
|
+
const _rc = sqlite3.capi.sqlite3_deserialize(
|
|
91
|
+
db.pointer,
|
|
92
|
+
'main',
|
|
93
|
+
p,
|
|
94
|
+
bytes.length,
|
|
95
|
+
bytes.length,
|
|
96
|
+
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE && sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
txn<TRes>(callback: () => TRes): TRes {
|
|
104
|
+
this.execute(sql`begin transaction;`)
|
|
105
|
+
let errored = false
|
|
106
|
+
let result: TRes
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
result = callback()
|
|
110
|
+
} catch (e) {
|
|
111
|
+
errored = true
|
|
112
|
+
this.execute(sql`rollback;`)
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!errored) {
|
|
117
|
+
this.execute(sql`commit;`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getTablesUsed(query: string) {
|
|
124
|
+
const cached = this.tablesUsedCache.get(query)
|
|
125
|
+
if (cached) {
|
|
126
|
+
return cached
|
|
127
|
+
}
|
|
128
|
+
const stmt = this.db.prepare(
|
|
129
|
+
`SELECT tbl_name FROM tables_used(?) AS u JOIN sqlite_master ON sqlite_master.name = u.name WHERE u.schema = 'main';`,
|
|
130
|
+
)
|
|
131
|
+
const tablesUsed = []
|
|
132
|
+
try {
|
|
133
|
+
stmt.bind([query])
|
|
134
|
+
while (stmt.step()) {
|
|
135
|
+
tablesUsed.push(stmt.get(0))
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
stmt.finalize()
|
|
139
|
+
}
|
|
140
|
+
this.tablesUsedCache.set(query, tablesUsed as string[])
|
|
141
|
+
return tablesUsed as string[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* NOTE `execute` is untraced since it's usually called from `applyEvent` which is traced
|
|
146
|
+
*/
|
|
147
|
+
execute(
|
|
148
|
+
query: string,
|
|
149
|
+
bindValues?: ParamsObject,
|
|
150
|
+
writeTables?: string[],
|
|
151
|
+
options?: { hasNoEffects?: boolean },
|
|
152
|
+
): void {
|
|
153
|
+
try {
|
|
154
|
+
let stmt = this.cachedStmts.get(query)
|
|
155
|
+
if (stmt === undefined) {
|
|
156
|
+
stmt = this.db.prepare(query)
|
|
157
|
+
this.cachedStmts.set(query, stmt)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
|
|
161
|
+
stmt.bind(prepareBindValues(bindValues, query))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
stmt.step()
|
|
166
|
+
} finally {
|
|
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
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
|
|
179
|
+
// TODO use write tables instead
|
|
180
|
+
// check what queries actually end up here.
|
|
181
|
+
this.resultCache.invalidate(writeTables ?? this.getTablesUsed(query))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options?.hasNoEffects === true) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
select<T = any>(
|
|
190
|
+
query: string,
|
|
191
|
+
options?: {
|
|
192
|
+
queriedTables?: string[]
|
|
193
|
+
bindValues?: Bindable
|
|
194
|
+
skipCache?: boolean
|
|
195
|
+
parentSpanContext?: otel.Context
|
|
196
|
+
},
|
|
197
|
+
): ReadonlyArray<T> {
|
|
198
|
+
const { queriedTables, bindValues, skipCache = false, parentSpanContext } = options ?? {}
|
|
199
|
+
return this.otelTracer.startActiveSpan(
|
|
200
|
+
'sql-in-memory-select',
|
|
201
|
+
{},
|
|
202
|
+
parentSpanContext ?? this.otelRootSpanContext,
|
|
203
|
+
(span) => {
|
|
204
|
+
try {
|
|
205
|
+
span.setAttribute('sql.query', query)
|
|
206
|
+
|
|
207
|
+
const key = this.resultCache.getKey(query, bindValues)
|
|
208
|
+
const cachedResult = this.resultCache.get(key)
|
|
209
|
+
if (skipCache === false && cachedResult !== undefined) {
|
|
210
|
+
span.setAttribute('sql.rowsCount', cachedResult.length)
|
|
211
|
+
span.setAttribute('sql.cached', true)
|
|
212
|
+
span.end()
|
|
213
|
+
return cachedResult
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let stmt = this.cachedStmts.get(query)
|
|
217
|
+
if (stmt === undefined) {
|
|
218
|
+
stmt = this.db.prepare(query)
|
|
219
|
+
this.cachedStmts.set(query, stmt)
|
|
220
|
+
}
|
|
221
|
+
if (bindValues) {
|
|
222
|
+
stmt.bind(bindValues ?? {})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result: T[] = []
|
|
226
|
+
try {
|
|
227
|
+
const columns = stmt.getColumnNames()
|
|
228
|
+
while (stmt.step()) {
|
|
229
|
+
const obj: { [key: string]: any } = {}
|
|
230
|
+
for (const [i, c] of columns.entries()) {
|
|
231
|
+
obj[c] = stmt.get(i)
|
|
232
|
+
}
|
|
233
|
+
result.push(obj as unknown as T)
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
// we're caching statements in this iteration. do not free.
|
|
237
|
+
// stmt.free();
|
|
238
|
+
// reset the cached statement so we can use it again in the future
|
|
239
|
+
stmt.reset()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
span.setAttribute('sql.rowsCount', result.length)
|
|
243
|
+
span.setAttribute('sql.cached', false)
|
|
244
|
+
|
|
245
|
+
const queriedTables_ = queriedTables ?? this.getTablesUsed(query)
|
|
246
|
+
this.resultCache.set(queriedTables_, key, result)
|
|
247
|
+
|
|
248
|
+
span.end()
|
|
249
|
+
|
|
250
|
+
const durationMs = getDurationMsFromSpan(span)
|
|
251
|
+
|
|
252
|
+
this.debugInfo.queryFrameDuration += durationMs
|
|
253
|
+
this.debugInfo.queryFrameCount++
|
|
254
|
+
|
|
255
|
+
// TODO also enable in non-dev mode
|
|
256
|
+
if (durationMs > 5 && import.meta.env.DEV) {
|
|
257
|
+
this.debugInfo.slowQueries.push([
|
|
258
|
+
query,
|
|
259
|
+
bindValues,
|
|
260
|
+
durationMs,
|
|
261
|
+
result.length,
|
|
262
|
+
queriedTables_,
|
|
263
|
+
getStartTimeHighResFromSpan(span),
|
|
264
|
+
])
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
} catch (e) {
|
|
269
|
+
span.end()
|
|
270
|
+
console.error(query)
|
|
271
|
+
console.error(bindValues)
|
|
272
|
+
shouldNeverHappen(`Error executing select query: ${e} \n ${JSON.stringify({ query, bindValues })}`)
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// TODO move `applyEvent` logic to Store and only call `execute` here
|
|
279
|
+
applyEvent(
|
|
280
|
+
event: LiveStoreEvent,
|
|
281
|
+
eventDefinition: ActionDefinition,
|
|
282
|
+
parentSpanContext: otel.Context,
|
|
283
|
+
): { durationMs: number } {
|
|
284
|
+
return this.otelTracer.startActiveSpan('livestore.in-memory-db:applyEvent', {}, parentSpanContext, (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
|
+
export() {
|
|
340
|
+
// Clear statement cache because exporting frees statements
|
|
341
|
+
for (const key of this.cachedStmts.keys()) {
|
|
342
|
+
this.cachedStmts.delete(key)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return this.db.capi.sqlite3_js_db_export(this.db.pointer)
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { Store, createStore, RESET_DB_LOCAL_STORAGE_KEY } from './store.js'
|
|
2
|
+
export type { LiveStoreQuery, BaseGraphQLContext, QueryResult, QueryDebugInfo, RefreshReason } from './store.js'
|
|
3
|
+
|
|
4
|
+
export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
defineComponentStateSchema,
|
|
8
|
+
EVENT_CURSOR_TABLE,
|
|
9
|
+
defineSchema,
|
|
10
|
+
defineAction,
|
|
11
|
+
defineActions,
|
|
12
|
+
defineTables,
|
|
13
|
+
defineMaterializedViews,
|
|
14
|
+
} from './schema.js'
|
|
15
|
+
export { InMemoryDatabase, type DebugInfo, emptyDebugInfo } from './inMemoryDatabase.js'
|
|
16
|
+
export { createBackend, IndexType } from './backends/index.js'
|
|
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'
|
|
21
|
+
export type {
|
|
22
|
+
GetAtom,
|
|
23
|
+
AtomDebugInfo,
|
|
24
|
+
RefreshDebugInfo,
|
|
25
|
+
RefreshReasonWithGenericReasons,
|
|
26
|
+
SerializedAtom,
|
|
27
|
+
SerializedEffect,
|
|
28
|
+
} from './reactive.js'
|
|
29
|
+
export type { LiveStoreJSQuery } from './reactiveQueries/js.js'
|
|
30
|
+
export type { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
|
|
31
|
+
export type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
|
|
32
|
+
|
|
33
|
+
export { labelForKey } from './componentKey.js'
|
|
34
|
+
export type { ComponentKey } from './componentKey.js'
|
|
35
|
+
export type {
|
|
36
|
+
Schema,
|
|
37
|
+
TableDefinition,
|
|
38
|
+
GetActionArgs,
|
|
39
|
+
GetApplyEventArgs,
|
|
40
|
+
ColumnDefinition,
|
|
41
|
+
Index,
|
|
42
|
+
ActionDefinition,
|
|
43
|
+
ActionDefinitions,
|
|
44
|
+
} from './schema.js'
|
|
45
|
+
|
|
46
|
+
export { sql, type Bindable } from './util.js'
|
|
47
|
+
export { isEqual } from 'lodash-es'
|
package/src/otel.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
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
|
+
export const getDurationMsFromSpan = (span: otel.Span): number => {
|
|
15
|
+
const durationHr: [seconds: number, nanos: number] = (span as any)._duration
|
|
16
|
+
return durationHr[0] * 1000 + durationHr[1] / 1_000_000
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const getStartTimeHighResFromSpan = (span: otel.Span): DOMHighResTimeStamp =>
|
|
20
|
+
(span as any)._performanceStartTime as DOMHighResTimeStamp
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { LiveStoreContext as LiveStoreContext_ } from '../effect/LiveStore.js'
|
|
4
|
+
import type { LiveStoreQuery } from '../store.js'
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
// NOTE Can be extended
|
|
8
|
+
interface LiveStoreQueryTypes {
|
|
9
|
+
[key: string]: LiveStoreQuery
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const LiveStoreContext = React.createContext<LiveStoreContext_ | undefined>(undefined)
|
|
14
|
+
|
|
15
|
+
export const useStore = (): LiveStoreContext_ => {
|
|
16
|
+
const storeContext = useContext(LiveStoreContext)
|
|
17
|
+
|
|
18
|
+
if (storeContext === undefined) {
|
|
19
|
+
throw new Error(`useStore can only be used inside StoreContext.Provider`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return storeContext
|
|
23
|
+
}
|