teamplay 0.4.0-alpha.94 → 0.4.0-alpha.96

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.
@@ -1,10 +1,12 @@
1
1
  import { getRaw } from '../dataTree.js'
2
2
  import { getConnection } from '../connection.js'
3
3
  import { isMissingShareDoc } from '../missingDoc.js'
4
- import { QUERIES, HASH, PARAMS, COLLECTION_NAME } from '../Query.js'
5
- import { AGGREGATIONS, IS_AGGREGATION } from '../Aggregation.js'
4
+ import { QUERIES, HASH, PARAMS, COLLECTION_NAME, querySubscriptions } from '../Query.js'
5
+ import { AGGREGATIONS, IS_AGGREGATION, aggregationSubscriptions } from '../Aggregation.js'
6
6
  import { getPrivateData, setPrivateData } from '../privateData.js'
7
7
  import { getRoot, ROOT_ID } from '../Root.js'
8
+ import { isRootContextClosed } from '../rootContext.js'
9
+ import { getScopedSignalHash, normalizeRootId } from '../rootScope.js'
8
10
 
9
11
  let imperativeQueryReadyTimeoutMs = 1000
10
12
 
@@ -47,7 +49,9 @@ export function isDocReady (segments) {
47
49
  export async function waitForImperativeQueryReady ($query) {
48
50
  const timeoutMs = imperativeQueryReadyTimeoutMs
49
51
  const startedAt = Date.now()
52
+ const ownerState = createImperativeOwnerState($query)
50
53
  while (true) {
54
+ if (isImperativeQueryCancelled($query, ownerState)) return
51
55
  if (isImperativeQueryReady($query)) {
52
56
  syncQueryDocsFromCollection($query)
53
57
  return
@@ -92,6 +96,28 @@ function isImperativeQueryReady ($query) {
92
96
  return true
93
97
  }
94
98
 
99
+ function isImperativeQueryCancelled ($query, ownerState) {
100
+ const rootId = getRoot($query)?.[ROOT_ID]
101
+ if (isRootContextClosed(rootId)) return true
102
+ if (!ownerState?.wasTracked) return false
103
+ const trackedOwnerCount = ownerState.subscriptions.getTrackedOwnerCount(ownerState.ownerKey)
104
+ return trackedOwnerCount == null || trackedOwnerCount <= 0
105
+ }
106
+
107
+ function createImperativeOwnerState ($query) {
108
+ const hash = $query[HASH]
109
+ const rootId = normalizeRootId(getRoot($query)?.[ROOT_ID])
110
+ const subscriptions = ($query[IS_AGGREGATION] || isAggregationQuery($query[PARAMS]))
111
+ ? aggregationSubscriptions
112
+ : querySubscriptions
113
+ const ownerKey = getScopedSignalHash(rootId, hash, 'queryOwner')
114
+ return {
115
+ subscriptions,
116
+ ownerKey,
117
+ wasTracked: subscriptions.getTrackedOwnerCount(ownerKey) != null
118
+ }
119
+ }
120
+
95
121
  function syncQueryDocsFromCollection ($query) {
96
122
  const params = $query[PARAMS]
97
123
  if ($query[IS_AGGREGATION] || isAggregationQuery(params) || isExtraQuery(params)) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.94",
3
+ "version": "0.4.0-alpha.96",
4
4
  "description": "Full-stack signals ORM with multiplayer",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -83,5 +83,5 @@
83
83
  ]
84
84
  },
85
85
  "license": "MIT",
86
- "gitHead": "28a9de4a846b5f36c31033c1c3486074cf9b86fc"
86
+ "gitHead": "78f33dc002740b425f3a056fad5b7e780a34219c"
87
87
  }
@@ -1,37 +1,46 @@
1
1
  class RenderAttemptDestroyer {
2
2
  constructor () {
3
3
  this.fns = []
4
- this.compatArmed = false
4
+ this.compatAttemptCleanupArmed = false
5
+ this.suspenseGateArmed = false
5
6
  }
6
7
 
7
8
  add (fn, { compat = false } = {}) {
8
9
  if (typeof fn !== 'function') return
9
10
  this.fns.push(fn)
10
- if (compat) this.compatArmed = true
11
+ if (compat) this.compatAttemptCleanupArmed = true
11
12
  }
12
13
 
13
- armCompat () {
14
- this.compatArmed = true
14
+ armCompatAttemptCleanup () {
15
+ this.compatAttemptCleanupArmed = true
15
16
  }
16
17
 
17
- getDestructor () {
18
- if (!this.compatArmed) {
19
- this.reset()
20
- return undefined
21
- }
18
+ armSuspenseGate () {
19
+ this.suspenseGateArmed = true
20
+ }
22
21
 
23
- const fns = [...this.fns]
22
+ consumeThenableHandling () {
23
+ const shouldRunAttemptCleanup = this.compatAttemptCleanupArmed && this.fns.length > 0
24
+ const shouldKeepShellAlive = this.suspenseGateArmed || shouldRunAttemptCleanup
25
+ let destroyAttempt
26
+ if (shouldRunAttemptCleanup) {
27
+ const fns = [...this.fns]
28
+ destroyAttempt = async () => {
29
+ await Promise.allSettled(fns.map(fn => fn()))
30
+ fns.length = 0
31
+ }
32
+ }
24
33
  this.reset()
25
- return async () => {
26
- if (fns.length === 0) return
27
- await Promise.allSettled(fns.map(fn => fn()))
28
- fns.length = 0
34
+ return {
35
+ shouldKeepShellAlive,
36
+ destroyAttempt
29
37
  }
30
38
  }
31
39
 
32
40
  reset () {
33
41
  this.fns.length = 0
34
- this.compatArmed = false
42
+ this.compatAttemptCleanupArmed = false
43
+ this.suspenseGateArmed = false
35
44
  }
36
45
  }
37
46
 
@@ -24,8 +24,11 @@ export default function trapRender ({ render, cache, destroy, componentId }) {
24
24
  destroyed = true
25
25
  throw err
26
26
  }
27
- const destroyAttempt = renderAttemptDestroyer.getDestructor()
28
- if (destroyAttempt) {
27
+ const {
28
+ shouldKeepShellAlive,
29
+ destroyAttempt
30
+ } = renderAttemptDestroyer.consumeThenableHandling()
31
+ if (shouldKeepShellAlive) {
29
32
  throw Promise.resolve(err).then(() => destroyAttempt?.())
30
33
  }
31
34
 
package/react/useSub.js CHANGED
@@ -158,6 +158,7 @@ function maybeThrottle (promise) {
158
158
  function registerCompatAttemptCleanup (signal, params) {
159
159
  // Compat hooks don't build per-hook init objects like Racer.
160
160
  // We still need a marker so trapRender can defer observer-shell cleanup
161
- // to Suspense resolution instead of tearing the whole shell down immediately.
162
- renderAttemptDestroyer.armCompat()
161
+ // only when a real attempt cleanup exists.
162
+ // This path must not arm suspense-gate keep-alive by itself.
163
+ renderAttemptDestroyer.armCompatAttemptCleanup()
163
164
  }
@@ -28,7 +28,7 @@ export default function useSuspendMemo (factory, deps) {
28
28
  if (entry.status === 'done') return entry.value
29
29
  if (entry.status === 'pending') {
30
30
  markCompatComponent(componentId)
31
- renderAttemptDestroyer.armCompat()
31
+ renderAttemptDestroyer.armSuspenseGate()
32
32
  throw entry.promise
33
33
  }
34
34
 
@@ -47,7 +47,7 @@ export default function useSuspendMemo (factory, deps) {
47
47
  entry.status = 'pending'
48
48
  entry.promise = promise
49
49
  markCompatComponent(componentId)
50
- renderAttemptDestroyer.armCompat()
50
+ renderAttemptDestroyer.armSuspenseGate()
51
51
  throw promise
52
52
  }
53
53
  }