teamplay 0.4.0-alpha.9 → 0.4.0-alpha.91

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.
Files changed (47) hide show
  1. package/README.md +73 -0
  2. package/index.d.ts +4 -0
  3. package/index.js +14 -1
  4. package/orm/Aggregation.js +48 -13
  5. package/orm/Compat/README.md +299 -25
  6. package/orm/Compat/SignalCompat.js +638 -147
  7. package/orm/Compat/eventsCompat.js +4 -3
  8. package/orm/Compat/hooksCompat.js +108 -22
  9. package/orm/Compat/modelEvents.js +63 -31
  10. package/orm/Compat/queryReadiness.js +165 -0
  11. package/orm/Compat/refFallback.js +62 -0
  12. package/orm/Compat/refRegistry.js +44 -10
  13. package/orm/Compat/silentContext.js +51 -0
  14. package/orm/Compat/startStopCompat.js +168 -0
  15. package/orm/Doc.js +771 -61
  16. package/orm/Query.js +944 -119
  17. package/orm/Reaction.js +12 -9
  18. package/orm/Root.js +47 -0
  19. package/orm/SignalBase.js +173 -67
  20. package/orm/Value.js +7 -5
  21. package/orm/associations.js +97 -0
  22. package/orm/batchScheduler.js +62 -0
  23. package/orm/connection.js +17 -2
  24. package/orm/dataTree.js +331 -73
  25. package/orm/disposeRootContext.js +68 -0
  26. package/orm/getSignal.js +37 -20
  27. package/orm/idFields.js +33 -2
  28. package/orm/index.d.ts +6 -0
  29. package/orm/index.js +5 -0
  30. package/orm/missingDoc.js +3 -0
  31. package/orm/privateData.js +170 -0
  32. package/orm/rootContext.js +313 -0
  33. package/orm/rootScope.js +51 -0
  34. package/orm/sub.js +15 -6
  35. package/orm/subscriptionGcDelay.js +32 -0
  36. package/package.json +4 -2
  37. package/react/compatComponentRegistry.js +20 -0
  38. package/react/convertToObserver.js +19 -2
  39. package/react/helpers.js +4 -2
  40. package/react/promiseBatcher.js +115 -0
  41. package/react/renderAttemptDestroyer.js +38 -0
  42. package/react/trapRender.js +23 -15
  43. package/react/useSub.js +48 -3
  44. package/react/useSuspendMemo.js +96 -0
  45. package/react/wrapIntoSuspense.js +1 -1
  46. package/server.js +2 -2
  47. package/utils/setDiffDeep.js +32 -12
package/README.md CHANGED
@@ -19,6 +19,79 @@ Features:
19
19
 
20
20
  For installation and documentation see [teamplay.dev](https://teamplay.dev)
21
21
 
22
+ ## ORM Compat Helpers
23
+
24
+ For legacy Racer-style model mixins (for example versioning libraries which call
25
+ `getAssociations()`), use ORM compat helpers from the `teamplay/orm` subpath:
26
+
27
+ ```js
28
+ import BaseModel, { hasMany, hasOne, belongsTo } from 'teamplay/orm'
29
+ ```
30
+
31
+ These helpers attach class-level associations and expose them through
32
+ `$doc.getAssociations()` on model signals.
33
+
34
+ ## React Suspense Gates
35
+
36
+ If you need to throw a thenable from render, prefer `useSuspendMemo()` or
37
+ `useSuspendMemoByKey()` over `useMemo()`.
38
+
39
+ Why:
40
+
41
+ - React may restart a suspended initial render.
42
+ - `useMemo()` is not a semantic "run this suspend gate once" primitive.
43
+ - Side-effectful async work like `join()` may accidentally start again on retry.
44
+
45
+ ### `useSuspendMemo(factory, deps)`
46
+
47
+ Use it when the suspend gate is local to one observer component instance.
48
+
49
+ ```js
50
+ import { observer, useSuspendMemo } from 'teamplay'
51
+
52
+ const Component = observer(({ $stage, userId, stageUserStore }) => {
53
+ useSuspendMemo(() => {
54
+ if (!stageUserStore?.startedAt) {
55
+ throw $stage.join(userId)
56
+ }
57
+ }, [$stage.getId()])
58
+
59
+ return <span>Ready</span>
60
+ })
61
+ ```
62
+
63
+ This keeps the same pending thenable for the same hook slot while the component
64
+ instance is alive.
65
+
66
+ ### `useSuspendMemoByKey(key, factory, deps)`
67
+
68
+ Use it when the async operation must be deduped by business meaning, not just
69
+ by component instance.
70
+
71
+ ```js
72
+ import { observer, useSuspendMemoByKey } from 'teamplay'
73
+
74
+ const Component = observer(({ $stage, stageId, userId, stageUserStore }) => {
75
+ useSuspendMemoByKey(
76
+ `stage.join:${stageId}:${userId}`,
77
+ () => {
78
+ if (!stageUserStore?.startedAt) {
79
+ throw $stage.join(userId)
80
+ }
81
+ },
82
+ [stageId, userId, !!stageUserStore?.startedAt]
83
+ )
84
+
85
+ return <span>Ready</span>
86
+ })
87
+ ```
88
+
89
+ This is the right choice when:
90
+
91
+ - the component may remount while the promise is still pending;
92
+ - two different components may trigger the same async operation;
93
+ - the operation should behave like a single in-flight business task.
94
+
22
95
  ## License
23
96
 
24
97
  MIT
package/index.d.ts CHANGED
@@ -42,6 +42,8 @@ export {
42
42
  setUseDeferredValue as __setUseDeferredValue,
43
43
  setDefaultDefer as __setDefaultDefer
44
44
  } from './react/useSub.js'
45
+ export function useSuspendMemo<T = any> (factory: () => T, deps?: any[]): T
46
+ export function useSuspendMemoByKey<T = any> (key: any, factory: () => T, deps?: any[]): T
45
47
  export function useValue (defaultValue?: any): [any, any]
46
48
  export function useValue$ (defaultValue?: any): any
47
49
  export function useModel (path?: any): any
@@ -94,6 +96,8 @@ export function useDidUpdate (fn: () => EffectCleanup, deps?: any[]): void
94
96
  export function useOnce (condition: any, fn: () => EffectCleanup): void
95
97
  export function useSyncEffect (fn: () => EffectCleanup, deps?: any[]): void
96
98
  export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
99
+ export function getSubscriptionGcDelay (): number
100
+ export function setSubscriptionGcDelay (ms?: number | null): number
97
101
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
98
102
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
99
103
  export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
package/index.js CHANGED
@@ -23,6 +23,10 @@ export {
23
23
  setUseDeferredValue as __setUseDeferredValue,
24
24
  setDefaultDefer as __setDefaultDefer
25
25
  } from './react/useSub.js'
26
+ export {
27
+ default as useSuspendMemo,
28
+ useSuspendMemoByKey
29
+ } from './react/useSuspendMemo.js'
26
30
  export { default as observer } from './react/observer.js'
27
31
  export {
28
32
  useValue,
@@ -65,7 +69,16 @@ export {
65
69
  useOnce,
66
70
  useSyncEffect
67
71
  } from './react/helpers.js'
68
- export { connection, setConnection, getConnection, fetchOnly, setFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js'
72
+ export {
73
+ connection,
74
+ setConnection,
75
+ getConnection,
76
+ getDefaultFetchOnly,
77
+ setDefaultFetchOnly,
78
+ publicOnly,
79
+ setPublicOnly
80
+ } from './orm/connection.js'
81
+ export { getSubscriptionGcDelay, setSubscriptionGcDelay } from './orm/subscriptionGcDelay.js'
69
82
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js'
70
83
  export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
71
84
  export { aggregation, aggregationHeader as __aggregationHeader } from '@teamplay/utils/aggregation'
@@ -1,34 +1,54 @@
1
1
  import { raw } from '@nx-js/observer-util'
2
- import { set as _set, del as _del, getRaw } from './dataTree.js'
2
+ import { getRaw } from './dataTree.js'
3
3
  import getSignal from './getSignal.js'
4
- import { QuerySubscriptions, hashQuery, Query, HASH, PARAMS, COLLECTION_NAME, parseQueryHash } from './Query.js'
4
+ import {
5
+ QuerySubscriptions,
6
+ hashQuery,
7
+ Query,
8
+ HASH,
9
+ PARAMS,
10
+ COLLECTION_NAME,
11
+ parseQueryHash
12
+ } from './Query.js'
5
13
  import Signal, { SEGMENTS } from './Signal.js'
6
14
  import { getIdFieldsForSegments, isPlainObject } from './idFields.js'
15
+ import { delPrivateData, getPrivateData, setPrivateData } from './privateData.js'
7
16
 
8
17
  export const IS_AGGREGATION = Symbol('is aggregation signal')
9
18
  export const AGGREGATIONS = '$aggregations'
10
19
 
11
20
  class Aggregation extends Query {
12
21
  _initData () {
13
- {
14
- const extra = raw(this.shareQuery.extra)
15
- injectAggregationIds(extra, this.collectionName)
16
- _set([AGGREGATIONS, this.hash], extra)
17
- }
22
+ this._syncAllRootsData()
18
23
 
19
24
  this.shareQuery.on('extra', extra => {
20
25
  extra = raw(extra)
21
26
  injectAggregationIds(extra, this.collectionName)
22
- _set([AGGREGATIONS, this.hash], extra)
27
+ this._forEachRoot(rootId => {
28
+ setPrivateData(rootId, [AGGREGATIONS, this.hash], extra)
29
+ })
23
30
  })
24
31
  }
25
32
 
33
+ _syncRootData (rootId) {
34
+ if (!this.shareQuery) return
35
+ const extra = raw(this.shareQuery.extra)
36
+ injectAggregationIds(extra, this.collectionName)
37
+ setPrivateData(rootId, [AGGREGATIONS, this.hash], extra)
38
+ }
39
+
40
+ _removeRootData (rootId) {
41
+ delPrivateData(rootId, [AGGREGATIONS, this.hash])
42
+ }
43
+
26
44
  _removeData () {
27
- _del([AGGREGATIONS, this.hash])
45
+ this._forEachRoot(rootId => this._removeRootData(rootId))
46
+ this.rootIds.clear()
28
47
  }
29
48
  }
30
49
 
31
50
  export const aggregationSubscriptions = new QuerySubscriptions(Aggregation)
51
+ aggregationSubscriptions.runtimeKind = 'aggregation'
32
52
 
33
53
  function injectAggregationIds (extra, collectionName) {
34
54
  if (!Array.isArray(extra)) return
@@ -44,13 +64,14 @@ function injectAggregationIds (extra, collectionName) {
44
64
 
45
65
  export function getAggregationSignal (collectionName, params, options) {
46
66
  params = JSON.parse(JSON.stringify(params))
47
- const hash = hashQuery(collectionName, params)
67
+ const transportHash = hashQuery(collectionName, params)
68
+ const { root, signalOptions } = parseAggregationSignalOptions(options)
48
69
 
49
- const $aggregation = getSignal(undefined, [AGGREGATIONS, hash], options)
70
+ const $aggregation = getSignal(root, [AGGREGATIONS, transportHash], signalOptions)
50
71
  $aggregation[IS_AGGREGATION] ??= true
51
72
  $aggregation[COLLECTION_NAME] ??= collectionName
52
73
  $aggregation[PARAMS] ??= params
53
- $aggregation[HASH] ??= hash
74
+ $aggregation[HASH] ??= transportHash
54
75
  return $aggregation
55
76
  }
56
77
 
@@ -65,10 +86,13 @@ export function isAggregationSignal ($signal) {
65
86
 
66
87
  // example: ['$aggregations', '{"active":true}', 42]
67
88
  // AND only if it also has either '_id' or 'id' field inside
68
- export function getAggregationDocId (segments, method = getRaw) {
89
+ export function getAggregationDocId (segments, rootId, method) {
69
90
  if (!(segments.length >= 3)) return
70
91
  if (!(segments[0] === AGGREGATIONS)) return
71
92
  if (!(typeof segments[2] === 'number')) return
93
+ if (typeof method !== 'function') {
94
+ method = path => rootId == null ? getRaw(path) : getPrivateData(rootId, path)
95
+ }
72
96
  const docId = method([...segments.slice(0, 3), '_id']) || method([...segments.slice(0, 3), 'id'])
73
97
  return docId
74
98
  }
@@ -80,3 +104,14 @@ export function getAggregationCollectionName (segments) {
80
104
  const { collectionName } = parseQueryHash(hash)
81
105
  return collectionName
82
106
  }
107
+
108
+ function parseAggregationSignalOptions (options) {
109
+ if (!options || typeof options !== 'object') {
110
+ return {
111
+ root: undefined,
112
+ signalOptions: {}
113
+ }
114
+ }
115
+ const { root, ...signalOptions } = options
116
+ return { root, signalOptions }
117
+ }