@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 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
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
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,7 @@
1
+ export * from './use-cero.js'
2
+ export * from './use-query.js'
3
+ export * from './use-client.jsx'
4
+ export * from './use-profile.jsx'
5
+ export * from './use-rooms.jsx'
6
+ export * from './use-collection.jsx'
7
+ export * from './use-members.jsx'
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { useCero } from './use-cero.js'
2
+ import { useQuery } from './use-query.js'
3
+
4
+ export function useRooms() {
5
+ const db = useCero()
6
+ const { data, busy, error } = useQuery(() => db.rooms.sub(), [db])
7
+ return { data: data || [], busy, error }
8
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './ui/index.js'
2
+ export * from './hooks/index.js'
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react'
2
+
3
+ export const CeroContext = createContext(null)
4
+ export const RoomContext = createContext(null)
@@ -0,0 +1,2 @@
1
+ export * from './cero.jsx'
2
+ export * from './room.jsx'
@@ -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
+ }