teamplay 0.4.0-alpha.1 → 0.4.0-alpha.2

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/index.d.ts CHANGED
@@ -73,6 +73,8 @@ export function useBatchQueryDoc (collection: string, query: any, options?: any)
73
73
  export function useBatchQueryDoc$ (collection: string, query: any, options?: any): any
74
74
  export function useAsyncQueryDoc (collection: string, query: any, options?: any): [any, any]
75
75
  export function useAsyncQueryDoc$ (collection: string, query: any, options?: any): any
76
+ export function useLocalDoc (collection: string, id: any): [any, any]
77
+ export function useLocalDoc$ (collection: string, id: any): any
76
78
  export function emit (eventName: string, ...args: any[]): void
77
79
  export function useOn (
78
80
  eventName: 'change' | 'all',
@@ -82,6 +84,15 @@ export function useOn (
82
84
  ): void
83
85
  export function useOn (eventName: string, handler: (...args: any[]) => void, deps?: any[]): void
84
86
  export function useEmit (): (eventName: string, ...args: any[]) => void
87
+ export function batch<T = any> (fn?: () => T): T | undefined
88
+ export function batchModel<T = any> (fn?: () => T): T | undefined
89
+ export function clone<T = any> (value: T): T
90
+ export function initLocalCollection (name: string): any
91
+ export function useApi (api: (...args: any[]) => any, args?: any[], options?: { debounce?: number }): [any, boolean, any]
92
+ type EffectCleanup = (() => void) | undefined
93
+ export function useDidUpdate (fn: () => EffectCleanup, deps?: any[]): void
94
+ export function useOnce (condition: any, fn: () => EffectCleanup): void
95
+ export function useSyncEffect (fn: () => EffectCleanup, deps?: any[]): void
85
96
  export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
86
97
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
87
98
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
package/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // In future, we might want to separate the plain JS and React APIs
6
6
  import { getRootSignal as _getRootSignal, GLOBAL_ROOT_ID } from './orm/Root.js'
7
7
  import universal$ from './react/universal$.js'
8
+ import useApi from './react/useApi.js'
8
9
 
9
10
  export { default as Signal, SEGMENTS } from './orm/Signal.js'
10
11
  export { __DEBUG_SIGNALS_CACHE__, rawSignal, getSignalClass } from './orm/getSignal.js'
@@ -29,6 +30,8 @@ export {
29
30
  useModel,
30
31
  useLocal,
31
32
  useLocal$,
33
+ useLocalDoc,
34
+ useLocalDoc$,
32
35
  useSession,
33
36
  useSession$,
34
37
  usePage,
@@ -57,12 +60,50 @@ export {
57
60
  useAsyncQueryDoc$
58
61
  } from './orm/Compat/hooksCompat.js'
59
62
  export { emit, useOn, useEmit } from './orm/Compat/eventsCompat.js'
63
+ export {
64
+ useDidUpdate,
65
+ useOnce,
66
+ useSyncEffect
67
+ } from './react/helpers.js'
60
68
  export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
61
69
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
62
70
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
63
71
  export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
64
72
  export { accessControl } from '@teamplay/utils/accessControl'
65
73
 
74
+ export function batch (fn) {
75
+ return $.batch(fn)
76
+ }
77
+
78
+ export function batchModel (fn) {
79
+ return $.batch(fn)
80
+ }
81
+
82
+ export function clone (value) {
83
+ if (typeof globalThis.structuredClone === 'function') {
84
+ try {
85
+ return globalThis.structuredClone(value)
86
+ } catch {}
87
+ }
88
+ if (value == null) return value
89
+ return JSON.parse(JSON.stringify(value))
90
+ }
91
+
92
+ export function initLocalCollection (name) {
93
+ if (typeof name !== 'string') throw Error('initLocalCollection() expects a collection name')
94
+ if (!name) return
95
+ const segments = name.split('.').filter(Boolean)
96
+ if (!segments.length) return
97
+ let $cursor = $
98
+ for (const segment of segments) {
99
+ $cursor = $cursor[segment]
100
+ }
101
+ if ($cursor.get() == null) $cursor.set({})
102
+ return $cursor
103
+ }
104
+
105
+ export { useApi }
106
+
66
107
  export function getRootSignal (options) {
67
108
  return _getRootSignal({
68
109
  rootFunction: universal$,
@@ -44,6 +44,18 @@ export function useLocal (path) {
44
44
  return [$sig.get(), $sig]
45
45
  }
46
46
 
47
+ export function useLocalDoc$ (collection, id) {
48
+ if (collection == null) throw Error('useLocalDoc() expects a collection name')
49
+ if (id == null) return undefined
50
+ return $root[collection][id]
51
+ }
52
+
53
+ export function useLocalDoc (collection, id) {
54
+ const $doc = useLocalDoc$(collection, id)
55
+ if (!$doc) return [undefined, undefined]
56
+ return [$doc.get(), $doc]
57
+ }
58
+
47
59
  export function useSession$ (path) {
48
60
  return useLocal$(prefixLocalPath('_session', path))
49
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.1",
3
+ "version": "0.4.0-alpha.2",
4
4
  "description": "Full-stack signals ORM with multiplayer",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -81,5 +81,5 @@
81
81
  ]
82
82
  },
83
83
  "license": "MIT",
84
- "gitHead": "d50a0206ce8b90189b6529d0b46db2ca079cae19"
84
+ "gitHead": "ad790a31ac628c9ea5ac851572533620ac0b7602"
85
85
  }
package/react/helpers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useContext, createContext, useRef, useEffect } from 'react'
1
+ import { useContext, createContext, useRef, useEffect, useLayoutEffect } from 'react'
2
2
 
3
3
  export const ComponentMetaContext = createContext({})
4
4
 
@@ -69,6 +69,52 @@ export function useUnmount (fn) {
69
69
  )
70
70
  }
71
71
 
72
+ export function useDidUpdate (fn, deps) {
73
+ const isFirst = useRef(true)
74
+ const stableDeps = useStableDeps(deps)
75
+ useEffect(() => {
76
+ if (isFirst.current) {
77
+ isFirst.current = false
78
+ return
79
+ }
80
+ return fn()
81
+ }, [fn, stableDeps])
82
+ }
83
+
84
+ export function useOnce (condition, fn) {
85
+ const fired = useRef(false)
86
+ useEffect(() => {
87
+ if (fired.current) return
88
+ if (!condition) return
89
+ fired.current = true
90
+ return fn()
91
+ }, [condition, fn])
92
+ }
93
+
94
+ export function useSyncEffect (fn, deps) {
95
+ const stableDeps = useStableDeps(deps)
96
+ useLayoutEffect(fn, [fn, stableDeps])
97
+ }
98
+
99
+ function useStableDeps (deps) {
100
+ const depsRef = useRef([])
101
+ const nextDeps = Array.isArray(deps) ? deps : []
102
+ if (!shallowEqualArrays(depsRef.current, nextDeps)) {
103
+ depsRef.current = nextDeps
104
+ }
105
+ return depsRef.current
106
+ }
107
+
108
+ function shallowEqualArrays (a, b) {
109
+ if (a === b) return true
110
+ if (!Array.isArray(a) || !Array.isArray(b)) return false
111
+ if (a.length !== b.length) return false
112
+ for (let i = 0; i < a.length; i++) {
113
+ if (!Object.is(a[i], b[i])) return false
114
+ }
115
+ return true
116
+ }
117
+
72
118
  const ERRORS = {
73
119
  useTriggerUpdate: `
74
120
  useTriggerUpdate() can only be used inside a component wrapped with observer().
@@ -0,0 +1,63 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ export default function useApi (api, args = [], options = {}) {
4
+ const { debounce = 0 } = options || {}
5
+ const [data, setData] = useState()
6
+ const [error, setError] = useState()
7
+ const [loading, setLoading] = useState(false)
8
+ const requestIdRef = useRef(0)
9
+ const stableArgs = useStableDeps(Array.isArray(args) ? args : [args])
10
+
11
+ useEffect(() => {
12
+ if (typeof api !== 'function') return
13
+ let cancelled = false
14
+ const requestId = ++requestIdRef.current
15
+ let timer
16
+
17
+ const run = async () => {
18
+ try {
19
+ setLoading(true)
20
+ const result = await api(...stableArgs)
21
+ if (cancelled || requestId !== requestIdRef.current) return
22
+ setData(result)
23
+ setError(undefined)
24
+ } catch (err) {
25
+ if (cancelled || requestId !== requestIdRef.current) return
26
+ setError(err)
27
+ } finally {
28
+ if (!cancelled && requestId === requestIdRef.current) setLoading(false)
29
+ }
30
+ }
31
+
32
+ if (debounce > 0) {
33
+ timer = setTimeout(run, debounce)
34
+ } else {
35
+ run()
36
+ }
37
+
38
+ return () => {
39
+ cancelled = true
40
+ if (timer) clearTimeout(timer)
41
+ }
42
+ }, [api, debounce, stableArgs])
43
+
44
+ return [data, loading, error]
45
+ }
46
+
47
+ function useStableDeps (deps) {
48
+ const depsRef = useRef([])
49
+ if (!shallowEqualArrays(depsRef.current, deps)) {
50
+ depsRef.current = deps
51
+ }
52
+ return depsRef.current
53
+ }
54
+
55
+ function shallowEqualArrays (a, b) {
56
+ if (a === b) return true
57
+ if (!Array.isArray(a) || !Array.isArray(b)) return false
58
+ if (a.length !== b.length) return false
59
+ for (let i = 0; i < a.length; i++) {
60
+ if (!Object.is(a[i], b[i])) return false
61
+ }
62
+ return true
63
+ }