teamplay 0.4.0-alpha.71 → 0.4.0-alpha.72

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.
@@ -169,10 +169,39 @@ $alias.get() // { name: 'Bob' }
169
169
  $alias.get() === $user.get() // false
170
170
  ```
171
171
 
172
+ ### `ref` on query/aggregation targets (`mirror-only`)
173
+
174
+ Compat supports `refExtra` / `refIds` and query/aggregation-backed refs, but with a
175
+ different contract from plain document refs.
176
+
177
+ When target is a query or aggregation signal, compat creates a **mirror-only** link:
178
+ - Source path is updated from target changes (target -> source).
179
+ - Source path does **not** become an alias to target path (no `REF_TARGET` forwarding).
180
+ - Writes to source path do not forward to query/aggregation internals.
181
+
182
+ Why:
183
+ - Query/aggregation paths are hashed/synthetic and are not safe as generic alias targets.
184
+ - Racer behavior for these cases is effectively "mirror data into page/local path",
185
+ not "make full bidirectional alias".
186
+
187
+ Reactivity:
188
+ - Initial sync runs immediately on `ref(...)`.
189
+ - Further target updates are mirrored through compat model-change events.
190
+
191
+ ```js
192
+ const $query = $.query('courses', { active: true })
193
+ const $table = $._page.tables._adminCourses
194
+
195
+ // mirror query.extra/docs into page model
196
+ $query.refExtra('_page.tables._adminCourses.dataSource')
197
+
198
+ // reactively mirrors target -> source
199
+ $table.dataSource.get()
200
+ ```
201
+
172
202
  **Limitations vs Racer**
173
- - No `refList`, `refExtra`, `refMap`.
203
+ - No `refList`, `refMap`.
174
204
  - No automatic list index patching on insert/remove/move.
175
- - No support for query/aggregation refs.
176
205
  - No event emissions specific to refs.
177
206
  - No support for racer-style ref meta/options beyond the basic signature.
178
207
 
@@ -1,4 +1,4 @@
1
- import { raw } from '@nx-js/observer-util'
1
+ import { raw, observe, unobserve } from '@nx-js/observer-util'
2
2
  import {
3
3
  Signal,
4
4
  GETTERS,
@@ -601,10 +601,19 @@ class SignalCompat extends Signal {
601
601
  const fromPath = $from.path()
602
602
  const existing = store.get(fromPath)
603
603
  if (existing) existing.stop()
604
- const stop = createRefLink($from, $to, options)
604
+ const mirrorOnly = !!($to?.[IS_QUERY] || $to?.[IS_AGGREGATION])
605
+ const { stop, onChange } = createRefLink($from, $to, { mirrorOnly, options })
605
606
  store.set(fromPath, { stop })
606
- $from[REF_TARGET] = $to
607
- setRefLink(fromPath, $to.path())
607
+ if (!mirrorOnly) {
608
+ $from[REF_TARGET] = $to
609
+ setRefLink(fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], { mirrorOnly: false })
610
+ } else {
611
+ setRefLink(fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], {
612
+ mirrorOnly: true,
613
+ onChange
614
+ })
615
+ if ($from[REF_TARGET]) delete $from[REF_TARGET]
616
+ }
608
617
  return $from
609
618
  }
610
619
 
@@ -715,23 +724,55 @@ function createSilentSignalWrapper ($signal, enabled = true) {
715
724
  }
716
725
 
717
726
  const REFS = Symbol('compat refs')
718
- const SKIP_REF_TICK = Symbol('compat ref skip tick')
719
-
720
727
  function getRefStore ($signal) {
721
728
  const $root = getRoot($signal) || $signal
722
729
  $root[REFS] ??= new Map()
723
730
  return $root[REFS]
724
731
  }
725
732
 
726
- function createRefLink ($from, $to) {
733
+ function createRefLink ($from, $to, { mirrorOnly = false } = {}) {
734
+ let disposed = false
735
+ let pendingRead = null
736
+ let mirrorObserver
737
+
727
738
  const syncFromTarget = () => {
728
739
  const value = readRefValue($to)
729
- if (value === SKIP_REF_TICK) return
740
+ if (isThenable(value)) {
741
+ pendingRead = value
742
+ value.then(() => {
743
+ if (disposed || pendingRead !== value) return
744
+ pendingRead = null
745
+ syncFromTarget()
746
+ }, () => {
747
+ if (pendingRead === value) pendingRead = null
748
+ })
749
+ return
750
+ }
751
+ if (value === undefined) return
730
752
  setDiffDeepBypassRef($from, deepCopy(value))
731
753
  }
754
+
732
755
  syncFromTarget()
733
- return () => {
734
- // Subsequent sync happens directly at mutation time via mirrorRefMutationFromTarget().
756
+ if (mirrorOnly) {
757
+ mirrorObserver = observe(
758
+ () => {
759
+ syncFromTarget()
760
+ return readRefValue($to)
761
+ },
762
+ {
763
+ scheduler: job => job()
764
+ }
765
+ )
766
+ // initialize dependency graph
767
+ mirrorObserver()
768
+ }
769
+ return {
770
+ onChange: syncFromTarget,
771
+ stop: () => {
772
+ disposed = true
773
+ if (mirrorObserver) unobserve(mirrorObserver)
774
+ // Subsequent sync happens directly at mutation time via mirrorRefMutationFromTarget().
775
+ }
735
776
  }
736
777
  }
737
778
 
@@ -739,7 +780,7 @@ function readRefValue ($signal) {
739
780
  try {
740
781
  return $signal.get()
741
782
  } catch (err) {
742
- if (isThenable(err)) return SKIP_REF_TICK
783
+ if (isThenable(err)) return err
743
784
  throw err
744
785
  }
745
786
  }
@@ -66,6 +66,9 @@ export function emitModelChange (path, value, prevValue, meta) {
66
66
 
67
67
  for (const link of getRefLinks().values()) {
68
68
  if (!isPathPrefix(link.toSegments, segments)) continue
69
+ if (link.mirrorOnly && typeof link.onChange === 'function') {
70
+ link.onChange()
71
+ }
69
72
  const suffix = segments.slice(link.toSegments.length)
70
73
  const nextSegments = link.fromSegments.concat(suffix)
71
74
  const nextKey = nextSegments.join('.')
@@ -39,6 +39,7 @@ export function resolveRefSegmentsSafe (segments, maxDepth = 32) {
39
39
  function findBestMatchingLink (segments) {
40
40
  let best
41
41
  for (const link of getRefLinks().values()) {
42
+ if (link.mirrorOnly) continue
42
43
  if (!isPathPrefix(link.fromSegments, segments)) continue
43
44
  if (!best || link.fromSegments.length > best.fromSegments.length) {
44
45
  best = link
@@ -1,12 +1,20 @@
1
1
  const refLinks = new Map()
2
2
 
3
- export function setRefLink (fromPath, toPath) {
3
+ export function setRefLink (fromPath, toPath, fromSegments, toSegments, options = {}) {
4
4
  if (typeof fromPath !== 'string' || typeof toPath !== 'string') return
5
+ const normalizedFromSegments = Array.isArray(fromSegments)
6
+ ? fromSegments.map(segment => String(segment))
7
+ : splitPath(fromPath)
8
+ const normalizedToSegments = Array.isArray(toSegments)
9
+ ? toSegments.map(segment => String(segment))
10
+ : splitPath(toPath)
5
11
  refLinks.set(fromPath, {
6
12
  fromPath,
7
13
  toPath,
8
- fromSegments: splitPath(fromPath),
9
- toSegments: splitPath(toPath)
14
+ fromSegments: normalizedFromSegments,
15
+ toSegments: normalizedToSegments,
16
+ mirrorOnly: !!options.mirrorOnly,
17
+ onChange: typeof options.onChange === 'function' ? options.onChange : undefined
10
18
  })
11
19
  }
12
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.4.0-alpha.71",
3
+ "version": "0.4.0-alpha.72",
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": "b110c0b271cc44ba3285f2e1ced0ead886026813"
86
+ "gitHead": "204b5dd49e950162c6930bb9fcfbceeea51382ec"
87
87
  }