@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
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
|
package/dist/util.js.map
ADDED
|
@@ -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
|
+
})
|