@livestore/livestore 0.0.39 → 0.0.41-dev.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 +15 -24
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +192 -17
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +10 -29
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/{mutations.d.ts → cud.d.ts} +14 -19
- package/dist/cud.d.ts.map +1 -0
- package/dist/{mutations.js → cud.js} +16 -10
- package/dist/cud.js.map +1 -0
- package/dist/cud.test.d.ts +2 -0
- package/dist/cud.test.d.ts.map +1 -0
- package/dist/cud.test.js +47 -0
- package/dist/cud.test.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +1 -1
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +1 -4
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +11 -7
- package/dist/migrations.js.map +1 -1
- package/dist/query-info.d.ts +2 -5
- package/dist/query-info.d.ts.map +1 -1
- package/dist/query-info.js +3 -2
- package/dist/query-info.js.map +1 -1
- package/dist/react/useAtom.d.ts.map +1 -1
- package/dist/react/useAtom.js +2 -2
- package/dist/react/useAtom.js.map +1 -1
- package/dist/react/useQuery.test.d.ts.map +1 -0
- package/dist/{__tests__/react → react}/useQuery.test.js +8 -11
- package/dist/react/useQuery.test.js.map +1 -0
- package/dist/react/useRow.js +4 -4
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useRow.test.d.ts.map +1 -0
- package/dist/{__tests__/react → react}/useRow.test.js +14 -38
- package/dist/react/useRow.test.js.map +1 -0
- package/dist/reactive.d.ts +2 -2
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +50 -15
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.d.ts.map +1 -0
- package/dist/{__tests__/reactive.test.js → reactive.test.js} +1 -1
- package/dist/reactive.test.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +2 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +1 -0
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +1 -0
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +1 -0
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +1 -0
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/{__tests__/reactiveQueries → reactiveQueries}/sql.test.js +44 -34
- package/dist/reactiveQueries/sql.test.js.map +1 -0
- package/dist/row-query.js +8 -6
- package/dist/row-query.js.map +1 -1
- package/dist/schema/index.d.ts +20 -7
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +18 -3
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/mutations.d.ts +81 -0
- package/dist/schema/mutations.d.ts.map +1 -0
- package/dist/schema/mutations.js +29 -0
- package/dist/schema/mutations.js.map +1 -0
- package/dist/schema/parse-utils.d.ts +3 -3
- package/dist/schema/parse-utils.d.ts.map +1 -1
- package/dist/schema/table-def.d.ts +2 -2
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +14 -6
- package/dist/schema/table-def.js.map +1 -1
- package/dist/storage/in-memory/index.d.ts +4 -0
- package/dist/storage/in-memory/index.d.ts.map +1 -1
- package/dist/storage/in-memory/index.js +3 -0
- package/dist/storage/in-memory/index.js.map +1 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/tauri/index.d.ts +4 -0
- package/dist/storage/tauri/index.d.ts.map +1 -1
- package/dist/storage/tauri/index.js +6 -0
- package/dist/storage/tauri/index.js.map +1 -1
- package/dist/storage/utils/idb.d.ts +1 -0
- package/dist/storage/utils/idb.d.ts.map +1 -1
- package/dist/storage/utils/idb.js +11 -0
- package/dist/storage/utils/idb.js.map +1 -1
- package/dist/storage/web-worker/common.d.ts +11 -0
- package/dist/storage/web-worker/common.d.ts.map +1 -0
- package/dist/storage/web-worker/common.js +2 -0
- package/dist/storage/web-worker/common.js.map +1 -0
- package/dist/storage/web-worker/index.d.ts +14 -7
- package/dist/storage/web-worker/index.d.ts.map +1 -1
- package/dist/storage/web-worker/index.js +70 -14
- package/dist/storage/web-worker/index.js.map +1 -1
- package/dist/storage/web-worker/make-worker.d.ts +20 -0
- package/dist/storage/web-worker/make-worker.d.ts.map +1 -0
- package/dist/storage/web-worker/make-worker.js +155 -0
- package/dist/storage/web-worker/make-worker.js.map +1 -0
- package/dist/storage/web-worker/vite-dev-polyfill.d.ts +2 -0
- package/dist/storage/web-worker/vite-dev-polyfill.d.ts.map +1 -0
- package/dist/storage/web-worker/vite-dev-polyfill.js +35 -0
- package/dist/storage/web-worker/vite-dev-polyfill.js.map +1 -0
- package/dist/store.d.ts +32 -42
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +82 -131
- package/dist/store.js.map +1 -1
- package/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -0
- package/dist/utils/dev.js +16 -0
- package/dist/utils/dev.js.map +1 -0
- package/dist/utils/util.d.ts +2 -0
- package/dist/utils/util.d.ts.map +1 -1
- package/dist/utils/util.js +2 -0
- package/dist/utils/util.js.map +1 -1
- package/package.json +24 -12
- package/src/__tests__/react/fixture.tsx +12 -30
- package/src/cud.test.ts +52 -0
- package/src/{mutations.ts → cud.ts} +29 -28
- package/src/inMemoryDatabase.ts +2 -7
- package/src/index.ts +14 -8
- package/src/migrations.ts +10 -7
- package/src/query-info.ts +4 -7
- package/src/react/useAtom.ts +2 -2
- package/src/{__tests__/react → react}/useQuery.test.tsx +11 -11
- package/src/{__tests__/react → react}/useRow.test.tsx +21 -39
- package/src/react/useRow.ts +4 -4
- package/src/{__tests__/reactive.test.ts → reactive.test.ts} +1 -1
- package/src/reactive.ts +60 -19
- package/src/reactiveQueries/base-class.ts +4 -0
- package/src/reactiveQueries/graphql.ts +2 -0
- package/src/reactiveQueries/js.ts +2 -0
- package/src/{__tests__/reactiveQueries → reactiveQueries}/sql.test.ts +44 -34
- package/src/reactiveQueries/sql.ts +2 -0
- package/src/row-query.ts +9 -10
- package/src/schema/index.ts +47 -11
- package/src/schema/mutations.ts +129 -0
- package/src/schema/parse-utils.ts +1 -1
- package/src/schema/table-def.ts +20 -8
- package/src/storage/in-memory/index.ts +7 -0
- package/src/storage/index.ts +8 -0
- package/src/storage/tauri/index.ts +10 -0
- package/src/storage/utils/idb.ts +14 -0
- package/src/storage/web-worker/common.ts +6 -0
- package/src/storage/web-worker/index.ts +86 -17
- package/src/storage/web-worker/make-worker.ts +214 -0
- package/src/storage/web-worker/vite-dev-polyfill.ts +33 -0
- package/src/store.ts +142 -212
- package/src/utils/dev.ts +23 -0
- package/src/utils/util.ts +4 -0
- package/dist/__tests__/mutations.test.d.ts +0 -2
- package/dist/__tests__/mutations.test.d.ts.map +0 -1
- package/dist/__tests__/mutations.test.js +0 -40
- package/dist/__tests__/mutations.test.js.map +0 -1
- package/dist/__tests__/react/useQuery.test.d.ts.map +0 -1
- package/dist/__tests__/react/useQuery.test.js.map +0 -1
- package/dist/__tests__/react/useRow.test.d.ts.map +0 -1
- package/dist/__tests__/react/useRow.test.js.map +0 -1
- package/dist/__tests__/reactive.test.d.ts.map +0 -1
- package/dist/__tests__/reactive.test.js.map +0 -1
- package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +0 -1
- package/dist/__tests__/reactiveQueries/sql.test.js.map +0 -1
- package/dist/events.d.ts +0 -7
- package/dist/events.d.ts.map +0 -1
- package/dist/events.js +0 -2
- package/dist/events.js.map +0 -1
- package/dist/mutations.d.ts.map +0 -1
- package/dist/mutations.js.map +0 -1
- package/dist/schema/action.d.ts +0 -30
- package/dist/schema/action.d.ts.map +0 -1
- package/dist/schema/action.js +0 -3
- package/dist/schema/action.js.map +0 -1
- package/dist/storage/web-worker/worker.d.ts +0 -13
- package/dist/storage/web-worker/worker.d.ts.map +0 -1
- package/dist/storage/web-worker/worker.js +0 -110
- package/dist/storage/web-worker/worker.js.map +0 -1
- package/src/__tests__/mutations.test.ts +0 -43
- package/src/events.ts +0 -8
- package/src/schema/action.ts +0 -41
- package/src/storage/web-worker/worker.ts +0 -141
- /package/dist/{__tests__/react → react}/useQuery.test.d.ts +0 -0
- /package/dist/{__tests__/react → react}/useRow.test.d.ts +0 -0
- /package/dist/{__tests__/reactive.test.d.ts → reactive.test.d.ts} +0 -0
- /package/dist/{__tests__/reactiveQueries → reactiveQueries}/sql.test.d.ts +0 -0
package/src/cud.test.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { tables } from './__tests__/react/fixture.js'
|
|
4
|
+
import { makeCudMutations } from './cud.js'
|
|
5
|
+
import type { MutationEvent } from './index.js'
|
|
6
|
+
|
|
7
|
+
describe('cud mutations', () => {
|
|
8
|
+
const cud = makeCudMutations(tables)
|
|
9
|
+
|
|
10
|
+
test('basic', () => {
|
|
11
|
+
expect(patchId(cud.todos.insert({ id: 't1', completed: true, text: 'Task 1' }))).toMatchInlineSnapshot(`
|
|
12
|
+
{
|
|
13
|
+
"args": {
|
|
14
|
+
"bindValues": {
|
|
15
|
+
"completed": 1,
|
|
16
|
+
"id": "t1",
|
|
17
|
+
"text": "Task 1",
|
|
18
|
+
},
|
|
19
|
+
"sql": "INSERT INTO todos (id, text, completed) VALUES ($id, $text, $completed)",
|
|
20
|
+
"writeTables": Set {
|
|
21
|
+
"todos",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
25
|
+
"mutation": "livestore.RawSql",
|
|
26
|
+
}
|
|
27
|
+
`)
|
|
28
|
+
|
|
29
|
+
expect(patchId(cud.todos.update({ where: { id: 't1' }, values: { text: 'Task 1 - fixed' } })))
|
|
30
|
+
.toMatchInlineSnapshot(`
|
|
31
|
+
{
|
|
32
|
+
"args": {
|
|
33
|
+
"bindValues": {
|
|
34
|
+
"update_text": "Task 1 - fixed",
|
|
35
|
+
"where_id": "t1",
|
|
36
|
+
},
|
|
37
|
+
"sql": "UPDATE todos SET text = $update_text WHERE id = $where_id",
|
|
38
|
+
"writeTables": Set {
|
|
39
|
+
"todos",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
43
|
+
"mutation": "livestore.RawSql",
|
|
44
|
+
}
|
|
45
|
+
`)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const patchId = (muationEvent: MutationEvent.Any) => {
|
|
50
|
+
const id = `00000000-0000-0000-0000-000000000000`
|
|
51
|
+
return { ...muationEvent, id }
|
|
52
|
+
}
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
import * as SqlQueries from '@livestore/sql-queries'
|
|
2
|
-
import { pipe, ReadonlyRecord } from '@livestore/utils/effect'
|
|
3
2
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
4
3
|
|
|
5
4
|
import type { RowResult } from './row-query.js'
|
|
6
|
-
import type
|
|
5
|
+
import { rawSqlMutation, type RawSqlMutationEvent } from './schema/index.js'
|
|
7
6
|
import { getDefaultValuesEncoded, type TableDef } from './schema/table-def.js'
|
|
8
|
-
import type
|
|
7
|
+
import { type GetValForKey, isIterable } from './utils/util.js'
|
|
9
8
|
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
):
|
|
13
|
-
|
|
9
|
+
export const makeCudMutations = <TTableDef extends TableDef>(
|
|
10
|
+
tables: Iterable<TTableDef> | Record<string, TTableDef>,
|
|
11
|
+
): CudMutations<TTableDef> => {
|
|
12
|
+
const cudMutationRecord: CudMutations<TTableDef> = {} as any
|
|
13
|
+
|
|
14
|
+
const tables_ = isIterable(tables) ? tables : Object.values(tables)
|
|
15
|
+
|
|
16
|
+
for (const tableDef of tables_) {
|
|
17
|
+
const [tableName, cudMutation] = cudMutationsForTable(tableDef)
|
|
18
|
+
cudMutationRecord[tableName] = cudMutation as any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return cudMutationRecord
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
const
|
|
24
|
+
const cudMutationsForTable = <TTableDef extends TableDef>(
|
|
25
|
+
tableDef: TTableDef,
|
|
26
|
+
): [TTableDef['sqliteDef']['name'], CudMutation<TTableDef>] => {
|
|
17
27
|
const table = tableDef.sqliteDef
|
|
18
28
|
const writeTables = new Set([table.name])
|
|
19
29
|
const api = {
|
|
20
30
|
insert: (values_: any) => {
|
|
21
|
-
const
|
|
22
|
-
const values = pipe(
|
|
23
|
-
tableDef.sqliteDef.columns,
|
|
24
|
-
ReadonlyRecord.map((_, columnName) => values_?.[columnName] ?? defaultValues[columnName]),
|
|
25
|
-
)
|
|
31
|
+
const values = getDefaultValuesEncoded(tableDef, values_)
|
|
26
32
|
|
|
27
33
|
const [sql, bindValues] = SqlQueries.insertRow({
|
|
28
34
|
tableName: table.name,
|
|
@@ -30,7 +36,7 @@ const mutationsForTable = <TTableDef extends TableDef>(tableDef: TTableDef): [st
|
|
|
30
36
|
options: { orReplace: false },
|
|
31
37
|
values: values as any,
|
|
32
38
|
})
|
|
33
|
-
return {
|
|
39
|
+
return rawSqlMutation({ sql, bindValues, writeTables })
|
|
34
40
|
},
|
|
35
41
|
update: ({ where, values }) => {
|
|
36
42
|
const [sql, bindValues] = SqlQueries.updateRows({
|
|
@@ -39,7 +45,7 @@ const mutationsForTable = <TTableDef extends TableDef>(tableDef: TTableDef): [st
|
|
|
39
45
|
where: where,
|
|
40
46
|
updateValues: values,
|
|
41
47
|
})
|
|
42
|
-
return {
|
|
48
|
+
return rawSqlMutation({ sql, bindValues, writeTables })
|
|
43
49
|
},
|
|
44
50
|
delete: ({ where }) => {
|
|
45
51
|
const [sql, bindValues] = SqlQueries.deleteRows({
|
|
@@ -47,40 +53,35 @@ const mutationsForTable = <TTableDef extends TableDef>(tableDef: TTableDef): [st
|
|
|
47
53
|
columns: table.columns,
|
|
48
54
|
where: where,
|
|
49
55
|
})
|
|
50
|
-
return {
|
|
56
|
+
return rawSqlMutation({ sql, bindValues, writeTables })
|
|
51
57
|
},
|
|
52
|
-
} satisfies
|
|
58
|
+
} satisfies CudMutation<TTableDef>
|
|
53
59
|
|
|
54
60
|
return [tableDef.sqliteDef.name, api]
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
export type MutationEvent = {
|
|
58
|
-
eventType: 'livestore.RawSql'
|
|
59
|
-
args: { sql: string; bindValues: SqlQueries.BindValues; writeTables: Set<string> }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
63
|
export type UpdateMutation<TTableDef extends TableDef> = (args: {
|
|
63
64
|
// TODO also allow `id` if present in `TTableDef`
|
|
64
65
|
where: Partial<RowResult<TTableDef>>
|
|
65
66
|
values: Partial<RowResult<TTableDef>>
|
|
66
|
-
}) =>
|
|
67
|
+
}) => RawSqlMutationEvent
|
|
67
68
|
|
|
68
69
|
export type RowInsert<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
|
|
69
70
|
? GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TTableDef['sqliteDef']['columns']>, 'value'>
|
|
70
71
|
: SqliteDsl.FromColumns.InsertRowDecoded<TTableDef['sqliteDef']['columns']>
|
|
71
72
|
|
|
72
|
-
export type InsertMutation<TTableDef extends TableDef> = (values: RowInsert<TTableDef>) =>
|
|
73
|
+
export type InsertMutation<TTableDef extends TableDef> = (values: RowInsert<TTableDef>) => RawSqlMutationEvent
|
|
73
74
|
|
|
74
75
|
export type DeleteMutation<TTableDef extends TableDef> = (args: {
|
|
75
76
|
where: Partial<RowResult<TTableDef>>
|
|
76
|
-
}) =>
|
|
77
|
+
}) => RawSqlMutationEvent
|
|
77
78
|
|
|
78
|
-
export type
|
|
79
|
+
export type CudMutation<TTableDef extends TableDef> = {
|
|
79
80
|
insert: InsertMutation<TTableDef>
|
|
80
81
|
update: UpdateMutation<TTableDef>
|
|
81
82
|
delete: DeleteMutation<TTableDef>
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export type
|
|
85
|
-
[TTableName in
|
|
85
|
+
export type CudMutations<TTableDef extends TableDef> = {
|
|
86
|
+
[TTableName in TTableDef['sqliteDef']['name']]: CudMutation<Extract<TTableDef, { sqliteDef: { name: TTableName } }>>
|
|
86
87
|
}
|
package/src/inMemoryDatabase.ts
CHANGED
|
@@ -138,7 +138,7 @@ export class InMemoryDatabase {
|
|
|
138
138
|
execute(
|
|
139
139
|
query: string,
|
|
140
140
|
bindValues?: PreparedBindValues,
|
|
141
|
-
writeTables?:
|
|
141
|
+
writeTables?: ReadonlySet<string>,
|
|
142
142
|
options?: { hasNoEffects?: boolean; otelContext?: otel.Context },
|
|
143
143
|
): { durationMs: number } {
|
|
144
144
|
// console.debug('in-memory-db:execute', query, bindValues)
|
|
@@ -170,12 +170,7 @@ export class InMemoryDatabase {
|
|
|
170
170
|
stmt.reset() // Reset is needed for next execution
|
|
171
171
|
}
|
|
172
172
|
} catch (error) {
|
|
173
|
-
shouldNeverHappen(
|
|
174
|
-
`Error executing query: ${error} \n ${JSON.stringify({
|
|
175
|
-
query,
|
|
176
|
-
bindValues,
|
|
177
|
-
})}`,
|
|
178
|
-
)
|
|
173
|
+
shouldNeverHappen(`Error executing query: ${error} \n ${JSON.stringify({ query, bindValues })}`)
|
|
179
174
|
}
|
|
180
175
|
|
|
181
176
|
if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
|
package/src/index.ts
CHANGED
|
@@ -13,23 +13,29 @@ export { LiveStoreSQLQuery, querySQL, type MapRows } from './reactiveQueries/sql
|
|
|
13
13
|
export { LiveStoreGraphQLQuery, queryGraphQL } from './reactiveQueries/graphql.js'
|
|
14
14
|
export { type GetAtomResult, type DbGraph, makeDbGraph, type LiveQuery } from './reactiveQueries/base-class.js'
|
|
15
15
|
|
|
16
|
-
export { globalDbGraph } from './global-state.js'
|
|
16
|
+
export { globalDbGraph, dynamicallyRegisteredTables } from './global-state.js'
|
|
17
17
|
|
|
18
18
|
export { type RowResult, type RowResultEncoded, rowQuery, deriveColQuery } from './row-query.js'
|
|
19
19
|
|
|
20
|
-
export * from './
|
|
20
|
+
export * from './cud.js'
|
|
21
21
|
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
makeSchema,
|
|
24
|
+
DbSchema,
|
|
25
|
+
ParseUtils,
|
|
26
|
+
defineMutation,
|
|
27
|
+
rawSqlMutation,
|
|
28
|
+
makeMutationEventSchema,
|
|
29
|
+
makeMutationDefRecord,
|
|
30
|
+
} from './schema/index.js'
|
|
23
31
|
|
|
24
32
|
export type {
|
|
25
33
|
LiveStoreSchema,
|
|
26
34
|
InputSchema,
|
|
27
|
-
GetActionArgs,
|
|
28
|
-
GetApplyEventArgs,
|
|
29
|
-
ActionDefinition,
|
|
30
|
-
ActionDefinitions,
|
|
31
|
-
SQLWriteStatement,
|
|
32
35
|
SchemaMetaRow,
|
|
36
|
+
MutationDef,
|
|
37
|
+
MutationEvent,
|
|
38
|
+
MutationDefMap,
|
|
33
39
|
} from './schema/index.js'
|
|
34
40
|
|
|
35
41
|
export { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
package/src/migrations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema as EffectSchema } from '@livestore/utils/effect'
|
|
2
2
|
import type * as otel from '@opentelemetry/api'
|
|
3
|
-
import { SqliteAst } from 'effect-db-schema'
|
|
3
|
+
import { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
4
4
|
import { memoize } from 'lodash-es'
|
|
5
5
|
|
|
6
6
|
import { dynamicallyRegisteredTables } from './global-state.js'
|
|
@@ -26,7 +26,7 @@ export const migrateDb = ({
|
|
|
26
26
|
// TODO use schema migration definition from schema.ts instead
|
|
27
27
|
sql`create table if not exists ${SCHEMA_META_TABLE} (tableName text primary key, schemaHash text, updatedAt text);`,
|
|
28
28
|
undefined,
|
|
29
|
-
|
|
29
|
+
new Set(),
|
|
30
30
|
{ otelContext },
|
|
31
31
|
)
|
|
32
32
|
|
|
@@ -80,11 +80,11 @@ export const migrateTable = ({
|
|
|
80
80
|
const columnSpec = makeColumnSpec(tableAst)
|
|
81
81
|
|
|
82
82
|
// TODO need to possibly handle cascading deletes due to foreign keys
|
|
83
|
-
db.execute(sql`drop table if exists ${tableName}`, undefined,
|
|
84
|
-
db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined,
|
|
83
|
+
db.execute(sql`drop table if exists ${tableName}`, undefined, new Set(), { otelContext })
|
|
84
|
+
db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, new Set(), { otelContext })
|
|
85
85
|
|
|
86
86
|
for (const index of tableAst.indexes) {
|
|
87
|
-
db.execute(createIndexFromDefinition(tableName, index), undefined,
|
|
87
|
+
db.execute(createIndexFromDefinition(tableName, index), undefined, new Set(), { otelContext })
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const updatedAt = getMemoizedTimestamp()
|
|
@@ -94,7 +94,7 @@ export const migrateTable = ({
|
|
|
94
94
|
ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
|
|
95
95
|
`,
|
|
96
96
|
{ $tableName: tableName, $schemaHash: schemaHash, $updatedAt: updatedAt } as unknown as PreparedBindValues,
|
|
97
|
-
|
|
97
|
+
new Set(),
|
|
98
98
|
{ otelContext },
|
|
99
99
|
)
|
|
100
100
|
}
|
|
@@ -121,10 +121,13 @@ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
|
|
|
121
121
|
const defaultValueStr = (() => {
|
|
122
122
|
if (column.default._tag === 'None') return ''
|
|
123
123
|
|
|
124
|
+
if (SqliteDsl.isSqlDefaultValue(column.default.value)) return `default ${column.default.value.sql}`
|
|
125
|
+
|
|
124
126
|
const encodeValue = EffectSchema.encodeSync(column.schema)
|
|
125
127
|
const encodedDefaultValue = encodeValue(column.default.value)
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
if (columnTypeStr === 'text') return `default '${encodedDefaultValue}'`
|
|
130
|
+
return `default ${encodedDefaultValue}`
|
|
128
131
|
})()
|
|
129
132
|
|
|
130
133
|
return `${column.name} ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
|
package/src/query-info.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { notYetImplemented, shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
|
+
import { rawSqlMutation, type RawSqlMutationEvent } from './schema/mutations.js'
|
|
4
5
|
import type { FromTable, TableDef } from './schema/table-def.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -49,8 +50,6 @@ type GetJsonColumn<TTableDef extends TableDef> = keyof {
|
|
|
49
50
|
: never]: {}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
// type GetObjValues<TObj extends {}> = TObj[keyof TObj]
|
|
53
|
-
|
|
54
53
|
export type UpdateValueForPath<TPath extends QueryInfo> = TPath extends { _tag: 'Row' }
|
|
55
54
|
? Partial<FromTable.RowDecodedAll<TPath['table']>>
|
|
56
55
|
: TPath extends { _tag: 'Col' }
|
|
@@ -59,10 +58,10 @@ export type UpdateValueForPath<TPath extends QueryInfo> = TPath extends { _tag:
|
|
|
59
58
|
? { TODO: true }
|
|
60
59
|
: never
|
|
61
60
|
|
|
62
|
-
export const
|
|
61
|
+
export const mutationForQueryInfo = <const TPath extends QueryInfo>(
|
|
63
62
|
updatePath: TPath,
|
|
64
63
|
value: UpdateValueForPath<TPath>,
|
|
65
|
-
):
|
|
64
|
+
): RawSqlMutationEvent => {
|
|
66
65
|
if (updatePath._tag === 'ColJsonValue' || updatePath._tag === 'None') {
|
|
67
66
|
return notYetImplemented('TODO')
|
|
68
67
|
}
|
|
@@ -96,7 +95,5 @@ export const storeEventForQueryInfo = <TPath extends QueryInfo>(
|
|
|
96
95
|
const sql = `UPDATE ${sqliteTableDef.name} SET ${updateClause} ${whereClause}`
|
|
97
96
|
const writeTables = new Set<string>([updatePath.table.sqliteDef.name])
|
|
98
97
|
|
|
99
|
-
return {
|
|
98
|
+
return rawSqlMutation({ sql, bindValues, writeTables })
|
|
100
99
|
}
|
|
101
|
-
|
|
102
|
-
type StoreEvent = { eventType: string; args: any }
|
package/src/react/useAtom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import { type QueryInfoCol, type QueryInfoRow
|
|
3
|
+
import { mutationForQueryInfo, type QueryInfoCol, type QueryInfoRow } from '../query-info.js'
|
|
4
4
|
import type { LiveQuery } from '../reactiveQueries/base-class.js'
|
|
5
5
|
import { useStore } from './LiveStoreContext.js'
|
|
6
6
|
import { useQueryRef } from './useQuery.js'
|
|
@@ -17,7 +17,7 @@ export const useAtom = <TQuery extends LiveQuery<any, QueryInfoRow<any> | QueryI
|
|
|
17
17
|
return (newValueOrFn: any) => {
|
|
18
18
|
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
|
|
19
19
|
|
|
20
|
-
store.
|
|
20
|
+
store.mutate(mutationForQueryInfo(query$.queryInfo!, newValue))
|
|
21
21
|
}
|
|
22
22
|
}, [query$.queryInfo, query$Ref, store])
|
|
23
23
|
|
|
@@ -2,15 +2,15 @@ import { act, renderHook } from '@testing-library/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { querySQL } from '
|
|
7
|
-
import
|
|
5
|
+
import { makeTodoMvc, parseTodos } from '../__tests__/react/fixture.js'
|
|
6
|
+
import { querySQL } from '../reactiveQueries/sql.js'
|
|
7
|
+
import * as LiveStoreReact from './index.js'
|
|
8
8
|
|
|
9
9
|
describe('useQuery', () => {
|
|
10
10
|
it('simple', async () => {
|
|
11
11
|
let renderCount = 0
|
|
12
12
|
|
|
13
|
-
const { wrapper, store,
|
|
13
|
+
const { wrapper, store, cud } = await makeTodoMvc()
|
|
14
14
|
|
|
15
15
|
const allTodos$ = querySQL(`select * from todos`, { map: parseTodos })
|
|
16
16
|
|
|
@@ -26,7 +26,7 @@ describe('useQuery', () => {
|
|
|
26
26
|
expect(result.current.length).toBe(0)
|
|
27
27
|
expect(renderCount).toBe(1)
|
|
28
28
|
|
|
29
|
-
act(() => store.
|
|
29
|
+
act(() => store.mutate(cud.todos.insert({ id: 't1', text: 'buy milk', completed: false })))
|
|
30
30
|
|
|
31
31
|
expect(result.current.length).toBe(1)
|
|
32
32
|
expect(result.current[0]!.text).toBe('buy milk')
|
|
@@ -36,15 +36,15 @@ describe('useQuery', () => {
|
|
|
36
36
|
it('same `useQuery` hook invoked with different queries', async () => {
|
|
37
37
|
let renderCount = 0
|
|
38
38
|
|
|
39
|
-
const { wrapper, store,
|
|
39
|
+
const { wrapper, store, cud } = await makeTodoMvc()
|
|
40
40
|
|
|
41
41
|
const todo1$ = querySQL(`select * from todos where id = 't1'`, { label: 'libraryTracksView1', map: parseTodos })
|
|
42
42
|
const todo2$ = querySQL(`select * from todos where id = 't2'`, { label: 'libraryTracksView2', map: parseTodos })
|
|
43
43
|
|
|
44
|
-
store.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
store.mutate(
|
|
45
|
+
cud.todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
46
|
+
cud.todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
|
|
47
|
+
)
|
|
48
48
|
|
|
49
49
|
const { result, rerender } = renderHook(
|
|
50
50
|
(todoId: string) => {
|
|
@@ -60,7 +60,7 @@ describe('useQuery', () => {
|
|
|
60
60
|
expect(result.current).toBe('buy milk')
|
|
61
61
|
expect(renderCount).toBe(1)
|
|
62
62
|
|
|
63
|
-
act(() => store.
|
|
63
|
+
act(() => store.mutate(cud.todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
|
|
64
64
|
|
|
65
65
|
expect(result.current).toBe('buy soy milk')
|
|
66
66
|
expect(renderCount).toBe(2)
|
|
@@ -2,10 +2,11 @@ import { act, render, renderHook } from '@testing-library/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
5
|
+
import type { Todo } from '../__tests__/react/fixture.js'
|
|
6
|
+
import { makeTodoMvc, todos } from '../__tests__/react/fixture.js'
|
|
7
|
+
import * as LiveStore from '../index.js'
|
|
8
|
+
import { mutationForQueryInfo } from '../query-info.js'
|
|
9
|
+
import * as LiveStoreReact from './index.js'
|
|
9
10
|
|
|
10
11
|
describe('useRow', () => {
|
|
11
12
|
it('should update the data based on component key', async () => {
|
|
@@ -27,9 +28,7 @@ describe('useRow', () => {
|
|
|
27
28
|
expect(result.current.state.username).toBe('')
|
|
28
29
|
expect(renderCount).toBe(1)
|
|
29
30
|
|
|
30
|
-
act(() =>
|
|
31
|
-
void store.execute(LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2');`)
|
|
32
|
-
})
|
|
31
|
+
act(() => store.execute(LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`))
|
|
33
32
|
|
|
34
33
|
rerender('u2')
|
|
35
34
|
|
|
@@ -85,9 +84,7 @@ describe('useRow', () => {
|
|
|
85
84
|
|
|
86
85
|
act(() => result.current.setState.username('username_u1_hello'))
|
|
87
86
|
|
|
88
|
-
act(() =>
|
|
89
|
-
void store.execute(LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`)
|
|
90
|
-
})
|
|
87
|
+
act(() => store.execute(LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`))
|
|
91
88
|
|
|
92
89
|
expect(result.current.state.id).toBe('u1')
|
|
93
90
|
expect(result.current.state.username).toBe('username_u1_hello')
|
|
@@ -149,10 +146,11 @@ describe('useRow', () => {
|
|
|
149
146
|
expect(appRouterRenderCount).toBe(1)
|
|
150
147
|
|
|
151
148
|
act(() =>
|
|
152
|
-
store.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
store.mutate(
|
|
150
|
+
LiveStore.rawSqlMutation({
|
|
151
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
156
154
|
)
|
|
157
155
|
|
|
158
156
|
expect(appRouterRenderCount).toBe(1)
|
|
@@ -168,31 +166,15 @@ describe('useRow', () => {
|
|
|
168
166
|
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
|
|
169
167
|
|
|
170
168
|
act(() =>
|
|
171
|
-
store.
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
eventType: 'livestore.UpdateComponentState',
|
|
181
|
-
args: {
|
|
182
|
-
id: 'singleton',
|
|
183
|
-
columnNames: ['currentTaskId'],
|
|
184
|
-
tableName: AppRouterSchema.sqliteDef.name,
|
|
185
|
-
bindValues: { currentTaskId: 't2' },
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
eventType: 'livestore.RawSql',
|
|
190
|
-
args: {
|
|
191
|
-
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0);`,
|
|
192
|
-
writeTables: ['todos'],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
]),
|
|
169
|
+
store.mutate(
|
|
170
|
+
LiveStore.rawSqlMutation({
|
|
171
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
|
|
172
|
+
}),
|
|
173
|
+
mutationForQueryInfo({ _tag: 'Col', table: AppRouterSchema, column: 'currentTaskId', id: 'singleton' }, 't2'),
|
|
174
|
+
LiveStore.rawSqlMutation({
|
|
175
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
196
178
|
)
|
|
197
179
|
|
|
198
180
|
expect(appRouterRenderCount).toBe(3)
|
package/src/react/useRow.ts
CHANGED
|
@@ -5,7 +5,7 @@ import React from 'react'
|
|
|
5
5
|
|
|
6
6
|
import type { DbGraph, LiveQuery } from '../index.js'
|
|
7
7
|
import type { QueryInfo } from '../query-info.js'
|
|
8
|
-
import {
|
|
8
|
+
import { mutationForQueryInfo } from '../query-info.js'
|
|
9
9
|
import type { RowResult } from '../row-query.js'
|
|
10
10
|
import { rowQuery } from '../row-query.js'
|
|
11
11
|
import { type DefaultSqliteTableDef, type TableDef, tableIsSingleton, type TableOptions } from '../schema/table-def.js'
|
|
@@ -117,7 +117,7 @@ export const useRow: {
|
|
|
117
117
|
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
|
|
118
118
|
if (query$Ref.current === newValue) return
|
|
119
119
|
|
|
120
|
-
store.
|
|
120
|
+
store.mutate(mutationForQueryInfo(query$.queryInfo!, { value: newValue }))
|
|
121
121
|
}
|
|
122
122
|
} else {
|
|
123
123
|
const setState = // TODO: do we have a better type for the values that can go in SQLite?
|
|
@@ -130,7 +130,7 @@ export const useRow: {
|
|
|
130
130
|
// @ts-expect-error TODO fix typing
|
|
131
131
|
if (query$Ref.current[columnName] === newValue) return
|
|
132
132
|
|
|
133
|
-
store.
|
|
133
|
+
store.mutate(mutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
|
|
134
134
|
})
|
|
135
135
|
|
|
136
136
|
setState.setMany = (columnValuesOrFn: Partial<TComponentState>) => {
|
|
@@ -147,7 +147,7 @@ export const useRow: {
|
|
|
147
147
|
return
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
store.
|
|
150
|
+
store.mutate(mutationForQueryInfo(query$.queryInfo!, columnValues))
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
return setState as any
|
package/src/reactive.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
25
25
|
|
|
26
26
|
import type { PrettifyFlat } from '@livestore/utils'
|
|
27
|
-
import {
|
|
27
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
28
28
|
import type * as otel from '@opentelemetry/api'
|
|
29
29
|
import { isEqual } from 'lodash-es'
|
|
30
30
|
|
|
@@ -94,7 +94,7 @@ export type DebugThunkInfo<T extends string = string> = {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export type DebugRefreshReasonBase =
|
|
97
|
-
/** Usually in response to some `
|
|
97
|
+
/** Usually in response to some `mutate` calls with `skipRefresh: true` */
|
|
98
98
|
| {
|
|
99
99
|
_tag: 'runDeferredEffects'
|
|
100
100
|
originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase>
|
|
@@ -135,7 +135,7 @@ const unknownRefreshReason = () => {
|
|
|
135
135
|
|
|
136
136
|
export type SerializedAtom = Readonly<
|
|
137
137
|
PrettifyFlat<
|
|
138
|
-
Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
|
|
138
|
+
Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta' | 'isDirty'> & {
|
|
139
139
|
sub: ReadonlyArray<string>
|
|
140
140
|
super: ReadonlyArray<string>
|
|
141
141
|
}
|
|
@@ -162,17 +162,6 @@ const uniqueNodeId = () => `node-${++nodeIdCounter}`
|
|
|
162
162
|
let refreshInfoIdCounter = 0
|
|
163
163
|
const uniqueRefreshInfoId = () => `refresh-info-${++refreshInfoIdCounter}`
|
|
164
164
|
|
|
165
|
-
const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
|
|
166
|
-
...pick(atom, ['_tag', 'id', 'label', 'meta', 'isDirty']),
|
|
167
|
-
sub: Array.from(atom.sub).map((a) => a.id),
|
|
168
|
-
super: Array.from(atom.super).map((a) => a.id),
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
const serializeEffect = (effect: Effect): SerializedEffect => ({
|
|
172
|
-
...pick(effect, ['_tag', 'id', 'label']),
|
|
173
|
-
sub: Array.from(effect.sub).map((a) => a.id),
|
|
174
|
-
})
|
|
175
|
-
|
|
176
165
|
let globalGraphIdCounter = 0
|
|
177
166
|
const uniqueGraphId = () => `graph-${++globalGraphIdCounter}`
|
|
178
167
|
|
|
@@ -515,11 +504,25 @@ export class ReactiveGraph<
|
|
|
515
504
|
subComp.super.delete(superComp)
|
|
516
505
|
}
|
|
517
506
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
507
|
+
// NOTE This function is performance-optimized (i.e. not using `Array.from`)
|
|
508
|
+
getSnapshot = (): ReactiveGraphSnapshot => {
|
|
509
|
+
const atoms: SerializedAtom[] = []
|
|
510
|
+
for (const atom of this.atoms) {
|
|
511
|
+
atoms.push(serializeAtom(atom))
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const effects: SerializedEffect[] = []
|
|
515
|
+
for (const effect of this.effects) {
|
|
516
|
+
effects.push(serializeEffect(effect))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const deferredEffects: string[] = []
|
|
520
|
+
for (const [effect] of this.deferredEffects) {
|
|
521
|
+
deferredEffects.push(effect.id)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { atoms, effects, deferredEffects }
|
|
525
|
+
}
|
|
523
526
|
|
|
524
527
|
subscribeToRefresh = (cb: () => void) => {
|
|
525
528
|
this.refreshCallbacks.add(cb)
|
|
@@ -561,3 +564,41 @@ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh:
|
|
|
561
564
|
export const throwContextNotSetError = (graph: ReactiveGraph<any, any, any>): never => {
|
|
562
565
|
throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph (${graph.id})`)
|
|
563
566
|
}
|
|
567
|
+
|
|
568
|
+
// NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
|
|
569
|
+
const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => {
|
|
570
|
+
const sub: string[] = []
|
|
571
|
+
for (const a of atom.sub) {
|
|
572
|
+
sub.push(a.id)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const super_: string[] = []
|
|
576
|
+
for (const a of atom.super) {
|
|
577
|
+
super_.push(a.id)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
_tag: atom._tag,
|
|
582
|
+
id: atom.id,
|
|
583
|
+
label: atom.label,
|
|
584
|
+
meta: atom.meta,
|
|
585
|
+
isDirty: atom.isDirty,
|
|
586
|
+
sub,
|
|
587
|
+
super: super_,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
|
|
592
|
+
const serializeEffect = (effect: Effect): SerializedEffect => {
|
|
593
|
+
const sub: string[] = []
|
|
594
|
+
for (const a of effect.sub) {
|
|
595
|
+
sub.push(a.id)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
_tag: effect._tag,
|
|
600
|
+
id: effect.id,
|
|
601
|
+
label: effect.label,
|
|
602
|
+
sub,
|
|
603
|
+
}
|
|
604
|
+
}
|