teamplay 0.3.13 → 0.3.14
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/orm/Cache.js +14 -1
- package/orm/getSignal.js +1 -1
- package/package.json +9 -9
- package/react/convertToObserver.js +1 -1
- package/react/helpers.js +10 -0
- package/react/universal$.js +3 -3
- package/react/useSub.js +8 -6
- package/react/wrapIntoSuspense.js +19 -6
package/orm/Cache.js
CHANGED
|
@@ -4,7 +4,19 @@ import WeakRef, { destroyMockWeakRef } from '../utils/MockWeakRef.js'
|
|
|
4
4
|
export default class Cache {
|
|
5
5
|
constructor () {
|
|
6
6
|
this.cache = new Map()
|
|
7
|
-
this.fr = new FinalizationRegistry(([key]) =>
|
|
7
|
+
this.fr = new FinalizationRegistry(([key]) => {
|
|
8
|
+
// handle situation when FinalizationRegistry triggers
|
|
9
|
+
// way later after the WeakRef is already garbage collected.
|
|
10
|
+
// In this case we might already have a new value in the cache
|
|
11
|
+
// and we don't want to delete it.
|
|
12
|
+
if (this.get(key)) return
|
|
13
|
+
this.delete(key)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// for testing purposes
|
|
18
|
+
_getKeys () {
|
|
19
|
+
return Array.from(this.cache.keys()).sort()
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
get (key) {
|
|
@@ -18,6 +30,7 @@ export default class Cache {
|
|
|
18
30
|
* to hold strong references to them until the value is garbage collected
|
|
19
31
|
*/
|
|
20
32
|
set (key, value, inputs = []) {
|
|
33
|
+
if (typeof key !== 'string') throw Error('Cache key should be a string')
|
|
21
34
|
this.cache.set(key, new WeakRef(value))
|
|
22
35
|
this.fr.register(value, [key, ...inputs])
|
|
23
36
|
}
|
package/orm/getSignal.js
CHANGED
|
@@ -59,7 +59,7 @@ export default function getSignal ($root, segments = [], {
|
|
|
59
59
|
if (segments.length > 2) {
|
|
60
60
|
if (segments[0] === LOCAL) {
|
|
61
61
|
dependencies.push(getSignal($root, segments.slice(0, 2)))
|
|
62
|
-
} else if (isPublicCollection(segments[0])) {
|
|
62
|
+
} else if (isPublicCollection(segments[0]) || segments[0] === QUERIES || segments[0] === AGGREGATIONS) {
|
|
63
63
|
dependencies.push(getSignal(undefined, segments.slice(0, 2)))
|
|
64
64
|
}
|
|
65
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"test": "npm run test-server && npm run test-client",
|
|
20
20
|
"test-server": "node --expose-gc -r ./test/_init.cjs --test",
|
|
21
21
|
"test-server-only": "node --expose-gc -r ./test/_init.cjs --test --test-only",
|
|
22
|
-
"test-client": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
|
|
22
|
+
"test-client": "NODE_OPTIONS=\"$NODE_OPTIONS --expose-gc --experimental-vm-modules\" jest"
|
|
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.14",
|
|
27
|
+
"@teamplay/cache": "^0.3.14",
|
|
28
|
+
"@teamplay/channel": "^0.3.14",
|
|
29
|
+
"@teamplay/debug": "^0.3.14",
|
|
30
|
+
"@teamplay/schema": "^0.3.14",
|
|
31
|
+
"@teamplay/utils": "^0.3.14",
|
|
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": "798d2a34fcb67bf255a2b3c126a5fb052243a866"
|
|
67
67
|
}
|
|
@@ -40,9 +40,9 @@ export default function convertToObserver (BaseComponent, {
|
|
|
40
40
|
if (throttle) update = _throttle(update, throttle)
|
|
41
41
|
destroyRef.current = (where) => {
|
|
42
42
|
if (!reactionRef.current) throw Error(`NO REACTION REF - ${where}`)
|
|
43
|
+
destroyRef.current = undefined
|
|
43
44
|
unobserve(reactionRef.current)
|
|
44
45
|
reactionRef.current = undefined
|
|
45
|
-
destroyRef.current = undefined
|
|
46
46
|
destroyCache(where)
|
|
47
47
|
}
|
|
48
48
|
const trappedRender = trapRender({
|
package/react/helpers.js
CHANGED
|
@@ -46,6 +46,12 @@ export function useScheduleUpdate () {
|
|
|
46
46
|
return context.scheduleUpdate
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function useCache (key) {
|
|
50
|
+
const context = useContext(ComponentMetaContext)
|
|
51
|
+
if (!context) throw Error(ERRORS.useCache)
|
|
52
|
+
return context.cache
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
export function useUnmount (fn) {
|
|
50
56
|
const fnRef = useRef()
|
|
51
57
|
if (fnRef.current !== fn) fnRef.current = fn
|
|
@@ -73,5 +79,9 @@ const ERRORS = {
|
|
|
73
79
|
useNow: `
|
|
74
80
|
useNow() can only be used inside a component wrapped with observer().
|
|
75
81
|
You have probably forgot to wrap your component with observer().
|
|
82
|
+
`,
|
|
83
|
+
useCache: `
|
|
84
|
+
useCache() can only be used inside a component wrapped with observer().
|
|
85
|
+
You have probably forgot to wrap your component with observer().
|
|
76
86
|
`
|
|
77
87
|
}
|
package/react/universal$.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
2
1
|
import $ from '../orm/$.js'
|
|
2
|
+
import { useCache } from './helpers.js'
|
|
3
3
|
import executionContextTracker from './executionContextTracker.js'
|
|
4
4
|
|
|
5
5
|
// universal versions of $() which work as a plain function or as a react hook
|
|
@@ -7,10 +7,10 @@ export default function universal$ ($root, value) {
|
|
|
7
7
|
if (executionContextTracker.isActive()) {
|
|
8
8
|
// within react component
|
|
9
9
|
const id = executionContextTracker.newHookId()
|
|
10
|
+
const cache = useCache() // eslint-disable-line react-hooks/rules-of-hooks
|
|
10
11
|
const $signal = $($root, value, id)
|
|
12
|
+
cache.set(id, $signal)
|
|
11
13
|
// save signal into ref to make sure it's not garbage collected while component exists
|
|
12
|
-
const $signalRef = useRef() // eslint-disable-line react-hooks/rules-of-hooks
|
|
13
|
-
if ($signalRef.current !== $signal) $signalRef.current = $signal
|
|
14
14
|
return $signal
|
|
15
15
|
} else {
|
|
16
16
|
return $($root, value)
|
package/react/useSub.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useDeferredValue } from 'react'
|
|
2
2
|
import sub from '../orm/sub.js'
|
|
3
|
-
import { useScheduleUpdate } from './helpers.js'
|
|
3
|
+
import { useScheduleUpdate, useCache } from './helpers.js'
|
|
4
|
+
import executionContextTracker from './executionContextTracker.js'
|
|
4
5
|
|
|
5
6
|
let TEST_THROTTLING = false
|
|
6
7
|
|
|
@@ -47,7 +48,8 @@ export function useSubDeferred (signal, params, { async = false } = {}) {
|
|
|
47
48
|
// classic version which initially throws promise for Suspense
|
|
48
49
|
// but if we get a promise second time, we return the last signal and wait for promise to resolve
|
|
49
50
|
export function useSubClassic (signal, params, { async = false } = {}) {
|
|
50
|
-
const
|
|
51
|
+
const id = executionContextTracker.newHookId()
|
|
52
|
+
const cache = useCache()
|
|
51
53
|
const activePromiseRef = useRef()
|
|
52
54
|
const scheduleUpdate = useScheduleUpdate()
|
|
53
55
|
const promiseOrSignal = params != null ? sub(signal, params) : sub(signal)
|
|
@@ -55,7 +57,7 @@ export function useSubClassic (signal, params, { async = false } = {}) {
|
|
|
55
57
|
if (promiseOrSignal.then) {
|
|
56
58
|
const promise = maybeThrottle(promiseOrSignal)
|
|
57
59
|
// first time we just throw the promise to be caught by Suspense
|
|
58
|
-
if (
|
|
60
|
+
if (!cache.has(id)) {
|
|
59
61
|
// if we are in async mode, we just return nothing and let the user
|
|
60
62
|
// handle appearance of signal on their own.
|
|
61
63
|
// We manually schedule an update when promise resolves since we can't
|
|
@@ -71,13 +73,13 @@ export function useSubClassic (signal, params, { async = false } = {}) {
|
|
|
71
73
|
}
|
|
72
74
|
// if we already have a previous signal, we return it and wait for new promise to resolve
|
|
73
75
|
scheduleUpdate(promise)
|
|
74
|
-
return
|
|
76
|
+
return cache.get(id)
|
|
75
77
|
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
76
78
|
} else {
|
|
77
79
|
const $signal = promiseOrSignal
|
|
78
|
-
if (
|
|
80
|
+
if (cache.get(id) !== $signal) {
|
|
79
81
|
activePromiseRef.current = undefined
|
|
80
|
-
|
|
82
|
+
cache.set(id, $signal)
|
|
81
83
|
}
|
|
82
84
|
return $signal
|
|
83
85
|
}
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
import { useSyncExternalStore, forwardRef as _forwardRef, memo, createElement as el, Suspense, useId, useRef } from 'react'
|
|
4
4
|
import { pipeComponentMeta, pipeComponentDisplayName, ComponentMetaContext } from './helpers.js'
|
|
5
5
|
|
|
6
|
+
// TODO: probably add FinalizationRegistry to handle destruction of observer() before it ever mounted.
|
|
7
|
+
// In such case we might have a memory leak because subscribe() would never fire and would never
|
|
8
|
+
// clean up the cache
|
|
9
|
+
function destroyAdm (adm) {
|
|
10
|
+
adm.onStoreChange = undefined
|
|
11
|
+
adm.scheduledUpdatePromise = undefined
|
|
12
|
+
adm.scheduleUpdate = undefined
|
|
13
|
+
adm.cache.clear()
|
|
14
|
+
adm.cache = undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
export default function wrapIntoSuspense ({
|
|
7
18
|
Component,
|
|
8
19
|
forwardRef,
|
|
@@ -19,6 +30,7 @@ export default function wrapIntoSuspense ({
|
|
|
19
30
|
stateVersion: Symbol(), // eslint-disable-line symbol-description
|
|
20
31
|
onStoreChange: undefined,
|
|
21
32
|
scheduledUpdatePromise: undefined,
|
|
33
|
+
cache: new Map(),
|
|
22
34
|
scheduleUpdate: promise => {
|
|
23
35
|
if (!promise?.then) throw Error('scheduleUpdate() expects a promise')
|
|
24
36
|
if (adm.scheduledUpdatePromise === promise) return
|
|
@@ -34,11 +46,7 @@ export default function wrapIntoSuspense ({
|
|
|
34
46
|
adm.stateVersion = Symbol() // eslint-disable-line symbol-description
|
|
35
47
|
onStoreChange()
|
|
36
48
|
}
|
|
37
|
-
return () =>
|
|
38
|
-
adm.onStoreChange = undefined
|
|
39
|
-
adm.scheduledUpdatePromise = undefined
|
|
40
|
-
adm.scheduleUpdate = undefined
|
|
41
|
-
}
|
|
49
|
+
return () => destroyAdm(adm)
|
|
42
50
|
},
|
|
43
51
|
getSnapshot () {
|
|
44
52
|
return adm.stateVersion
|
|
@@ -55,7 +63,12 @@ export default function wrapIntoSuspense ({
|
|
|
55
63
|
componentId,
|
|
56
64
|
createdAt: Date.now(),
|
|
57
65
|
triggerUpdate: () => adm.onStoreChange?.(),
|
|
58
|
-
scheduleUpdate: promise => adm.scheduleUpdate?.(promise)
|
|
66
|
+
scheduleUpdate: promise => adm.scheduleUpdate?.(promise),
|
|
67
|
+
cache: {
|
|
68
|
+
get: key => adm.cache?.get(key),
|
|
69
|
+
set: (key, value) => adm.cache?.set(key, value),
|
|
70
|
+
has: key => adm.cache?.has(key)
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
73
|
}
|
|
61
74
|
|