@based/react 4.3.1 → 4.5.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/src/Ctx.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react'
2
+ import { BasedClient } from '@based/client'
3
+
4
+ export const Ctx = createContext<BasedClient>(null)
@@ -0,0 +1,10 @@
1
+ import { createElement, FC, ReactNode } from 'react'
2
+ import { BasedClient } from '@based/client'
3
+ import { Ctx } from './Ctx'
4
+
5
+ export const Provider: FC<{
6
+ client: BasedClient
7
+ children: ReactNode
8
+ }> = ({ client, children }) => {
9
+ return createElement(Ctx.Provider, { value: client }, children)
10
+ }
package/src/index.ts CHANGED
@@ -1,161 +1,8 @@
1
- import {
2
- useContext,
3
- createContext,
4
- createElement,
5
- useState,
6
- useEffect,
7
- FC,
8
- ReactNode,
9
- } from 'react'
10
- import { BasedClient, AuthState } from '@based/client'
11
- import { BasedError } from '@based/client'
12
-
13
- const Ctx = createContext<BasedClient>(null)
14
-
15
- export const Provider: FC<{
16
- client: BasedClient
17
- children: ReactNode
18
- }> = ({ client, children }) => {
19
- return createElement(Ctx.Provider, { value: client }, children)
20
- }
21
-
22
- export const useAuthState = (): AuthState => {
23
- const client: BasedClient = useContext(Ctx)
24
- const [state, setState] = useState<AuthState>(client?.authState || {})
25
-
26
- useEffect(() => {
27
- if (client) {
28
- setState(client.authState)
29
- const listener = (authState) => {
30
- setState(authState)
31
- }
32
- client.on('authstate-change', listener)
33
- return () => client.off('authstate-change', listener)
34
- }
35
- }, [client])
36
-
37
- return state
38
- }
39
-
40
- const useLoadingListeners: Set<Function> = new Set()
41
- const hooksLoading: Set<number> = new Set()
42
-
43
- export const useLoading = () => {
44
- const [isLoading, setLoading] = useState(hooksLoading.size > 0)
45
- useEffect(() => {
46
- useLoadingListeners.add(setLoading)
47
- return () => {
48
- useLoadingListeners.delete(setLoading)
49
- }
50
- }, [])
51
- return isLoading
52
- }
53
-
54
- export const useConnected = () => {
55
- const client: BasedClient = useContext(Ctx)
56
- const [connected, setConnected] = useState(client.connected)
57
-
58
- useEffect(() => {
59
- if (client) {
60
- setConnected(client.connected)
61
- const listener = () => {
62
- setConnected(client.connected)
63
- }
64
- client.on('disconnect', listener)
65
- client.on('reconnect', listener)
66
- client.on('connect', listener)
67
- return () => {
68
- client.off('disconnect', listener)
69
- client.off('reconnect', listener)
70
- client.off('connect', listener)
71
- }
72
- }
73
- }, [client])
74
-
75
- return { connected }
76
- }
77
-
78
- export const useQuery = <T = any>(
79
- name?: string,
80
- payload?: any,
81
- opts?: {
82
- persistent: boolean
83
- }
84
- ): {
85
- loading: boolean
86
- data?: T
87
- error?: BasedError
88
- checksum?: number
89
- } => {
90
- const client: BasedClient = useContext(Ctx)
91
-
92
- if (client && name) {
93
- const q = client.query(name, payload, opts)
94
- const { id, cache } = q
95
- const [checksumOrError, update] = useState<number | BasedError>(
96
- cache?.checksum
97
- )
98
-
99
- useEffect(() => {
100
- const unsubscribe = q.subscribe(
101
- (_, checksum) => {
102
- update(checksum)
103
- },
104
- (err) => {
105
- update(err)
106
- }
107
- )
108
- return () => {
109
- const isLoading = hooksLoading.size > 0
110
- if (hooksLoading.delete(id) && !(hooksLoading.size > 0) && isLoading) {
111
- useLoadingListeners.forEach((fn) => {
112
- fn(false)
113
- })
114
- }
115
- unsubscribe()
116
- update(0)
117
- }
118
- }, [id])
119
-
120
- if (checksumOrError) {
121
- const isLoading = hooksLoading.size > 0
122
- if (hooksLoading.delete(id)) {
123
- if (!(hooksLoading.size > 0) && isLoading) {
124
- useLoadingListeners.forEach((fn) => {
125
- fn(false)
126
- })
127
- }
128
- }
129
-
130
- if (typeof checksumOrError === 'number') {
131
- if (!cache) {
132
- return { loading: true }
133
- }
134
-
135
- return { loading: false, data: cache.value, checksum: checksumOrError }
136
- }
137
-
138
- return { loading: false, error: checksumOrError }
139
- }
140
-
141
- const isLoading = hooksLoading.size > 0
142
- if (hooksLoading.add(id)) {
143
- if (!isLoading) {
144
- useLoadingListeners.forEach((fn) => {
145
- fn(true)
146
- })
147
- }
148
- }
149
-
150
- return { loading: true }
151
- }
152
-
153
- useState()
154
- useEffect(() => {}, [null])
155
-
156
- return { loading: true }
157
- }
158
-
159
- export const useClient = (): BasedClient => {
160
- return useContext(Ctx)
161
- }
1
+ export * from './Ctx'
2
+ export * from './Provider'
3
+ export * from './useAuthState'
4
+ export * from './useClient'
5
+ export * from './useConnected'
6
+ export * from './useLoading'
7
+ export * from './useQuery'
8
+ export * from './useWindow'
@@ -0,0 +1,21 @@
1
+ import { useContext, useState, useEffect } from 'react'
2
+ import { BasedClient, AuthState } from '@based/client'
3
+ import { Ctx } from './Ctx'
4
+
5
+ export const useAuthState = (): AuthState => {
6
+ const client: BasedClient = useContext(Ctx)
7
+ const [state, setState] = useState<AuthState>(client?.authState || {})
8
+
9
+ useEffect(() => {
10
+ if (client) {
11
+ setState(client.authState)
12
+ const listener = (authState) => {
13
+ setState(authState)
14
+ }
15
+ client.on('authstate-change', listener)
16
+ return () => client.off('authstate-change', listener)
17
+ }
18
+ }, [client])
19
+
20
+ return state
21
+ }
@@ -0,0 +1,7 @@
1
+ import { useContext } from 'react'
2
+ import { BasedClient } from '@based/client'
3
+ import { Ctx } from './Ctx'
4
+
5
+ export const useClient = (): BasedClient => {
6
+ return useContext(Ctx)
7
+ }
@@ -0,0 +1,27 @@
1
+ import { useContext, useState, useEffect } from 'react'
2
+ import { BasedClient } from '@based/client'
3
+ import { Ctx } from './Ctx'
4
+
5
+ export const useConnected = () => {
6
+ const client: BasedClient = useContext(Ctx)
7
+ const [connected, setConnected] = useState(client.connected)
8
+
9
+ useEffect(() => {
10
+ if (client) {
11
+ setConnected(client.connected)
12
+ const listener = () => {
13
+ setConnected(client.connected)
14
+ }
15
+ client.on('disconnect', listener)
16
+ client.on('reconnect', listener)
17
+ client.on('connect', listener)
18
+ return () => {
19
+ client.off('disconnect', listener)
20
+ client.off('reconnect', listener)
21
+ client.off('connect', listener)
22
+ }
23
+ }
24
+ }, [client])
25
+
26
+ return { connected }
27
+ }
@@ -0,0 +1,14 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ export const useLoadingListeners: Set<Function> = new Set()
4
+ export const hooksLoading: Set<number> = new Set()
5
+ export const useLoading = () => {
6
+ const [isLoading, setLoading] = useState(hooksLoading.size > 0)
7
+ useEffect(() => {
8
+ useLoadingListeners.add(setLoading)
9
+ return () => {
10
+ useLoadingListeners.delete(setLoading)
11
+ }
12
+ }, [])
13
+ return isLoading
14
+ }
@@ -0,0 +1,156 @@
1
+ import { useContext, useState, useEffect } from 'react'
2
+ import { BasedClient, BasedError } from '@based/client'
3
+ import { Ctx } from './Ctx'
4
+ import { hooksLoading, useLoadingListeners } from './useLoading'
5
+
6
+ export const useQueries = <T = any>(
7
+ name?: string,
8
+ payloads?: any[],
9
+ opts?: {
10
+ persistent: boolean
11
+ }
12
+ ): {
13
+ loading: boolean
14
+ data?: T
15
+ error?: BasedError
16
+ checksum?: string
17
+ }[] => {
18
+ // TODO add error handling
19
+ const client: BasedClient = useContext(Ctx)
20
+ let key = ''
21
+ let sum = ''
22
+
23
+ if (client && name) {
24
+ const queries = Array(payloads.length)
25
+ const result = payloads.map((payload, i) => {
26
+ const q = client.query(name, payload, opts)
27
+ const { id, cache } = q
28
+ queries[i] = q
29
+ key += id
30
+
31
+ if (cache) {
32
+ sum += cache.checksum
33
+ return { loading: false, data: cache.value, checksum: cache.checksum }
34
+ }
35
+
36
+ return { loading: true }
37
+ })
38
+
39
+ const [, update] = useState(sum)
40
+
41
+ useEffect(() => {
42
+ let raf
43
+ const listener = () => {
44
+ if (!raf) {
45
+ raf = requestAnimationFrame(() => {
46
+ raf = null
47
+ update(
48
+ queries.reduce((sum, { cache }) => {
49
+ return cache ? sum + cache.checksum : sum
50
+ }, '')
51
+ )
52
+ })
53
+ }
54
+ }
55
+
56
+ const unsubs = queries.map((q) => {
57
+ return q.subscribe(listener)
58
+ })
59
+
60
+ return () => {
61
+ unsubs.forEach((unsubscribe) => unsubscribe())
62
+ if (raf) {
63
+ cancelAnimationFrame(raf)
64
+ }
65
+ }
66
+ }, [key])
67
+
68
+ return result
69
+ }
70
+
71
+ useState(sum)
72
+ useEffect(() => {}, [key])
73
+
74
+ return Array(payloads.length).fill({ loading: true })
75
+ }
76
+
77
+ export const useQuery = <T = any>(
78
+ name?: string,
79
+ payload?: any,
80
+ opts?: {
81
+ persistent: boolean
82
+ }
83
+ ): {
84
+ loading: boolean
85
+ data?: T
86
+ error?: BasedError
87
+ checksum?: number
88
+ } => {
89
+ const client: BasedClient = useContext(Ctx)
90
+
91
+ if (client && name) {
92
+ const q = client.query(name, payload, opts)
93
+ const { id, cache } = q
94
+ const [checksumOrError, update] = useState<number | BasedError>(
95
+ cache?.checksum
96
+ )
97
+
98
+ useEffect(() => {
99
+ const unsubscribe = q.subscribe(
100
+ (_, checksum) => {
101
+ update(checksum)
102
+ },
103
+ (err) => {
104
+ update(err)
105
+ }
106
+ )
107
+ return () => {
108
+ const isLoading = hooksLoading.size > 0
109
+ if (hooksLoading.delete(id) && !(hooksLoading.size > 0) && isLoading) {
110
+ useLoadingListeners.forEach((fn) => {
111
+ fn(false)
112
+ })
113
+ }
114
+ unsubscribe()
115
+ update(0)
116
+ }
117
+ }, [id])
118
+
119
+ if (checksumOrError) {
120
+ const isLoading = hooksLoading.size > 0
121
+ if (hooksLoading.delete(id)) {
122
+ if (!(hooksLoading.size > 0) && isLoading) {
123
+ useLoadingListeners.forEach((fn) => {
124
+ fn(false)
125
+ })
126
+ }
127
+ }
128
+
129
+ if (typeof checksumOrError === 'number') {
130
+ if (!cache) {
131
+ return { loading: true }
132
+ }
133
+
134
+ return { loading: false, data: cache.value, checksum: checksumOrError }
135
+ }
136
+
137
+ return { loading: false, error: checksumOrError }
138
+ }
139
+
140
+ const isLoading = hooksLoading.size > 0
141
+ if (hooksLoading.add(id)) {
142
+ if (!isLoading) {
143
+ useLoadingListeners.forEach((fn) => {
144
+ fn(true)
145
+ })
146
+ }
147
+ }
148
+
149
+ return { loading: true }
150
+ }
151
+
152
+ useState()
153
+ useEffect(() => {}, [null])
154
+
155
+ return { loading: true }
156
+ }
@@ -0,0 +1,151 @@
1
+ import { useState, useEffect, useContext, useRef } from 'react'
2
+ import { Ctx } from './Ctx'
3
+ import { BasedClient, BasedQuery } from '@based/client'
4
+ import { hash } from '@saulx/hash'
5
+
6
+ type UseWindowState = {
7
+ loading: boolean
8
+ items: any[]
9
+ checksum?: number
10
+ }
11
+
12
+ export const useWindow = (
13
+ name: string,
14
+ getPayload: ({ offset, limit }) => any,
15
+ opts: {
16
+ path: string[]
17
+ pages: number[]
18
+ size: number
19
+ persistent?: boolean
20
+ },
21
+ dependencies?: any
22
+ ): UseWindowState => {
23
+ const [checksum, setCheckum] = useState(0)
24
+ const cache = useRef<UseWindowState>()
25
+ const client: BasedClient = useContext(Ctx)
26
+ const queries = useRef<BasedQuery[]>()
27
+ const unsubs = useRef<{}>()
28
+ const raf = useRef<number>()
29
+ const currModifyHash = useRef<string>()
30
+ const currResetHash = useRef<number>()
31
+ const modifyHash = `${opts.size}.${opts.persistent}`
32
+
33
+ if (dependencies) {
34
+ // complete reset
35
+ const resetHash = hash(dependencies)
36
+ if (currResetHash.current !== resetHash) {
37
+ currResetHash.current = resetHash
38
+ currModifyHash.current = null
39
+ cache.current = null
40
+ }
41
+ }
42
+
43
+ if (currModifyHash.current !== modifyHash) {
44
+ // reset queries but keep cache
45
+ for (const n in unsubs.current) {
46
+ unsubs.current[n]()
47
+ }
48
+ if (raf.current) {
49
+ cancelAnimationFrame(raf.current)
50
+ raf.current = null
51
+ }
52
+ queries.current = []
53
+ unsubs.current = {}
54
+ currModifyHash.current = modifyHash
55
+
56
+ if (!cache.current) {
57
+ cache.current = { items: [], loading: true, checksum: 0 }
58
+ }
59
+ }
60
+
61
+ useEffect(() => {
62
+ return () => {
63
+ for (const n in unsubs.current) {
64
+ unsubs.current[n]()
65
+ }
66
+ if (raf.current) {
67
+ cancelAnimationFrame(raf.current)
68
+ }
69
+ }
70
+ }, [])
71
+
72
+ if (client && name) {
73
+ const { pages, size, persistent = false, path } = opts
74
+ const active = new Set()
75
+
76
+ // add new subs
77
+ pages.forEach((n) => {
78
+ // pages start at 1 => shift to 0
79
+ n -= 1
80
+ active.add(n)
81
+ if (!(n in queries.current)) {
82
+ const payload = getPayload({ offset: n * size, limit: size })
83
+ const q = client.query(name, payload, { persistent })
84
+ queries.current[n] = q
85
+ }
86
+ if (n in unsubs.current) return
87
+ unsubs.current[n] = queries.current[n].subscribe(() => {
88
+ if (raf.current) return
89
+ raf.current = requestAnimationFrame(() => {
90
+ raf.current = null
91
+ setCheckum(
92
+ queries.current.reduce((combined, { cache }) => {
93
+ const checksum = cache?.checksum
94
+ return checksum ? combined + checksum : combined
95
+ }, 0)
96
+ )
97
+ })
98
+ })
99
+ })
100
+
101
+ // remove inactive subs
102
+ queries.current.forEach((_, n) => {
103
+ if (n in unsubs.current && !active.has(n)) {
104
+ unsubs.current[n]()
105
+ delete unsubs.current[n]
106
+ }
107
+ })
108
+
109
+ if (cache.current.checksum === checksum) {
110
+ return cache.current
111
+ }
112
+
113
+ let l = queries.current.length
114
+ cache.current.items = []
115
+ cache.current.loading = false
116
+ cache.current.checksum = checksum
117
+
118
+ while (l--) {
119
+ let i = size * l
120
+ const q = queries.current[l]
121
+ const m = i + size
122
+
123
+ if (q) {
124
+ if (q.cache) {
125
+ let data = q.cache.value
126
+ for (const i of path) {
127
+ data = data?.[i]
128
+ }
129
+ if (data) {
130
+ for (let j = 0; i < m; i++) {
131
+ const item = data[j++]
132
+ if (!item) break
133
+ cache.current.items[i] = item
134
+ }
135
+ }
136
+ } else {
137
+ cache.current.loading = true
138
+ }
139
+ }
140
+
141
+ // // fill up empty items with null
142
+ // for (; i < m; i++) {
143
+ // if (!(i in cache.current.items)) {
144
+ // cache.current.items[i] = null
145
+ // }
146
+ // }
147
+ }
148
+ }
149
+
150
+ return cache.current
151
+ }
@@ -16,10 +16,37 @@ const counter: BasedQueryFunction<{ speed: number }, { cnt: number }> = (
16
16
  }
17
17
  }
18
18
 
19
+ const fakeDb = (_based, { offset, limit }, update) => {
20
+ let i
21
+ let cnt = 0
22
+ const timer = setTimeout(() => {
23
+ const doit = () => {
24
+ cnt++
25
+ const things = Array.from(Array(limit)).map((_, i) => {
26
+ return {
27
+ id: `${i + offset} - ${cnt}`,
28
+ }
29
+ })
30
+ update({ things })
31
+ }
32
+
33
+ i = setInterval(doit, 1e3)
34
+ doit()
35
+ }, 100)
36
+ return () => {
37
+ clearTimeout(timer)
38
+ clearInterval(i)
39
+ }
40
+ }
41
+
19
42
  const server = new BasedServer({
20
43
  port: 8081,
21
44
  functions: {
22
45
  configs: {
46
+ 'fake-db': {
47
+ type: 'query',
48
+ fn: fakeDb,
49
+ },
23
50
  counter: {
24
51
  type: 'query',
25
52
  fn: counter,
package/test/browser.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { useState } from 'react'
2
2
  import based from '@based/client'
3
3
  import { createRoot } from 'react-dom/client'
4
- import { Provider, useQuery, useLoading } from '../src'
4
+ import { Provider, useQuery, useLoading, useWindow } from '../src'
5
5
 
6
6
  const client = based({
7
7
  url: 'ws://localhost:8081',
@@ -37,10 +37,11 @@ const Tester = () => {
37
37
  height: 50,
38
38
  background: somethingIsLoading ? 'red' : 'blue',
39
39
  }}
40
- ></div>
41
- {payloads.map((v) => {
40
+ />
41
+ {payloads.map((v, i) => {
42
42
  return (
43
43
  <div
44
+ key={i}
44
45
  style={{
45
46
  border: '1px solid green',
46
47
  padding: 5,
@@ -53,12 +54,68 @@ const Tester = () => {
53
54
  )
54
55
  })}
55
56
  </div>
56
-
57
- <pre>{JSON.stringify(x, null, 2)}</pre>
58
57
  </div>
59
58
  )
60
59
  }
61
60
 
61
+ const UseWindowTester = () => {
62
+ const [pages, setPages] = useState([1, 2])
63
+ const [size, setSize] = useState(4)
64
+ const { items, loading } = useWindow(
65
+ 'fake-db',
66
+ ({ offset, limit }) => {
67
+ return {
68
+ offset,
69
+ limit,
70
+ }
71
+ },
72
+ {
73
+ path: ['things'], // where are the items in the response?
74
+ pages, // array of page numbers - starts at 1
75
+ size, // amount of items per page
76
+ }
77
+ )
78
+
79
+ return (
80
+ <>
81
+ <div style={{ display: 'flex' }}>
82
+ <pre>{JSON.stringify({ loading, items }, null, 2)}</pre>
83
+ <div>
84
+ {items.map((item, index) => {
85
+ return <div key={index}>{item?.id || '-'}</div>
86
+ })}
87
+ </div>
88
+ </div>
89
+ <button
90
+ style={{
91
+ background: 'lightgrey',
92
+ padding: 16,
93
+ }}
94
+ onClick={() => {
95
+ setPages(
96
+ pages.map((n) => {
97
+ return n + 3
98
+ })
99
+ )
100
+ }}
101
+ >
102
+ Move ({pages.join(',')})
103
+ </button>
104
+ <button
105
+ style={{
106
+ background: 'lightgrey',
107
+ padding: 16,
108
+ }}
109
+ onClick={() => {
110
+ setSize(size + 1)
111
+ }}
112
+ >
113
+ Resize ({size})
114
+ </button>
115
+ </>
116
+ )
117
+ }
118
+
62
119
  function App() {
63
120
  return (
64
121
  <div
@@ -68,6 +125,7 @@ function App() {
68
125
  >
69
126
  <Provider client={client}>
70
127
  <Tester />
128
+ <UseWindowTester />
71
129
  </Provider>
72
130
  </div>
73
131
  )