@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
package/dist/util.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * This is a tag function for tagged literals.
3
+ * it lets us get syntax highlighting on SQL queries in VSCode, but
4
+ * doesn't do anything at runtime.
5
+ * Code copied from: https://esdiscuss.org/topic/string-identity-template-tag
6
+ */
7
+ export const sql = (template, ...args) => {
8
+ let str = '';
9
+ for (const [i, arg] of args.entries()) {
10
+ str += template[i] + String(arg);
11
+ }
12
+ return str + template.at(-1);
13
+ };
14
+ /** Prepare bind values to send to SQLite
15
+ /* Add $ to the beginning of keys; which we use as our interpolation syntax
16
+ /* We also strip out any params that aren't used in the statement,
17
+ /* because rusqlite doesn't allow unused named params
18
+ /* TODO: Search for unused params via proper parsing, not string search
19
+ **/
20
+ export const prepareBindValues = (values, statement) => {
21
+ const result = {};
22
+ for (const [key, value] of Object.entries(values)) {
23
+ if (statement.includes(key)) {
24
+ result[`$${key}`] = value;
25
+ }
26
+ }
27
+ return result;
28
+ };
29
+ /**
30
+ * Use this to make assertion at end of if-else chain that all members of a
31
+ * union have been accounted for.
32
+ */
33
+ /* eslint-disable-next-line prefer-arrow/prefer-arrow-functions */
34
+ export function casesHandled(x) {
35
+ throw new Error(`A case was not handled for value: ${objectToString(x)}`);
36
+ }
37
+ export const objectToString = (error) => {
38
+ const stack = typeof process !== 'undefined' && process.env.CL_DEBUG ? error.stack : undefined;
39
+ const str = error.toString();
40
+ const stackStr = stack ? `\n${stack}` : '';
41
+ if (str !== '[object Object]')
42
+ return str + stackStr;
43
+ try {
44
+ return JSON.stringify({ ...error, stack }, null, 2);
45
+ }
46
+ catch (e) {
47
+ console.log(error);
48
+ return 'Error while printing error: ' + e;
49
+ }
50
+ };
51
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,QAA8B,EAAE,GAAG,IAAe,EAAU,EAAE;IAChF,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE;QACrC,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;KACjC;IACD,OAAO,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAoB,EAAE,SAAiB,EAAgB,EAAE;IACzF,MAAM,MAAM,GAAiB,EAAE,CAAA;IAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;YAC3B,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA;SAC1B;KACF;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;GAGG;AACH,kEAAkE;AAClE,MAAM,UAAU,YAAY,CAAC,CAAQ;IACnC,MAAM,IAAI,KAAK,CAAC,qCAAqC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AAC3E,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAU,EAAU,EAAE;IACnD,MAAM,KAAK,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IAC9F,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1C,IAAI,GAAG,KAAK,iBAAiB;QAAE,OAAO,GAAG,GAAG,QAAQ,CAAA;IAEpD,IAAI;QACF,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;KACpD;IAAC,OAAO,CAAM,EAAE;QACf,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAElB,OAAO,8BAA8B,GAAG,CAAC,CAAA;KAC1C;AACH,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@livestore/livestore",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./dist/index.js",
7
+ "./react": "./dist/react/index.js",
8
+ "./util": "./dist/util.js"
9
+ },
10
+ "types": "./dist/index.d.ts",
11
+ "typesVersions": {
12
+ "*": {
13
+ "react": [
14
+ "./dist/react/index.d.ts"
15
+ ],
16
+ "util": [
17
+ "./dist/util.d.ts"
18
+ ]
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "test": "vitest"
24
+ },
25
+ "dependencies": {
26
+ "@graphql-typed-document-node/core": "^3.2.0",
27
+ "@livestore/utils": "0.0.0",
28
+ "@opentelemetry/api": "^1.4.1",
29
+ "comlink": "^4.4.1",
30
+ "graphql": "^16.8.0",
31
+ "lodash-es": "^4.17.21",
32
+ "sqlite-esm": "3.42.0-build6",
33
+ "uuid": "^9.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@tauri-apps/api": "=1.0.0-rc.1",
37
+ "react": "^17",
38
+ "react-dom": "^17"
39
+ },
40
+ "devDependencies": {
41
+ "@tauri-apps/api": "^1.4.0",
42
+ "@testing-library/react": "^14.0.0",
43
+ "@types/lodash-es": "^4.17.9",
44
+ "@types/uuid": "^9.0.3",
45
+ "jsdom": "^22.1.0",
46
+ "react": "^18.2.0",
47
+ "react-dom": "^18.2.0",
48
+ "typescript": "5.2.2",
49
+ "vite": "4.4.9",
50
+ "vitest": "^0.34.3"
51
+ }
52
+ }
@@ -0,0 +1,81 @@
1
+ import BoundMap, { BoundSet } from './bounded-collections.js'
2
+ import type { Bindable } from './util.js'
3
+
4
+ type Opaque<BaseType, BrandType = unknown> = BaseType & {
5
+ readonly [Symbols.base]: BaseType
6
+ readonly [Symbols.brand]: BrandType
7
+ }
8
+
9
+ namespace Symbols {
10
+ export declare const base: unique symbol
11
+ export declare const brand: unique symbol
12
+ }
13
+
14
+ export type CacheKey = Opaque<string, string>
15
+ type TableName = string
16
+
17
+ const ignore = ['begin', 'rollback', 'commit', 'savepoint', 'release']
18
+
19
+ // TODO: profile to see how big we need this cache to be.
20
+ const cacheSize = 200
21
+ export default class QueryCache {
22
+ #entries = new BoundMap<CacheKey, any>(cacheSize)
23
+ #dependencies = new Map<TableName, BoundSet<CacheKey>>()
24
+
25
+ getKey = (sql: string, bindValues?: Bindable): CacheKey => {
26
+ if (bindValues == null) {
27
+ return sql as CacheKey
28
+ }
29
+
30
+ if (Array.isArray(bindValues)) {
31
+ return (sql + '\n' + bindValues.join('\n')) as CacheKey
32
+ }
33
+
34
+ return (sql + '\n' + Object.values(bindValues).join('\n')) as CacheKey
35
+ }
36
+
37
+ get = (key: CacheKey) => {
38
+ return this.#entries.get(key)
39
+ }
40
+
41
+ set = (queriedTables: string[], key: CacheKey, results: any) => {
42
+ this.#entries.set(key, results)
43
+ for (const table of queriedTables) {
44
+ let keys = this.#dependencies.get(table)
45
+ if (keys == null) {
46
+ keys = new BoundSet(cacheSize)
47
+ keys.onEvict = this.#dependencyTrackerEvicted
48
+ this.#dependencies.set(table, keys)
49
+ }
50
+ keys.add(key)
51
+ }
52
+ }
53
+
54
+ #dependencyTrackerEvicted = (key: CacheKey) => {
55
+ this.#entries.delete(key)
56
+ }
57
+
58
+ ignoreQuery = (query: string) => {
59
+ return ignore.some((prefix) => query.startsWith(prefix))
60
+ }
61
+
62
+ // The next simplest step is to create a specific implementation for invalidating
63
+ // the expensive track list queries only when constraints data in a write overlaps with read constraints.
64
+ //
65
+ // As well as either:
66
+ // a. removeing the big view (since we'll have our cache)
67
+ // b. incrementally updating the view on insert by the EventImporter
68
+ //
69
+ // We'll not try to tackle any generalized approach until we have a proof of concept working.
70
+ invalidate = (queriedTables: string[]) => {
71
+ for (const table of queriedTables) {
72
+ const keys = this.#dependencies.get(table)
73
+ if (keys == null) {
74
+ continue
75
+ }
76
+ for (const k of keys) {
77
+ this.#entries.delete(k)
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,106 @@
1
+ import { mapObjectValues } from '@livestore/utils'
2
+ import * as otel from '@opentelemetry/api'
3
+ import React from 'react'
4
+
5
+ import * as LiveStore from '../../index.js'
6
+ import { sql } from '../../index.js'
7
+ import * as LiveStoreReact from '../../react/index.js'
8
+
9
+ const mockOtelCtx = otel.context.active()
10
+
11
+ export type Todo = {
12
+ id: string
13
+ text: string | null
14
+ completed: boolean
15
+ }
16
+
17
+ export type Filter = 'all' | 'active' | 'completed'
18
+
19
+ export type AppState = {
20
+ newTodoText: string
21
+ filter: Filter
22
+ }
23
+
24
+ const appState: LiveStore.QueryDefinition = (store) =>
25
+ store
26
+ .querySQL<AppState>(
27
+ () => `select newTodoText, filter from app;`,
28
+ ['app'],
29
+ undefined,
30
+ undefined,
31
+ undefined,
32
+ mockOtelCtx,
33
+ )
34
+ .getFirstRow()
35
+
36
+ export const globalQueryDefs = {
37
+ appState,
38
+ }
39
+
40
+ export const schema = LiveStore.defineSchema({
41
+ tables: {
42
+ todos: {
43
+ columns: {
44
+ id: { type: 'text', primaryKey: true },
45
+ text: { type: 'text', default: '', nullable: false },
46
+ completed: { type: 'boolean', default: false, nullable: false },
47
+ },
48
+ },
49
+ app: {
50
+ columns: {
51
+ id: { type: 'text', primaryKey: true },
52
+ newTodoText: { type: 'text', default: '', nullable: true },
53
+ filter: { type: 'text', default: 'all', nullable: false },
54
+ },
55
+ },
56
+ },
57
+ materializedViews: {},
58
+ actions: {
59
+ // TODO: fix these actions to make them have write annotatinos
60
+ addTodo: {
61
+ statement: {
62
+ sql: sql`INSERT INTO todos (id, text, completed) VALUES ($id, $text, false);`,
63
+ writeTables: ['app'],
64
+ },
65
+ },
66
+ completeTodo: { statement: { sql: sql`UPDATE todos SET completed = true WHERE id = $id;`, writeTables: ['app'] } },
67
+ uncompleteTodo: {
68
+ statement: { sql: sql`UPDATE todos SET completed = false WHERE id = $id;`, writeTables: ['app'] },
69
+ },
70
+ deleteTodo: { statement: { sql: sql`DELETE FROM todos WHERE id = $id;`, writeTables: ['app'] } },
71
+ clearCompleted: { statement: { sql: sql`DELETE FROM todos WHERE completed = true;`, writeTables: ['app'] } },
72
+ updateNewTodoText: { statement: { sql: sql`UPDATE app SET newTodoText = $text;`, writeTables: ['app'] } },
73
+ setFilter: { statement: { sql: sql`UPDATE app SET filter = $filter;`, writeTables: ['app'] } },
74
+ },
75
+ })
76
+
77
+ export const makeTodoMvc = async () => {
78
+ type UserInfoComponentState = { username: string }
79
+
80
+ const AppSchema = LiveStore.defineComponentStateSchema<UserInfoComponentState>({
81
+ componentType: 'UserInfo',
82
+ columns: {
83
+ username: { type: 'text', default: '' },
84
+ },
85
+ })
86
+
87
+ const store = await LiveStore.createStore({
88
+ schema,
89
+ backendOptions: { type: 'web-in-memory' },
90
+ boot: async (backend) => {
91
+ backend.execute(sql`INSERT INTO app (newTodoText, filter) VALUES ('', 'all');`)
92
+ // NOTE we can't insert into components__UserInfo yet because the table doesn't exist yet
93
+ // backend.execute(sql`INSERT INTO components__UserInfo (id, username) VALUES ('u1', 'username_u1');`)
94
+ // backend.execute(sql`INSERT INTO components__UserInfo (id, username) VALUES ('u2', 'username_u2');`)
95
+ },
96
+ })
97
+
98
+ const globalQueries = mapObjectValues(globalQueryDefs, (_, queryDef) => queryDef(store))
99
+ const storeContext: LiveStore.LiveStoreContext = { store, globalQueries }
100
+
101
+ const wrapper = ({ children }: any) => (
102
+ <LiveStoreReact.LiveStoreContext.Provider value={storeContext}>{children}</LiveStoreReact.LiveStoreContext.Provider>
103
+ )
104
+
105
+ return { wrapper, AppSchema, store }
106
+ }
@@ -0,0 +1,111 @@
1
+ import { act, renderHook } from '@testing-library/react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { sql } from '../../index.js'
5
+ import * as LiveStoreReact from '../../react/index.js'
6
+ import { makeTodoMvc } from './fixture.js'
7
+
8
+ describe('useLiveStoreComponent', () => {
9
+ it('should update the data based on component key', async () => {
10
+ let renderCount = 0
11
+
12
+ const { wrapper, AppSchema, store } = await makeTodoMvc()
13
+
14
+ const { result, rerender } = renderHook(
15
+ (userId: string) => {
16
+ renderCount++
17
+
18
+ return LiveStoreReact.useLiveStoreComponent({
19
+ stateSchema: AppSchema,
20
+ componentKey: { name: 'UserInfo', id: userId },
21
+ queries: () => ({}),
22
+ })
23
+ },
24
+ { wrapper, initialProps: 'u1' },
25
+ )
26
+
27
+ expect(result.current.state.id).toBe('u1')
28
+ expect(result.current.state.username).toBe('')
29
+ expect(renderCount).toBe(1)
30
+
31
+ act(() => {
32
+ void store.execute(sql`INSERT INTO components__UserInfo (id, username) VALUES ('u2', 'username_u2');`)
33
+ })
34
+
35
+ rerender('u2')
36
+
37
+ expect(result.current.state.id).toBe('u2')
38
+ expect(result.current.state.username).toBe('username_u2')
39
+ expect(renderCount).toBe(2)
40
+ })
41
+
42
+ it('should update the data reactively - via setState', async () => {
43
+ let renderCount = 0
44
+
45
+ const { wrapper, AppSchema } = await makeTodoMvc()
46
+
47
+ const { result } = renderHook(
48
+ (userId: string) => {
49
+ renderCount++
50
+
51
+ return LiveStoreReact.useLiveStoreComponent({
52
+ stateSchema: AppSchema,
53
+ componentKey: { name: 'UserInfo', id: userId },
54
+ queries: () => ({}),
55
+ })
56
+ },
57
+ { wrapper, initialProps: 'u1' },
58
+ )
59
+
60
+ expect(result.current.state.id).toBe('u1')
61
+ expect(result.current.state.username).toBe('')
62
+ expect(renderCount).toBe(1)
63
+
64
+ act(() => {
65
+ result.current.setState.username('username_u1_hello')
66
+ })
67
+
68
+ // act(() => {
69
+ // store.execute(sql`UPDATE components__UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`)
70
+ // })
71
+
72
+ expect(result.current.state.id).toBe('u1')
73
+ expect(result.current.state.username).toBe('username_u1_hello')
74
+ expect(renderCount).toBe(2)
75
+ })
76
+
77
+ it('should update the data reactively - via raw store update', async () => {
78
+ let renderCount = 0
79
+
80
+ const { wrapper, AppSchema, store } = await makeTodoMvc()
81
+
82
+ const { result } = renderHook(
83
+ (userId: string) => {
84
+ renderCount++
85
+
86
+ return LiveStoreReact.useLiveStoreComponent({
87
+ stateSchema: AppSchema,
88
+ componentKey: { name: 'UserInfo', id: userId },
89
+ queries: () => ({}),
90
+ })
91
+ },
92
+ { wrapper, initialProps: 'u1' },
93
+ )
94
+
95
+ expect(result.current.state.id).toBe('u1')
96
+ expect(result.current.state.username).toBe('')
97
+ expect(renderCount).toBe(1)
98
+
99
+ act(() => {
100
+ result.current.setState.username('username_u1_hello')
101
+ })
102
+
103
+ act(() => {
104
+ void store.execute(sql`UPDATE components__UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`)
105
+ })
106
+
107
+ expect(result.current.state.id).toBe('u1')
108
+ expect(result.current.state.username).toBe('username_u1_hello')
109
+ expect(renderCount).toBe(2)
110
+ })
111
+ })
@@ -0,0 +1,227 @@
1
+ import { makeNoopTracer } from '@livestore/utils'
2
+ import * as otel from '@opentelemetry/api'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { ReactiveGraph } from '../reactive.js'
6
+
7
+ const mockOtelCtx = otel.context.active()
8
+
9
+ describe('a trivial graph', () => {
10
+ const makeGraph = () => {
11
+ const graph = new ReactiveGraph({ otelTracer: makeNoopTracer() })
12
+ const a = graph.makeRef(1)
13
+ const b = graph.makeRef(2)
14
+ const numberOfRunsForC = { runs: 0 }
15
+ const c = graph.makeThunk(
16
+ (get) => {
17
+ numberOfRunsForC.runs++
18
+ return get(a) + get(b)
19
+ },
20
+ undefined,
21
+ mockOtelCtx,
22
+ )
23
+ const d = graph.makeRef(3)
24
+ const e = graph.makeThunk((get) => get(c) + get(d), undefined, mockOtelCtx)
25
+
26
+ // a(1) b(2)
27
+ // \ /
28
+ // \ /
29
+ // c = a + b
30
+ // \
31
+ // \
32
+ // d(3) \
33
+ // \ \
34
+ // \ \
35
+ // e = c + d
36
+
37
+ return { graph, a, b, c, d, e, numberOfRunsForC }
38
+ }
39
+
40
+ it('has the right initial values', () => {
41
+ const { c, e } = makeGraph()
42
+ expect(c.result).toBe(3)
43
+ expect(e.result).toBe(6)
44
+ })
45
+
46
+ it('propagates change through the graph', () => {
47
+ const { graph, a, c, e } = makeGraph()
48
+ graph.setRef(a, 5, undefined, mockOtelCtx)
49
+ expect(c.result).toBe(7)
50
+ expect(e.result).toBe(10)
51
+ })
52
+
53
+ it('cuts off reactive propagation when a thunk evaluates to same result as before', () => {
54
+ const { graph, a, c, d } = makeGraph()
55
+
56
+ let numberOfRuns = 0
57
+ const f = graph.makeThunk(
58
+ (get) => {
59
+ numberOfRuns++
60
+ return get(c) + get(d)
61
+ },
62
+ undefined,
63
+ mockOtelCtx,
64
+ )
65
+ expect(numberOfRuns).toBe(1) // initializing f should run it once
66
+
67
+ // f doesn't run because a is set to same value as before
68
+ graph.setRef(a, 1, undefined, mockOtelCtx)
69
+ expect(f.result).toBe(6)
70
+ expect(numberOfRuns).toBe(1)
71
+
72
+ // f runs because a is set to a different value
73
+ graph.setRef(a, 5, undefined, mockOtelCtx)
74
+ expect(f.result).toBe(10)
75
+ expect(numberOfRuns).toBe(2)
76
+
77
+ // f runs again when d is set to a different value
78
+ graph.setRef(d, 4, undefined, mockOtelCtx)
79
+ expect(f.result).toBe(11)
80
+ expect(numberOfRuns).toBe(3)
81
+
82
+ // f only runs one time if we set two refs together
83
+ graph.setRefs(
84
+ [
85
+ [a, 6],
86
+ [d, 5],
87
+ ],
88
+ undefined,
89
+ mockOtelCtx,
90
+ )
91
+ expect(f.result).toBe(13)
92
+ expect(numberOfRuns).toBe(4)
93
+ })
94
+
95
+ it('only runs a thunk once when two upstream refs are updated together', () => {
96
+ const { graph, a, b, c, numberOfRunsForC } = makeGraph()
97
+ expect(numberOfRunsForC.runs).toBe(1)
98
+ graph.setRefs(
99
+ [
100
+ [a, 5],
101
+ [b, 6],
102
+ ],
103
+ undefined,
104
+ mockOtelCtx,
105
+ )
106
+ expect(numberOfRunsForC.runs).toBe(2)
107
+ expect(c.result).toBe(11)
108
+ })
109
+
110
+ it('skips refresh when that option is passed when setting a single ref', () => {
111
+ const { graph, a, c, numberOfRunsForC } = makeGraph()
112
+ expect(numberOfRunsForC.runs).toBe(1)
113
+
114
+ graph.setRef(a, 5, { skipRefresh: true }, mockOtelCtx)
115
+
116
+ // C hasn't changed
117
+ expect(numberOfRunsForC.runs).toBe(1)
118
+ expect(c.result).toBe(3)
119
+
120
+ // Now we trigger a refresh and everything runs
121
+ graph.refresh(undefined, mockOtelCtx)
122
+ expect(numberOfRunsForC.runs).toBe(2)
123
+ expect(c.result).toBe(7)
124
+ })
125
+
126
+ it('skips refresh when that option is passed when setting multiple refs together', () => {
127
+ const { graph, a, b, c, numberOfRunsForC } = makeGraph()
128
+ expect(numberOfRunsForC.runs).toBe(1)
129
+
130
+ graph.setRefs(
131
+ [
132
+ [a, 5],
133
+ [b, 6],
134
+ ],
135
+ { skipRefresh: true },
136
+ mockOtelCtx,
137
+ )
138
+
139
+ // C hasn't changed
140
+ expect(numberOfRunsForC.runs).toBe(1)
141
+ expect(c.result).toBe(3)
142
+
143
+ // Now we trigger a refresh and everything runs
144
+ graph.refresh(undefined, mockOtelCtx)
145
+ expect(numberOfRunsForC.runs).toBe(2)
146
+ expect(c.result).toBe(11)
147
+ })
148
+
149
+ describe('effects', () => {
150
+ it('only reruns an effect if the thunk value changed', () => {
151
+ const { graph, a, c } = makeGraph()
152
+ let numberOfCallsToC = 0
153
+ graph.makeEffect(
154
+ (get) => {
155
+ // establish a dependency on thunk c and mutate an outside value
156
+ get(c)
157
+ numberOfCallsToC++
158
+ },
159
+ undefined,
160
+ mockOtelCtx,
161
+ )
162
+ expect(numberOfCallsToC).toBe(1)
163
+
164
+ // if we set a to the same value, the effect should not run again
165
+ graph.setRef(a, 1, undefined, mockOtelCtx)
166
+ expect(numberOfCallsToC).toBe(1)
167
+
168
+ graph.setRef(a, 2, undefined, mockOtelCtx)
169
+ expect(numberOfCallsToC).toBe(2)
170
+ })
171
+ })
172
+ })
173
+
174
+ describe('a diamond shaped graph', () => {
175
+ const makeGraph = () => {
176
+ const graph = new ReactiveGraph({ otelTracer: makeNoopTracer() })
177
+ const a = graph.makeRef(1)
178
+ const b = graph.makeThunk((get) => get(a) + 1, undefined, mockOtelCtx)
179
+ const c = graph.makeThunk((get) => get(a) + 1, undefined, mockOtelCtx)
180
+
181
+ // track the number of times d has run in an object so we can mutate it
182
+ const dRuns = { runs: 0 }
183
+
184
+ // normally thunks aren't supposed to side effect;
185
+ // we do it here to track the number of times d has run
186
+ const d = graph.makeThunk(
187
+ (get) => {
188
+ dRuns.runs++
189
+ return get(b) + get(c)
190
+ },
191
+ undefined,
192
+ mockOtelCtx,
193
+ )
194
+
195
+ // a(1)
196
+ // / \
197
+ // b c
198
+ // \ /
199
+ // d = b + c
200
+
201
+ return { graph, a, b, c, d, dRuns }
202
+ }
203
+
204
+ it('has the right initial values', () => {
205
+ const { b, c, d } = makeGraph()
206
+ expect(b.result).toBe(2)
207
+ expect(c.result).toBe(2)
208
+ expect(d.result).toBe(4)
209
+ })
210
+
211
+ it('propagates change through the graph', () => {
212
+ const { graph, a, b, c, d } = makeGraph()
213
+ graph.setRef(a, 5, undefined, mockOtelCtx)
214
+ expect(b.result).toBe(6)
215
+ expect(c.result).toBe(6)
216
+ expect(d.result).toBe(12)
217
+ })
218
+
219
+ // if we're being efficient, we should update b and c before updating d,
220
+ // so d only needs to update one time
221
+ it('only runs d once when a changes', () => {
222
+ const { graph, a, dRuns } = makeGraph()
223
+ expect(dRuns.runs).toBe(1)
224
+ graph.setRef(a, 5, undefined, mockOtelCtx)
225
+ expect(dRuns.runs).toBe(2)
226
+ })
227
+ })
@@ -0,0 +1,7 @@
1
+ interface Window {
2
+ [key: `__debug${string}`]: any
3
+ }
4
+
5
+ interface ImportMeta {
6
+ readonly env: ImportMetaEnv
7
+ }