@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.
Files changed (205) hide show
  1. package/README.md +108 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/LiveRiffleStore.d.ts +42 -0
  4. package/dist/LiveRiffleStore.d.ts.map +1 -0
  5. package/dist/LiveRiffleStore.js +36 -0
  6. package/dist/LiveRiffleStore.js.map +1 -0
  7. package/dist/QueryCache.d.ts +20 -0
  8. package/dist/QueryCache.d.ts.map +1 -0
  9. package/dist/QueryCache.js +71 -0
  10. package/dist/QueryCache.js.map +1 -0
  11. package/dist/__tests__/react/fixture.d.ts +141 -0
  12. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  13. package/dist/__tests__/react/fixture.js +72 -0
  14. package/dist/__tests__/react/fixture.js.map +1 -0
  15. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  19. package/dist/__tests__/react/useRiffleComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useRiffleComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useRiffleComponent.test.js +78 -0
  22. package/dist/__tests__/react/useRiffleComponent.test.js.map +1 -0
  23. package/dist/__tests__/reactive.test.d.ts +2 -0
  24. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  25. package/dist/__tests__/reactive.test.js +167 -0
  26. package/dist/__tests__/reactive.test.js.map +1 -0
  27. package/dist/backends/base.d.ts +13 -0
  28. package/dist/backends/base.d.ts.map +1 -0
  29. package/dist/backends/base.js +53 -0
  30. package/dist/backends/base.js.map +1 -0
  31. package/dist/backends/index.d.ts +41 -0
  32. package/dist/backends/index.d.ts.map +1 -0
  33. package/dist/backends/index.js +38 -0
  34. package/dist/backends/index.js.map +1 -0
  35. package/dist/backends/noop.d.ts +18 -0
  36. package/dist/backends/noop.d.ts.map +1 -0
  37. package/dist/backends/noop.js +21 -0
  38. package/dist/backends/noop.js.map +1 -0
  39. package/dist/backends/tauri.d.ts +24 -0
  40. package/dist/backends/tauri.d.ts.map +1 -0
  41. package/dist/backends/tauri.js +48 -0
  42. package/dist/backends/tauri.js.map +1 -0
  43. package/dist/backends/utils/idb.d.ts +10 -0
  44. package/dist/backends/utils/idb.d.ts.map +1 -0
  45. package/dist/backends/utils/idb.js +58 -0
  46. package/dist/backends/utils/idb.js.map +1 -0
  47. package/dist/backends/web-in-memory.d.ts +24 -0
  48. package/dist/backends/web-in-memory.d.ts.map +1 -0
  49. package/dist/backends/web-in-memory.js +46 -0
  50. package/dist/backends/web-in-memory.js.map +1 -0
  51. package/dist/backends/web-worker.d.ts +17 -0
  52. package/dist/backends/web-worker.d.ts.map +1 -0
  53. package/dist/backends/web-worker.js +139 -0
  54. package/dist/backends/web-worker.js.map +1 -0
  55. package/dist/backends/web.d.ts +28 -0
  56. package/dist/backends/web.d.ts.map +1 -0
  57. package/dist/backends/web.js +64 -0
  58. package/dist/backends/web.js.map +1 -0
  59. package/dist/bounded-collections.d.ts +34 -0
  60. package/dist/bounded-collections.d.ts.map +1 -0
  61. package/dist/bounded-collections.js +103 -0
  62. package/dist/bounded-collections.js.map +1 -0
  63. package/dist/componentKey.d.ts +20 -0
  64. package/dist/componentKey.d.ts.map +1 -0
  65. package/dist/componentKey.js +3 -0
  66. package/dist/componentKey.js.map +1 -0
  67. package/dist/effect/LiveStore.d.ts +42 -0
  68. package/dist/effect/LiveStore.d.ts.map +1 -0
  69. package/dist/effect/LiveStore.js +36 -0
  70. package/dist/effect/LiveStore.js.map +1 -0
  71. package/dist/effect/index.d.ts +2 -0
  72. package/dist/effect/index.d.ts.map +1 -0
  73. package/dist/effect/index.js +2 -0
  74. package/dist/effect/index.js.map +1 -0
  75. package/dist/events.d.ts +7 -0
  76. package/dist/events.d.ts.map +1 -0
  77. package/dist/events.js +2 -0
  78. package/dist/events.js.map +1 -0
  79. package/dist/inMemoryDatabase.d.ts +65 -0
  80. package/dist/inMemoryDatabase.d.ts.map +1 -0
  81. package/dist/inMemoryDatabase.js +241 -0
  82. package/dist/inMemoryDatabase.js.map +1 -0
  83. package/dist/index.d.ts +20 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +10 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/otel.d.ts +5 -0
  88. package/dist/otel.d.ts.map +1 -0
  89. package/dist/otel.js +17 -0
  90. package/dist/otel.js.map +1 -0
  91. package/dist/react/LiveStoreContext.d.ts +11 -0
  92. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  93. package/dist/react/LiveStoreContext.js +10 -0
  94. package/dist/react/LiveStoreContext.js.map +1 -0
  95. package/dist/react/LiveStoreProvider.d.ts +21 -0
  96. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  97. package/dist/react/LiveStoreProvider.js +48 -0
  98. package/dist/react/LiveStoreProvider.js.map +1 -0
  99. package/dist/react/RiffleProvider.d.ts +21 -0
  100. package/dist/react/RiffleProvider.d.ts.map +1 -0
  101. package/dist/react/RiffleProvider.js +48 -0
  102. package/dist/react/RiffleProvider.js.map +1 -0
  103. package/dist/react/StoreContext.d.ts +11 -0
  104. package/dist/react/StoreContext.d.ts.map +1 -0
  105. package/dist/react/StoreContext.js +10 -0
  106. package/dist/react/StoreContext.js.map +1 -0
  107. package/dist/react/index.d.ts +7 -0
  108. package/dist/react/index.d.ts.map +1 -0
  109. package/dist/react/index.js +6 -0
  110. package/dist/react/index.js.map +1 -0
  111. package/dist/react/useGlobalQuery.d.ts +3 -0
  112. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  113. package/dist/react/useGlobalQuery.js +25 -0
  114. package/dist/react/useGlobalQuery.js.map +1 -0
  115. package/dist/react/useGraphQL.d.ts +11 -0
  116. package/dist/react/useGraphQL.d.ts.map +1 -0
  117. package/dist/react/useGraphQL.js +68 -0
  118. package/dist/react/useGraphQL.js.map +1 -0
  119. package/dist/react/useLiveStoreComponent.d.ts +70 -0
  120. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  121. package/dist/react/useLiveStoreComponent.js +261 -0
  122. package/dist/react/useLiveStoreComponent.js.map +1 -0
  123. package/dist/react/useRiffleComponent.d.ts +70 -0
  124. package/dist/react/useRiffleComponent.d.ts.map +1 -0
  125. package/dist/react/useRiffleComponent.js +261 -0
  126. package/dist/react/useRiffleComponent.js.map +1 -0
  127. package/dist/react/useRiffleJsonHook.d.ts +4 -0
  128. package/dist/react/useRiffleJsonHook.d.ts.map +1 -0
  129. package/dist/react/useRiffleJsonHook.js +21 -0
  130. package/dist/react/useRiffleJsonHook.js.map +1 -0
  131. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  132. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  133. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  134. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  135. package/dist/reactive.d.ts +140 -0
  136. package/dist/reactive.d.ts.map +1 -0
  137. package/dist/reactive.js +301 -0
  138. package/dist/reactive.js.map +1 -0
  139. package/dist/reactiveQueries/base-class.d.ts +24 -0
  140. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  141. package/dist/reactiveQueries/base-class.js +22 -0
  142. package/dist/reactiveQueries/base-class.js.map +1 -0
  143. package/dist/reactiveQueries/graphql.d.ts +25 -0
  144. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/graphql.js +14 -0
  146. package/dist/reactiveQueries/graphql.js.map +1 -0
  147. package/dist/reactiveQueries/js.d.ts +19 -0
  148. package/dist/reactiveQueries/js.d.ts.map +1 -0
  149. package/dist/reactiveQueries/js.js +13 -0
  150. package/dist/reactiveQueries/js.js.map +1 -0
  151. package/dist/reactiveQueries/sql.d.ts +31 -0
  152. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  153. package/dist/reactiveQueries/sql.js +28 -0
  154. package/dist/reactiveQueries/sql.js.map +1 -0
  155. package/dist/schema.d.ts +163 -0
  156. package/dist/schema.d.ts.map +1 -0
  157. package/dist/schema.js +92 -0
  158. package/dist/schema.js.map +1 -0
  159. package/dist/store.d.ts +175 -0
  160. package/dist/store.d.ts.map +1 -0
  161. package/dist/store.js +546 -0
  162. package/dist/store.js.map +1 -0
  163. package/dist/util.d.ts +24 -0
  164. package/dist/util.d.ts.map +1 -0
  165. package/dist/util.js +51 -0
  166. package/dist/util.js.map +1 -0
  167. package/package.json +52 -0
  168. package/src/QueryCache.ts +81 -0
  169. package/src/__tests__/react/fixture.tsx +106 -0
  170. package/src/__tests__/react/useLiveStoreComponent.test.tsx +111 -0
  171. package/src/__tests__/reactive.test.ts +227 -0
  172. package/src/ambient.d.ts +7 -0
  173. package/src/backends/base.ts +67 -0
  174. package/src/backends/index.ts +94 -0
  175. package/src/backends/noop.ts +32 -0
  176. package/src/backends/tauri.ts +74 -0
  177. package/src/backends/utils/idb.ts +71 -0
  178. package/src/backends/web-in-memory.ts +65 -0
  179. package/src/backends/web-worker.ts +176 -0
  180. package/src/backends/web.ts +96 -0
  181. package/src/bounded-collections.ts +112 -0
  182. package/src/componentKey.ts +9 -0
  183. package/src/effect/LiveStore.ts +123 -0
  184. package/src/effect/index.ts +7 -0
  185. package/src/events.ts +8 -0
  186. package/src/inMemoryDatabase.ts +347 -0
  187. package/src/index.ts +47 -0
  188. package/src/otel.ts +20 -0
  189. package/src/react/LiveStoreContext.ts +23 -0
  190. package/src/react/LiveStoreProvider.tsx +93 -0
  191. package/src/react/index.ts +11 -0
  192. package/src/react/useGlobalQuery.ts +40 -0
  193. package/src/react/useGraphQL.ts +113 -0
  194. package/src/react/useLiveStoreComponent.ts +493 -0
  195. package/src/react/utils/useStateRefWithReactiveInput.ts +51 -0
  196. package/src/reactive.ts +538 -0
  197. package/src/reactiveQueries/base-class.ts +49 -0
  198. package/src/reactiveQueries/graphql.ts +52 -0
  199. package/src/reactiveQueries/js.ts +38 -0
  200. package/src/reactiveQueries/sql.ts +65 -0
  201. package/src/schema.ts +219 -0
  202. package/src/store.ts +889 -0
  203. package/src/util.ts +59 -0
  204. package/tsconfig.json +15 -0
  205. 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
+ )
@@ -0,0 +1,7 @@
1
+ export {
2
+ LiveStoreContextLayer,
3
+ LiveStoreContext,
4
+ LiveStoreContextDeferred,
5
+ DeferredStoreContext,
6
+ type LiveStoreContextProps,
7
+ } from './LiveStore.js'
package/src/events.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Todo: typesafe events
2
+ export type LiveStoreEvent = {
3
+ type: string
4
+ id: string
5
+ args?: any
6
+ }
7
+
8
+ export const EVENTS_TABLE_NAME = '__events'
@@ -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
+ }