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 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.7",
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.7",
27
- "@teamplay/cache": "^0.3.7",
28
- "@teamplay/channel": "^0.3.7",
29
- "@teamplay/debug": "^0.3.7",
30
- "@teamplay/schema": "^0.3.7",
31
- "@teamplay/utils": "^0.3.7",
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": "c5840863c3afd56432f923700f7cd04333168469"
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 default function useSub (signal, params) {
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
- if (TEST_THROTTLING) {
28
- // simulate slow network
29
- throw new Promise((resolve, reject) => {
30
- setTimeout(() => {
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 promiseOrSignal
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
- const $signalRef = useRef() // eslint-disable-line react-hooks/rules-of-hooks
39
- if ($signalRef.current !== promiseOrSignal) $signalRef.current = promiseOrSignal
40
- return promiseOrSignal
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
- 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
- }
56
+ const promise = maybeThrottle(promiseOrSignal)
63
57
  // first time we just throw the promise to be caught by Suspense
64
- if (!$signalRef.current) throw promise
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
- if ($signalRef.current !== promiseOrSignal) {
71
- activePromiseRef.current = undefined
72
- $signalRef.current = promiseOrSignal
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 that uses setTimeout to
5
- // schedule the sweep of outdated objects.
6
- // It is used in environments where FinalizationRegistry is not available.
7
- // For now we permanently keep the values in the registry until they are
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
- if (!PERMANENT) this.scheduleSweep()
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 = (maxAge = REGISTRY_FINALIZE_AFTER) => {
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 (now - registration.registeredAt >= maxAge) {
44
- this.finalize(registration.value)
45
- this.registrations.delete(token)
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 === undefined) {
61
- this.sweepTimeout = setTimeout(this.sweep, REGISTRY_SWEEP_INTERVAL)
62
- }
67
+ if (this.sweepTimeout) return
68
+ this.sweepTimeout = setTimeout(this.sweep, REGISTRY_SWEEP_INTERVAL)
63
69
  }
64
70
  }
65
71
 
66
- export default (typeof FinalizationRegistry !== 'undefined' ? FinalizationRegistry : TimerBasedFinalizationRegistry)
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
@@ -13,4 +13,13 @@ export function destroyMockWeakRef (weakRef) {
13
13
  weakRef.value = undefined
14
14
  }
15
15
 
16
- export default (typeof WeakRef !== 'undefined' ? WeakRef : MockWeakRef)
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