teamplay 0.3.13 → 0.3.15

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 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]) => this.delete(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.13",
3
+ "version": "0.3.15",
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.13",
27
- "@teamplay/cache": "^0.3.13",
28
- "@teamplay/channel": "^0.3.13",
29
- "@teamplay/debug": "^0.3.13",
30
- "@teamplay/schema": "^0.3.13",
31
- "@teamplay/utils": "^0.3.13",
26
+ "@teamplay/backend": "^0.3.15",
27
+ "@teamplay/cache": "^0.3.15",
28
+ "@teamplay/channel": "^0.3.15",
29
+ "@teamplay/debug": "^0.3.15",
30
+ "@teamplay/schema": "^0.3.15",
31
+ "@teamplay/utils": "^0.3.15",
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": "c08d644eb32ef66112285de31a63201771a0eb97"
66
+ "gitHead": "108543261b228e76cfd4ceb4673b8190bb31bbb8"
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
  }
@@ -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 $signalRef = useRef()
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 (!$signalRef.current) {
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 $signalRef.current
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 ($signalRef.current !== $signal) {
80
+ if (cache.get(id) !== $signal) {
79
81
  activePromiseRef.current = undefined
80
- $signalRef.current = $signal
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