teamplay 0.2.6 → 0.3.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/index.js +1 -1
- package/package.json +8 -8
- package/react/convertToObserver.js +4 -30
- package/react/helpers.js +30 -2
- package/react/useSub.js +53 -3
- package/react/wrapIntoSuspense.js +43 -2
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { GLOBAL_ROOT_ID } from './orm/Root.js'
|
|
|
14
14
|
export const $ = _getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
|
|
15
15
|
export default $
|
|
16
16
|
export { default as sub } from './orm/sub.js'
|
|
17
|
-
export { default as useSub } from './react/useSub.js'
|
|
17
|
+
export { default as useSub, setUseDeferredValue as __setUseDeferredValue } from './react/useSub.js'
|
|
18
18
|
export { default as observer } from './react/observer.js'
|
|
19
19
|
export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
|
|
20
20
|
export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@nx-js/observer-util": "^4.1.3",
|
|
26
|
-
"@teamplay/backend": "^0.
|
|
27
|
-
"@teamplay/cache": "^0.
|
|
28
|
-
"@teamplay/channel": "^0.
|
|
29
|
-
"@teamplay/debug": "^0.
|
|
30
|
-
"@teamplay/schema": "^0.
|
|
31
|
-
"@teamplay/utils": "^0.
|
|
26
|
+
"@teamplay/backend": "^0.3.0",
|
|
27
|
+
"@teamplay/cache": "^0.3.0",
|
|
28
|
+
"@teamplay/channel": "^0.3.0",
|
|
29
|
+
"@teamplay/debug": "^0.3.0",
|
|
30
|
+
"@teamplay/schema": "^0.3.0",
|
|
31
|
+
"@teamplay/utils": "^0.3.0",
|
|
32
32
|
"diff-match-patch": "^1.0.5",
|
|
33
33
|
"events": "^3.3.0",
|
|
34
34
|
"json0-ot-diff": "^1.1.2",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"license": "MIT",
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "1cc50389c3f1aa7b75d727cb6f0b1f61627a6b22"
|
|
67
67
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
// to cleanup the observer() reaction when the component is unmounted or was abandoned and unmounts will never trigger.
|
|
3
|
-
// ref: https://github.com/mobxjs/mobx/blob/94bc4997c14152ff5aefcaac64d982d5c21ba51a/packages/mobx-react-lite/src/useObserver.ts
|
|
4
|
-
import { forwardRef as _forwardRef, useRef, useSyncExternalStore } from 'react'
|
|
1
|
+
import { forwardRef as _forwardRef, useRef } from 'react'
|
|
5
2
|
import { observe, unobserve } from '@nx-js/observer-util'
|
|
6
3
|
import _throttle from 'lodash/throttle.js'
|
|
7
4
|
import { createCaches, getDummyCache } from '@teamplay/cache'
|
|
8
5
|
import { __increment, __decrement } from '@teamplay/debug'
|
|
9
6
|
import executionContextTracker from './executionContextTracker.js'
|
|
10
|
-
import { pipeComponentMeta, useUnmount, useId } from './helpers.js'
|
|
7
|
+
import { pipeComponentMeta, useUnmount, useId, useTriggerUpdate } from './helpers.js'
|
|
11
8
|
import trapRender from './trapRender.js'
|
|
12
9
|
|
|
13
10
|
const DEFAULT_THROTTLE_TIMEOUT = 100
|
|
@@ -28,30 +25,7 @@ export default function convertToObserver (BaseComponent, {
|
|
|
28
25
|
let Component = (...args) => {
|
|
29
26
|
const [cache, destroyCache] = useCreateCacheRef(enableCache)
|
|
30
27
|
const componentId = useId()
|
|
31
|
-
|
|
32
|
-
const admRef = useRef()
|
|
33
|
-
if (!admRef.current) {
|
|
34
|
-
const adm = {
|
|
35
|
-
stateVersion: Symbol(), // eslint-disable-line symbol-description
|
|
36
|
-
onStoreChange: undefined,
|
|
37
|
-
subscribe (onStoreChange) {
|
|
38
|
-
adm.onStoreChange = () => {
|
|
39
|
-
adm.stateVersion = Symbol() // eslint-disable-line symbol-description
|
|
40
|
-
onStoreChange()
|
|
41
|
-
}
|
|
42
|
-
return () => {
|
|
43
|
-
adm.onStoreChange = undefined
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
getSnapshot () {
|
|
47
|
-
return adm.stateVersion
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
admRef.current = adm
|
|
51
|
-
}
|
|
52
|
-
const adm = admRef.current
|
|
53
|
-
|
|
54
|
-
useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot)
|
|
28
|
+
const triggerUpdate = useTriggerUpdate()
|
|
55
29
|
|
|
56
30
|
// wrap the BaseComponent into an observe decorator once.
|
|
57
31
|
// This way it will track any observable changes and will trigger rerender
|
|
@@ -61,7 +35,7 @@ export default function convertToObserver (BaseComponent, {
|
|
|
61
35
|
let update = () => {
|
|
62
36
|
// It's important to block updates caused by rendering itself
|
|
63
37
|
// (when the sync rendering is in progress).
|
|
64
|
-
if (!executionContextTracker.isActive())
|
|
38
|
+
if (!executionContextTracker.isActive()) triggerUpdate()
|
|
65
39
|
}
|
|
66
40
|
if (throttle) update = _throttle(update, throttle)
|
|
67
41
|
destroyRef.current = (where) => {
|
package/react/helpers.js
CHANGED
|
@@ -23,8 +23,21 @@ export function pipeComponentMeta (SourceComponent, TargetComponent, suffix = ''
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function useId () {
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const context = useContext(ComponentMetaContext)
|
|
27
|
+
if (!context) throw Error(ERRORS.useId)
|
|
28
|
+
return context.componentId
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useTriggerUpdate () {
|
|
32
|
+
const context = useContext(ComponentMetaContext)
|
|
33
|
+
if (!context) throw Error(ERRORS.useTriggerUpdate)
|
|
34
|
+
return context.triggerUpdate
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useScheduleUpdate () {
|
|
38
|
+
const context = useContext(ComponentMetaContext)
|
|
39
|
+
if (!context) throw Error(ERRORS.useScheduleUpdate)
|
|
40
|
+
return context.scheduleUpdate
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
export function useUnmount (fn) {
|
|
@@ -37,3 +50,18 @@ export function useUnmount (fn) {
|
|
|
37
50
|
[]
|
|
38
51
|
)
|
|
39
52
|
}
|
|
53
|
+
|
|
54
|
+
const ERRORS = {
|
|
55
|
+
useTriggerUpdate: `
|
|
56
|
+
useTriggerUpdate() can only be used inside a component wrapped with observer().
|
|
57
|
+
You have probably forgot to wrap your component with observer().
|
|
58
|
+
`,
|
|
59
|
+
useScheduleUpdate: `
|
|
60
|
+
useScheduleUpdate() can only be used inside a component wrapped with observer().
|
|
61
|
+
You have probably forgot to wrap your component with observer().
|
|
62
|
+
`,
|
|
63
|
+
useId: `
|
|
64
|
+
useId() can only be used inside a component wrapped with observer().
|
|
65
|
+
You have probably forgot to wrap your component with observer().
|
|
66
|
+
`
|
|
67
|
+
}
|
package/react/useSub.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { useRef, useDeferredValue } from 'react'
|
|
2
2
|
import sub from '../orm/sub.js'
|
|
3
|
+
import { useScheduleUpdate } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
let TEST_THROTTLING = false
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
+
// experimental feature to leverage useDeferredValue() to handle re-subscriptions.
|
|
8
|
+
// Currently it does lead to issues with extra rerenders and requires further investigation
|
|
9
|
+
let USE_DEFERRED_VALUE = false
|
|
10
|
+
|
|
7
11
|
export default function useSub (signal, params) {
|
|
12
|
+
if (USE_DEFERRED_VALUE) {
|
|
13
|
+
return useSubDeferred(signal, params) // eslint-disable-line react-hooks/rules-of-hooks
|
|
14
|
+
} else {
|
|
15
|
+
return useSubClassic(signal, params) // eslint-disable-line react-hooks/rules-of-hooks
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// version of sub() which works as a react hook and throws promise for Suspense
|
|
20
|
+
export function useSubDeferred (signal, params) {
|
|
8
21
|
signal = useDeferredValue(signal)
|
|
9
22
|
params = useDeferredValue(params ? JSON.stringify(params) : undefined)
|
|
10
|
-
params = params ? JSON.parse(params) : undefined
|
|
11
|
-
const promiseOrSignal = params ? sub(signal, params) : sub(signal)
|
|
23
|
+
params = params != null ? JSON.parse(params) : undefined
|
|
24
|
+
const promiseOrSignal = params != null ? sub(signal, params) : sub(signal)
|
|
12
25
|
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
13
26
|
if (promiseOrSignal.then) {
|
|
14
27
|
if (TEST_THROTTLING) {
|
|
@@ -27,6 +40,40 @@ export default function useSub (signal, params) {
|
|
|
27
40
|
return promiseOrSignal
|
|
28
41
|
}
|
|
29
42
|
|
|
43
|
+
// classic version which initially throws promise for Suspense
|
|
44
|
+
// but if we get a promise second time, we return the last signal and wait for promise to resolve
|
|
45
|
+
export function useSubClassic (signal, params) {
|
|
46
|
+
const $signalRef = useRef()
|
|
47
|
+
const activePromiseRef = useRef()
|
|
48
|
+
const scheduleUpdate = useScheduleUpdate()
|
|
49
|
+
const promiseOrSignal = params != null ? sub(signal, params) : sub(signal)
|
|
50
|
+
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
51
|
+
if (promiseOrSignal.then) {
|
|
52
|
+
let promise
|
|
53
|
+
if (TEST_THROTTLING) {
|
|
54
|
+
// simulate slow network
|
|
55
|
+
promise = new Promise((resolve, reject) => {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
promiseOrSignal.then(resolve, reject)
|
|
58
|
+
}, TEST_THROTTLING)
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
promise = promiseOrSignal
|
|
62
|
+
}
|
|
63
|
+
// first time we just throw the promise to be caught by Suspense
|
|
64
|
+
if (!$signalRef.current) throw promise
|
|
65
|
+
// if we already have a previous signal, we return it and wait for new promise to resolve
|
|
66
|
+
scheduleUpdate(promise)
|
|
67
|
+
return $signalRef.current
|
|
68
|
+
}
|
|
69
|
+
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
70
|
+
if ($signalRef.current !== promiseOrSignal) {
|
|
71
|
+
activePromiseRef.current = undefined
|
|
72
|
+
$signalRef.current = promiseOrSignal
|
|
73
|
+
}
|
|
74
|
+
return promiseOrSignal
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
export function setTestThrottling (ms) {
|
|
31
78
|
if (typeof ms !== 'number') throw Error('setTestThrottling() accepts only a number in ms')
|
|
32
79
|
if (ms === 0) throw Error('setTestThrottling(0) is not allowed, use resetTestThrottling() instead')
|
|
@@ -36,3 +83,6 @@ export function setTestThrottling (ms) {
|
|
|
36
83
|
export function resetTestThrottling () {
|
|
37
84
|
TEST_THROTTLING = false
|
|
38
85
|
}
|
|
86
|
+
export function setUseDeferredValue (value) {
|
|
87
|
+
USE_DEFERRED_VALUE = value
|
|
88
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// useSyncExternalStore is used to trigger an update same as in MobX
|
|
2
|
+
// ref: https://github.com/mobxjs/mobx/blob/94bc4997c14152ff5aefcaac64d982d5c21ba51a/packages/mobx-react-lite/src/useObserver.ts
|
|
3
|
+
import { useSyncExternalStore, forwardRef as _forwardRef, memo, createElement as el, Suspense, useId, useRef } from 'react'
|
|
2
4
|
import { pipeComponentMeta, pipeComponentDisplayName, ComponentMetaContext } from './helpers.js'
|
|
3
5
|
|
|
4
6
|
export default function wrapIntoSuspense ({
|
|
@@ -11,10 +13,49 @@ export default function wrapIntoSuspense ({
|
|
|
11
13
|
let SuspenseWrapper = (props, ref) => {
|
|
12
14
|
const componentId = useId()
|
|
13
15
|
const componentMetaRef = useRef()
|
|
16
|
+
const admRef = useRef()
|
|
17
|
+
if (!admRef.current) {
|
|
18
|
+
const adm = {
|
|
19
|
+
stateVersion: Symbol(), // eslint-disable-line symbol-description
|
|
20
|
+
onStoreChange: undefined,
|
|
21
|
+
scheduledUpdatePromise: undefined,
|
|
22
|
+
subscribe (onStoreChange) {
|
|
23
|
+
adm.onStoreChange = () => {
|
|
24
|
+
adm.stateVersion = Symbol() // eslint-disable-line symbol-description
|
|
25
|
+
onStoreChange()
|
|
26
|
+
}
|
|
27
|
+
adm.scheduleUpdate = promise => {
|
|
28
|
+
if (!promise?.then) throw Error('scheduleUpdate() expects a promise')
|
|
29
|
+
if (adm.scheduledUpdatePromise === promise) return
|
|
30
|
+
adm.scheduledUpdatePromise = promise
|
|
31
|
+
promise.then(() => {
|
|
32
|
+
if (adm.scheduledUpdatePromise !== promise) return
|
|
33
|
+
adm.scheduledUpdatePromise = undefined
|
|
34
|
+
adm.onStoreChange?.()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return () => {
|
|
38
|
+
adm.onStoreChange = undefined
|
|
39
|
+
adm.scheduledUpdatePromise = undefined
|
|
40
|
+
adm.scheduleUpdate = undefined
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
getSnapshot () {
|
|
44
|
+
return adm.stateVersion
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
admRef.current = adm
|
|
48
|
+
}
|
|
49
|
+
const adm = admRef.current
|
|
50
|
+
|
|
51
|
+
useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot)
|
|
52
|
+
|
|
14
53
|
if (!componentMetaRef.current) {
|
|
15
54
|
componentMetaRef.current = {
|
|
16
55
|
componentId,
|
|
17
|
-
createdAt: Date.now()
|
|
56
|
+
createdAt: Date.now(),
|
|
57
|
+
triggerUpdate: () => adm.onStoreChange?.(),
|
|
58
|
+
scheduleUpdate: promise => adm.scheduleUpdate?.(promise)
|
|
18
59
|
}
|
|
19
60
|
}
|
|
20
61
|
|