teamplay 0.3.7 → 0.3.9
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 +2 -1
- package/package.json +8 -8
- package/react/helpers.js +10 -0
- package/react/useSub.js +53 -34
- package/react/wrapIntoSuspense.js +10 -10
- package/utils/MockFinalizationRegistry.js +52 -32
- package/utils/MockWeakRef.js +10 -1
package/index.js
CHANGED
|
@@ -14,9 +14,10 @@ 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, setUseDeferredValue as __setUseDeferredValue } from './react/useSub.js'
|
|
17
|
+
export { default as useSub, useAsyncSub, 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
|
+
export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
|
|
20
21
|
export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
|
|
21
22
|
export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
|
|
22
23
|
export { accessControl } from '@teamplay/utils/accessControl'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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.3.
|
|
27
|
-
"@teamplay/cache": "^0.3.
|
|
28
|
-
"@teamplay/channel": "^0.3.
|
|
29
|
-
"@teamplay/debug": "^0.3.
|
|
30
|
-
"@teamplay/schema": "^0.3.
|
|
31
|
-
"@teamplay/utils": "^0.3.
|
|
26
|
+
"@teamplay/backend": "^0.3.9",
|
|
27
|
+
"@teamplay/cache": "^0.3.9",
|
|
28
|
+
"@teamplay/channel": "^0.3.9",
|
|
29
|
+
"@teamplay/debug": "^0.3.9",
|
|
30
|
+
"@teamplay/schema": "^0.3.9",
|
|
31
|
+
"@teamplay/utils": "^0.3.9",
|
|
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": "7c53fc31b160822abc448b75e831409514f8945a"
|
|
67
67
|
}
|
package/react/helpers.js
CHANGED
|
@@ -22,6 +22,12 @@ export function pipeComponentMeta (SourceComponent, TargetComponent, suffix = ''
|
|
|
22
22
|
return TargetComponent
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function useNow () {
|
|
26
|
+
const context = useContext(ComponentMetaContext)
|
|
27
|
+
if (!context) throw Error(ERRORS.useNow)
|
|
28
|
+
return context.createdAt
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export function useId () {
|
|
26
32
|
const context = useContext(ComponentMetaContext)
|
|
27
33
|
if (!context) throw Error(ERRORS.useId)
|
|
@@ -63,5 +69,9 @@ const ERRORS = {
|
|
|
63
69
|
useId: `
|
|
64
70
|
useId() can only be used inside a component wrapped with observer().
|
|
65
71
|
You have probably forgot to wrap your component with observer().
|
|
72
|
+
`,
|
|
73
|
+
useNow: `
|
|
74
|
+
useNow() can only be used inside a component wrapped with observer().
|
|
75
|
+
You have probably forgot to wrap your component with observer().
|
|
66
76
|
`
|
|
67
77
|
}
|
package/react/useSub.js
CHANGED
|
@@ -8,70 +8,79 @@ let TEST_THROTTLING = false
|
|
|
8
8
|
// Currently it does lead to issues with extra rerenders and requires further investigation
|
|
9
9
|
let USE_DEFERRED_VALUE = false
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export function useAsyncSub (signal, params, options) {
|
|
12
|
+
return useSub(signal, params, { ...options, async: true })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function useSub (signal, params, options) {
|
|
12
16
|
if (USE_DEFERRED_VALUE) {
|
|
13
|
-
return useSubDeferred(signal, params) // eslint-disable-line react-hooks/rules-of-hooks
|
|
17
|
+
return useSubDeferred(signal, params, options) // eslint-disable-line react-hooks/rules-of-hooks
|
|
14
18
|
} else {
|
|
15
|
-
return useSubClassic(signal, params) // eslint-disable-line react-hooks/rules-of-hooks
|
|
19
|
+
return useSubClassic(signal, params, options) // eslint-disable-line react-hooks/rules-of-hooks
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
// version of sub() which works as a react hook and throws promise for Suspense
|
|
20
|
-
export function useSubDeferred (signal, params) {
|
|
24
|
+
export function useSubDeferred (signal, params, { async = false } = {}) {
|
|
25
|
+
const $signalRef = useRef() // eslint-disable-line react-hooks/rules-of-hooks
|
|
26
|
+
const scheduleUpdate = useScheduleUpdate()
|
|
21
27
|
signal = useDeferredValue(signal)
|
|
22
28
|
params = useDeferredValue(params ? JSON.stringify(params) : undefined)
|
|
23
29
|
params = params != null ? JSON.parse(params) : undefined
|
|
24
30
|
const promiseOrSignal = params != null ? sub(signal, params) : sub(signal)
|
|
25
31
|
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
26
32
|
if (promiseOrSignal.then) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
promiseOrSignal.then(resolve, reject)
|
|
32
|
-
}, TEST_THROTTLING)
|
|
33
|
-
})
|
|
33
|
+
const promise = maybeThrottle(promiseOrSignal)
|
|
34
|
+
if (async) {
|
|
35
|
+
scheduleUpdate(promise)
|
|
36
|
+
return
|
|
34
37
|
}
|
|
35
|
-
throw
|
|
36
|
-
}
|
|
38
|
+
throw promise
|
|
37
39
|
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
} else {
|
|
41
|
+
const $signal = promiseOrSignal
|
|
42
|
+
if ($signalRef.current !== $signal) $signalRef.current = $signal
|
|
43
|
+
return $signal
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
// classic version which initially throws promise for Suspense
|
|
44
48
|
// 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) {
|
|
49
|
+
export function useSubClassic (signal, params, { async = false } = {}) {
|
|
46
50
|
const $signalRef = useRef()
|
|
47
51
|
const activePromiseRef = useRef()
|
|
48
52
|
const scheduleUpdate = useScheduleUpdate()
|
|
49
53
|
const promiseOrSignal = params != null ? sub(signal, params) : sub(signal)
|
|
50
54
|
// 1. if it's a promise, throw it so that Suspense can catch it and wait for subscription to finish
|
|
51
55
|
if (promiseOrSignal.then) {
|
|
52
|
-
|
|
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
|
-
}
|
|
56
|
+
const promise = maybeThrottle(promiseOrSignal)
|
|
63
57
|
// first time we just throw the promise to be caught by Suspense
|
|
64
|
-
if (!$signalRef.current)
|
|
58
|
+
if (!$signalRef.current) {
|
|
59
|
+
// if we are in async mode, we just return nothing and let the user
|
|
60
|
+
// handle appearance of signal on their own.
|
|
61
|
+
// We manually schedule an update when promise resolves since we can't
|
|
62
|
+
// rely on Suspense in this case to automatically trigger component's re-render
|
|
63
|
+
if (async) {
|
|
64
|
+
scheduleUpdate(promise)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
// in regular mode we throw the promise to be caught by Suspense
|
|
68
|
+
// this way we guarantee that the signal with all the data
|
|
69
|
+
// will always be there when component is rendered
|
|
70
|
+
throw promise
|
|
71
|
+
}
|
|
65
72
|
// if we already have a previous signal, we return it and wait for new promise to resolve
|
|
66
73
|
scheduleUpdate(promise)
|
|
67
74
|
return $signalRef.current
|
|
68
|
-
}
|
|
69
75
|
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
$signalRef.current
|
|
76
|
+
} else {
|
|
77
|
+
const $signal = promiseOrSignal
|
|
78
|
+
if ($signalRef.current !== $signal) {
|
|
79
|
+
activePromiseRef.current = undefined
|
|
80
|
+
$signalRef.current = $signal
|
|
81
|
+
}
|
|
82
|
+
return $signal
|
|
73
83
|
}
|
|
74
|
-
return promiseOrSignal
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
export function setTestThrottling (ms) {
|
|
@@ -86,3 +95,13 @@ export function resetTestThrottling () {
|
|
|
86
95
|
export function setUseDeferredValue (value) {
|
|
87
96
|
USE_DEFERRED_VALUE = value
|
|
88
97
|
}
|
|
98
|
+
|
|
99
|
+
// throttle to simulate slow network
|
|
100
|
+
function maybeThrottle (promise) {
|
|
101
|
+
if (!TEST_THROTTLING) return promise
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
promise.then(resolve, reject)
|
|
105
|
+
}, TEST_THROTTLING)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -19,21 +19,21 @@ export default function wrapIntoSuspense ({
|
|
|
19
19
|
stateVersion: Symbol(), // eslint-disable-line symbol-description
|
|
20
20
|
onStoreChange: undefined,
|
|
21
21
|
scheduledUpdatePromise: undefined,
|
|
22
|
+
scheduleUpdate: promise => {
|
|
23
|
+
if (!promise?.then) throw Error('scheduleUpdate() expects a promise')
|
|
24
|
+
if (adm.scheduledUpdatePromise === promise) return
|
|
25
|
+
adm.scheduledUpdatePromise = promise
|
|
26
|
+
promise.then(() => {
|
|
27
|
+
if (adm.scheduledUpdatePromise !== promise) return
|
|
28
|
+
adm.scheduledUpdatePromise = undefined
|
|
29
|
+
adm.onStoreChange?.()
|
|
30
|
+
})
|
|
31
|
+
},
|
|
22
32
|
subscribe (onStoreChange) {
|
|
23
33
|
adm.onStoreChange = () => {
|
|
24
34
|
adm.stateVersion = Symbol() // eslint-disable-line symbol-description
|
|
25
35
|
onStoreChange()
|
|
26
36
|
}
|
|
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
37
|
return () => {
|
|
38
38
|
adm.onStoreChange = undefined
|
|
39
39
|
adm.scheduledUpdatePromise = undefined
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
export const REGISTRY_FINALIZE_AFTER = 10_000
|
|
2
1
|
export const REGISTRY_SWEEP_INTERVAL = 10_000
|
|
2
|
+
const PERMANENT = false
|
|
3
3
|
|
|
4
|
-
// This is a mock implementation of FinalizationRegistry
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// manually unregistered since we don't have a way to know when the object is
|
|
9
|
-
// no longer needed. In the future we might add the control logic to properly
|
|
10
|
-
// invalidate the objects.
|
|
11
|
-
export let PERMANENT = true
|
|
12
|
-
export function setPermanent (permanent) { PERMANENT = permanent }
|
|
13
|
-
|
|
14
|
-
export class TimerBasedFinalizationRegistry {
|
|
4
|
+
// This is a mock implementation of FinalizationRegistry which doesn't actually
|
|
5
|
+
// finalize anything. It's used in environments where FinalizationRegistry is not
|
|
6
|
+
// available and it can not be simulated using WeakRef (e.g. React Native <0.75 or Old Architecture).
|
|
7
|
+
export class PermanentFinalizationRegistry {
|
|
15
8
|
registrations = new Map()
|
|
16
9
|
sweepTimeout
|
|
17
10
|
|
|
@@ -25,7 +18,31 @@ export class TimerBasedFinalizationRegistry {
|
|
|
25
18
|
value,
|
|
26
19
|
registeredAt: Date.now()
|
|
27
20
|
})
|
|
28
|
-
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
unregister (token) {
|
|
24
|
+
this.registrations.delete(token)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// This is a mock implementation of FinalizationRegistry which uses WeakRef to
|
|
29
|
+
// track the target objects. It's used in environments where FinalizationRegistry
|
|
30
|
+
// is not available but WeakRef is (e.g. React Native >=0.75 on New Architecture).
|
|
31
|
+
export class WeakRefBasedFinalizationRegistry {
|
|
32
|
+
registrations = new Map()
|
|
33
|
+
sweepTimeout
|
|
34
|
+
|
|
35
|
+
constructor (finalize) {
|
|
36
|
+
this.finalize = finalize
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Token is actually required with this impl
|
|
40
|
+
register (target, value, token) {
|
|
41
|
+
this.registrations.set(token, {
|
|
42
|
+
targetRef: new WeakRef(target),
|
|
43
|
+
value
|
|
44
|
+
})
|
|
45
|
+
this.scheduleSweep()
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
unregister (token) {
|
|
@@ -33,34 +50,37 @@ export class TimerBasedFinalizationRegistry {
|
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
// Bound so it can be used directly as setTimeout callback.
|
|
36
|
-
sweep = (
|
|
37
|
-
// cancel timeout so we can force sweep anytime
|
|
53
|
+
sweep = () => {
|
|
38
54
|
clearTimeout(this.sweepTimeout)
|
|
39
55
|
this.sweepTimeout = undefined
|
|
40
56
|
|
|
41
|
-
const now = Date.now()
|
|
42
57
|
this.registrations.forEach((registration, token) => {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
58
|
+
if (registration.targetRef.deref() !== undefined) return
|
|
59
|
+
this.finalize(registration.value)
|
|
60
|
+
this.registrations.delete(token)
|
|
47
61
|
})
|
|
48
62
|
|
|
49
|
-
if (this.registrations.size > 0)
|
|
50
|
-
this.scheduleSweep()
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Bound so it can be exported directly as clearTimers test utility.
|
|
55
|
-
finalizeAllImmediately = () => {
|
|
56
|
-
this.sweep(0)
|
|
63
|
+
if (this.registrations.size > 0) this.scheduleSweep()
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
scheduleSweep () {
|
|
60
|
-
if (this.sweepTimeout
|
|
61
|
-
|
|
62
|
-
}
|
|
67
|
+
if (this.sweepTimeout) return
|
|
68
|
+
this.sweepTimeout = setTimeout(this.sweep, REGISTRY_SWEEP_INTERVAL)
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
let ExportedFinalizationRegistry
|
|
73
|
+
|
|
74
|
+
if (typeof FinalizationRegistry !== 'undefined') {
|
|
75
|
+
ExportedFinalizationRegistry = FinalizationRegistry
|
|
76
|
+
} else if (typeof WeakRef !== 'undefined' && !PERMANENT) {
|
|
77
|
+
console.warn('FinalizationRegistry is not available in this environment. ' +
|
|
78
|
+
'Using a mock implementation: WeakRefBasedFinalizationRegistry')
|
|
79
|
+
ExportedFinalizationRegistry = WeakRefBasedFinalizationRegistry
|
|
80
|
+
} else {
|
|
81
|
+
console.warn('FinalizationRegistry is not available in this environment. ' +
|
|
82
|
+
'Using a mock implementation: PermanentFinalizationRegistry')
|
|
83
|
+
ExportedFinalizationRegistry = PermanentFinalizationRegistry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default ExportedFinalizationRegistry
|
package/utils/MockWeakRef.js
CHANGED
|
@@ -13,4 +13,13 @@ export function destroyMockWeakRef (weakRef) {
|
|
|
13
13
|
weakRef.value = undefined
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
let ExportedWeakRef
|
|
17
|
+
|
|
18
|
+
if (typeof WeakRef !== 'undefined') {
|
|
19
|
+
ExportedWeakRef = WeakRef
|
|
20
|
+
} else {
|
|
21
|
+
console.warn('WeakRef is not available in this environment. Using a mock implementation: MockWeakRef')
|
|
22
|
+
ExportedWeakRef = MockWeakRef
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default ExportedWeakRef
|