@cero-base/react 0.0.1
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 +96 -0
- package/package.json +50 -0
- package/src/CLAUDE.md +3 -0
- package/src/README.md +89 -0
- package/src/hooks/index.js +7 -0
- package/src/hooks/use-cero.js +12 -0
- package/src/hooks/use-client.jsx +39 -0
- package/src/hooks/use-collection.jsx +30 -0
- package/src/hooks/use-members.jsx +11 -0
- package/src/hooks/use-profile.jsx +9 -0
- package/src/hooks/use-query.js +48 -0
- package/src/hooks/use-rooms.jsx +8 -0
- package/src/index.js +2 -0
- package/src/ui/cero.jsx +19 -0
- package/src/ui/context.js +4 -0
- package/src/ui/index.js +2 -0
- package/src/ui/room.jsx +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @cero-base/react
|
|
2
|
+
|
|
3
|
+
React hooks and providers for cero-base. Works with both `CeroBase` and `Client` (RPC) instances.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cero-base/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import { Cero, Room } from '@cero-base/react'
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
return (
|
|
18
|
+
<Cero value={db}>
|
|
19
|
+
<Room id={roomId}>
|
|
20
|
+
<Chat />
|
|
21
|
+
</Room>
|
|
22
|
+
</Cero>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Hooks
|
|
28
|
+
|
|
29
|
+
### useCero()
|
|
30
|
+
|
|
31
|
+
Access the db instance from context.
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
const db = useCero()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### useRoom()
|
|
38
|
+
|
|
39
|
+
Access the current Room from context.
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
const room = useRoom()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### useCollection(name, query?)
|
|
46
|
+
|
|
47
|
+
Subscribe to a collection. Returns live data + CRUD methods.
|
|
48
|
+
|
|
49
|
+
- Local/private collections: uses `useCero()` context
|
|
50
|
+
- Shared collections: requires `<Room>` provider
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
const { data, put, del, get, sub } = useCollection('messages')
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### useRooms()
|
|
57
|
+
|
|
58
|
+
List all rooms. Re-fetches on update events.
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
const { data: rooms } = useRooms()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### useMembers()
|
|
65
|
+
|
|
66
|
+
Subscribe to room members. Requires `<Room>` provider.
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
const { data: members } = useMembers()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### useProfile()
|
|
73
|
+
|
|
74
|
+
Subscribe to identity profile. Returns profile data + setter.
|
|
75
|
+
|
|
76
|
+
```jsx
|
|
77
|
+
const { data: profile, set } = useProfile()
|
|
78
|
+
await set({ name: 'Alice' })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Exports
|
|
82
|
+
|
|
83
|
+
| Export | Description |
|
|
84
|
+
| --------------------- | ------------------------------------ |
|
|
85
|
+
| `Cero` | Context provider — wraps db instance |
|
|
86
|
+
| `Room` | Context provider — wraps room by id |
|
|
87
|
+
| `useCero()` | Access db from context |
|
|
88
|
+
| `useRoom()` | Access room from context |
|
|
89
|
+
| `useProfile()` | Subscribe to profile |
|
|
90
|
+
| `useRooms()` | Subscribe to room list |
|
|
91
|
+
| `useCollection(name)` | Subscribe to collection data |
|
|
92
|
+
| `useMembers()` | Subscribe to room members |
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
Apache-2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cero-base/react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React hooks and providers for cero-base",
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "brittle test/*.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "src/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@cero-base/core": "^0.0.1",
|
|
22
|
+
"@cero-base/rpc": "^0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@testing-library/react": "^16.0.0",
|
|
26
|
+
"brittle": "^3.7.0",
|
|
27
|
+
"global-jsdom": "^25.0.0",
|
|
28
|
+
"react": "^19.2.0",
|
|
29
|
+
"react-dom": "^19.2.0",
|
|
30
|
+
"streamx": "^2.22.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=18",
|
|
34
|
+
"@simplestack/store": ">=0.7"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"react": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"@simplestack/store": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/lekinox/cero-base.git",
|
|
48
|
+
"directory": "packages/react"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/CLAUDE.md
ADDED
package/src/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# cero-base/react
|
|
2
|
+
|
|
3
|
+
React hooks and providers for cero-base. Works with both direct Cero instances and RPC proxy clients.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { CeroProvider } from 'cero-base/react'
|
|
9
|
+
|
|
10
|
+
const db = new Chats('./data')
|
|
11
|
+
|
|
12
|
+
createRoot(root).render(
|
|
13
|
+
<CeroProvider value={db}>
|
|
14
|
+
<App />
|
|
15
|
+
</CeroProvider>
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Hooks
|
|
20
|
+
|
|
21
|
+
### useCero()
|
|
22
|
+
|
|
23
|
+
Access the Cero instance from context.
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
const db = useCero()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### useRoom()
|
|
30
|
+
|
|
31
|
+
Access the current Room from context. Returns `null` if no `RoomProvider`.
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
const room = useRoom()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### useCollection(name, query?)
|
|
38
|
+
|
|
39
|
+
Subscribe to a collection. Returns live data + CRUD methods.
|
|
40
|
+
|
|
41
|
+
- Local/private collections: uses `useCero()` context
|
|
42
|
+
- Shared collections: requires `RoomProvider`
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
const { data, loading, insert, update, remove, get } = useCollection('tasks')
|
|
46
|
+
|
|
47
|
+
// With filter
|
|
48
|
+
const { data: done } = useCollection('tasks', { done: true })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### useRooms()
|
|
52
|
+
|
|
53
|
+
List all rooms. Re-fetches on 'update' events.
|
|
54
|
+
|
|
55
|
+
```jsx
|
|
56
|
+
const { data: rooms, loading } = useRooms()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### useMembers()
|
|
60
|
+
|
|
61
|
+
Subscribe to room members. Requires `RoomProvider`.
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
const { data: members, loading } = useMembers()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### useProfile()
|
|
68
|
+
|
|
69
|
+
Subscribe to identity profile. Returns profile data + setter.
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
const { data: profile, loading, set } = useProfile()
|
|
73
|
+
await set({ name: 'Alice' })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Room Context
|
|
77
|
+
|
|
78
|
+
Wrap shared collection views with `RoomProvider`:
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
import { RoomProvider } from 'cero-base/react'
|
|
82
|
+
import { Room } from 'cero-base'
|
|
83
|
+
|
|
84
|
+
const room = await Room.from(db, roomId)
|
|
85
|
+
|
|
86
|
+
<RoomProvider value={room}>
|
|
87
|
+
<ChatView /> {/* useCollection('messages') works here */}
|
|
88
|
+
</RoomProvider>
|
|
89
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { CeroContext, RoomContext } from '../ui/context.js'
|
|
3
|
+
|
|
4
|
+
export function useCero() {
|
|
5
|
+
const db = useContext(CeroContext)
|
|
6
|
+
if (!db) throw new Error('useCero requires <Cero> provider')
|
|
7
|
+
return db
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useRoom() {
|
|
11
|
+
return useContext(RoomContext)
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { Client, getRpc } from '@cero-base/rpc'
|
|
3
|
+
|
|
4
|
+
export function useClient(setup, schema, spec) {
|
|
5
|
+
const [db, setDb] = useState(null)
|
|
6
|
+
const [ready, setReady] = useState(false)
|
|
7
|
+
const [error, setError] = useState(null)
|
|
8
|
+
const ref = useRef(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let client = null
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const ctx = setup()
|
|
15
|
+
ref.current = ctx
|
|
16
|
+
const rpc = getRpc(ctx.pipe, spec)
|
|
17
|
+
client = new Client(rpc, schema, spec)
|
|
18
|
+
|
|
19
|
+
client
|
|
20
|
+
.ready()
|
|
21
|
+
.then(() => {
|
|
22
|
+
setDb(client)
|
|
23
|
+
setReady(true)
|
|
24
|
+
})
|
|
25
|
+
.catch((err) => setError('RPC ready failed: ' + (err.message || String(err))))
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setError('Client setup failed: ' + (err.message || String(err)))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
try {
|
|
32
|
+
if (client) client.close()
|
|
33
|
+
if (ref.current?.destroy) ref.current.destroy()
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
return { db, ready, error }
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react'
|
|
2
|
+
import { useCero, useRoom } from './use-cero.js'
|
|
3
|
+
import { useQuery } from './use-query.js'
|
|
4
|
+
import { SCOPE_SHARED } from '@cero-base/core/constants'
|
|
5
|
+
|
|
6
|
+
export function useCollection(name, query, opts) {
|
|
7
|
+
const db = useCero()
|
|
8
|
+
const room = useRoom()
|
|
9
|
+
const scope = db.schema.getScope(name)
|
|
10
|
+
|
|
11
|
+
if (scope === SCOPE_SHARED && !room) {
|
|
12
|
+
throw new Error(`useCollection('${name}') requires <Room> provider (shared scope)`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const owner = scope === SCOPE_SHARED ? room : db
|
|
16
|
+
const col = useMemo(() => owner.collection(name), [owner, name])
|
|
17
|
+
const queryKey = query ? JSON.stringify(query) : ''
|
|
18
|
+
|
|
19
|
+
const { data, busy, error } = useQuery(() => col.sub(query || {}, opts || {}), [col, queryKey])
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
data: data || [],
|
|
23
|
+
busy,
|
|
24
|
+
error,
|
|
25
|
+
put: useCallback((doc) => col.put(doc), [col]),
|
|
26
|
+
del: useCallback((q) => col.del(q), [col]),
|
|
27
|
+
get: useCallback((q, o) => col.get(q, o), [col]),
|
|
28
|
+
sub: useCallback((q, o) => col.sub(q || {}, o || {}), [col])
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRoom } from './use-cero.js'
|
|
2
|
+
import { useQuery } from './use-query.js'
|
|
3
|
+
|
|
4
|
+
export function useMembers() {
|
|
5
|
+
const room = useRoom()
|
|
6
|
+
if (!room) throw new Error('useMembers requires <Room> provider')
|
|
7
|
+
|
|
8
|
+
const { data, busy, error } = useQuery(() => room.members.sub(), [room])
|
|
9
|
+
|
|
10
|
+
return { data: data || [], busy, error }
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import { useCero } from './use-cero.js'
|
|
3
|
+
import { useQuery } from './use-query.js'
|
|
4
|
+
|
|
5
|
+
export function useProfile() {
|
|
6
|
+
const db = useCero()
|
|
7
|
+
const { data, busy, error } = useQuery(() => db.profile.sub(), [db])
|
|
8
|
+
return { data, busy, error, set: useCallback((p) => db.profile.set(p), [db]) }
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { store } from '@simplestack/store'
|
|
3
|
+
import { useStoreValue } from '@simplestack/store/react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* useQuery — the primitive. Subscribes to a Readable stream, returns reactive data.
|
|
7
|
+
*
|
|
8
|
+
* All named hooks compose from this. Exported as escape hatch for custom subscriptions.
|
|
9
|
+
*
|
|
10
|
+
* Uses @simplestack/store as the reactive layer — signal-based, fine-grained updates.
|
|
11
|
+
* The store persists between mounts (stale-while-revalidate for free).
|
|
12
|
+
* Redundant updates are skipped via Object.is inside store.select().set().
|
|
13
|
+
*
|
|
14
|
+
* @param {Function} streamFn - Returns a Readable stream (from .sub())
|
|
15
|
+
* @param {Array} deps - Dependency array (re-subscribes when changed)
|
|
16
|
+
* @returns {{ data: any, busy: boolean, error: Error|null }}
|
|
17
|
+
*/
|
|
18
|
+
export function useQuery(streamFn, deps) {
|
|
19
|
+
const s = useRef(null)
|
|
20
|
+
const ref = useRef(null)
|
|
21
|
+
if (s.current === null) {
|
|
22
|
+
s.current = store({ data: null, busy: true, error: null })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const st = s.current
|
|
27
|
+
st.select('busy').set(true)
|
|
28
|
+
|
|
29
|
+
const stream = streamFn()
|
|
30
|
+
|
|
31
|
+
stream.on('data', (d) => {
|
|
32
|
+
if (d === ref.current) return // skip redundant updates
|
|
33
|
+
ref.current = d
|
|
34
|
+
st.select('data').set(d)
|
|
35
|
+
st.select('busy').set(false)
|
|
36
|
+
st.select('error').set(null)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
stream.on('error', (err) => {
|
|
40
|
+
st.select('error').set(err)
|
|
41
|
+
st.select('busy').set(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return () => stream.destroy()
|
|
45
|
+
}, deps) // eslint-disable-line react-hooks/exhaustive-deps
|
|
46
|
+
|
|
47
|
+
return useStoreValue(s.current)
|
|
48
|
+
}
|
package/src/index.js
ADDED
package/src/ui/cero.jsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { CeroContext } from './context.js'
|
|
3
|
+
|
|
4
|
+
export function Cero({ value, children }) {
|
|
5
|
+
const [ready, setReady] = useState(value.opened)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!value.opened)
|
|
9
|
+
value
|
|
10
|
+
.ready()
|
|
11
|
+
.then(() => setReady(true))
|
|
12
|
+
.catch(console.error)
|
|
13
|
+
else setReady(true)
|
|
14
|
+
}, [value])
|
|
15
|
+
|
|
16
|
+
if (!ready) return null
|
|
17
|
+
|
|
18
|
+
return <CeroContext.Provider value={value}>{children}</CeroContext.Provider>
|
|
19
|
+
}
|
package/src/ui/index.js
ADDED
package/src/ui/room.jsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { RoomContext } from './context.js'
|
|
3
|
+
import { useCero } from '../hooks/use-cero.js'
|
|
4
|
+
|
|
5
|
+
export function Room({ id, children }) {
|
|
6
|
+
const db = useCero()
|
|
7
|
+
const [room, setRoom] = useState(null)
|
|
8
|
+
const roomRef = useRef(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!id) return
|
|
12
|
+
let cancelled = false
|
|
13
|
+
|
|
14
|
+
db.rooms.open(id).then((r) => {
|
|
15
|
+
if (cancelled) {
|
|
16
|
+
r.close()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
roomRef.current = r
|
|
20
|
+
setRoom(r)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
cancelled = true
|
|
25
|
+
if (roomRef.current) {
|
|
26
|
+
roomRef.current.close()
|
|
27
|
+
roomRef.current = null
|
|
28
|
+
}
|
|
29
|
+
setRoom(null)
|
|
30
|
+
}
|
|
31
|
+
}, [db, id])
|
|
32
|
+
|
|
33
|
+
if (!room) return null
|
|
34
|
+
|
|
35
|
+
return <RoomContext.Provider value={room}>{children}</RoomContext.Provider>
|
|
36
|
+
}
|