teamplay 0.4.0-alpha.80 → 0.4.0-alpha.81

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,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
+ }
@@ -8,23 +8,14 @@ import {
8
8
  isPublicCollectionSignal,
9
9
  isPublicDocumentSignal
10
10
  } from '../SignalBase.js'
11
- import { getRoot, ROOT, getRootSignal, GLOBAL_ROOT_ID } from '../Root.js'
11
+ import { getRoot, ROOT, ROOT_ID, getRootSignal, GLOBAL_ROOT_ID, unregisterRootFinalizer } from '../Root.js'
12
12
  import { publicOnly, fetchOnly, setFetchOnly } from '../connection.js'
13
13
  import { docSubscriptions } from '../Doc.js'
14
14
  import { IS_QUERY, getQuerySignal, querySubscriptions } from '../Query.js'
15
15
  import { IS_AGGREGATION, aggregationSubscriptions, getAggregationSignal } from '../Aggregation.js'
16
16
  import { getIdFieldsForSegments, isIdFieldPath, isPublicDocPath, normalizeIdFields, isPlainObject } from '../idFields.js'
17
17
  import {
18
- del as _del,
19
- setReplace as _setReplace,
20
18
  incrementPublic as _incrementPublic,
21
- arrayPush as _arrayPush,
22
- arrayUnshift as _arrayUnshift,
23
- arrayInsert as _arrayInsert,
24
- arrayPop as _arrayPop,
25
- arrayShift as _arrayShift,
26
- arrayRemove as _arrayRemove,
27
- arrayMove as _arrayMove,
28
19
  arrayPushPublic as _arrayPushPublic,
29
20
  arrayUnshiftPublic as _arrayUnshiftPublic,
30
21
  arrayInsertPublic as _arrayInsertPublic,
@@ -32,19 +23,32 @@ import {
32
23
  arrayShiftPublic as _arrayShiftPublic,
33
24
  arrayRemovePublic as _arrayRemovePublic,
34
25
  arrayMovePublic as _arrayMovePublic,
35
- stringInsertLocal as _stringInsertLocal,
36
- stringRemoveLocal as _stringRemoveLocal,
37
26
  stringInsertPublic as _stringInsertPublic,
38
27
  stringRemovePublic as _stringRemovePublic
39
28
  } from '../dataTree.js'
40
29
  import { on as onCustomEvent, removeListener as removeCustomEventListener } from './eventsCompat.js'
41
30
  import { waitForImperativeQueryReady } from './queryReadiness.js'
42
31
  import { normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js'
43
- import { setRefLink, removeRefLink, getRefLinks } from './refRegistry.js'
32
+ import { setRefLink, removeRefLink, getAllRefLinks } from './refRegistry.js'
44
33
  import { REF_TARGET, resolveRefSignalSafe, resolveRefSegmentsSafe } from './refFallback.js'
45
34
  import { runInBatch } from '../batchScheduler.js'
46
35
  import { runInSilentContext, runInModelEventsSilentContext } from './silentContext.js'
47
36
  import universal$ from '../../react/universal$.js'
37
+ import { getRootContext } from '../rootContext.js'
38
+ import disposeRootContext from '../disposeRootContext.js'
39
+ import {
40
+ arrayInsertPrivateData,
41
+ arrayMovePrivateData,
42
+ arrayPopPrivateData,
43
+ arrayPushPrivateData,
44
+ arrayRemovePrivateData,
45
+ arrayShiftPrivateData,
46
+ arrayUnshiftPrivateData,
47
+ delPrivateData,
48
+ setReplacePrivateData,
49
+ stringInsertPrivateData,
50
+ stringRemovePrivateData
51
+ } from '../privateData.js'
48
52
 
49
53
  class SignalCompat extends Signal {
50
54
  static ID_FIELDS = ['_id', 'id']
@@ -100,10 +104,12 @@ class SignalCompat extends Signal {
100
104
  if (arguments.length < 1 || arguments.length > 3) throw Error('Signal.query() expects one to three arguments')
101
105
  if (typeof collection !== 'string') throw Error('Signal.query() expects collection to be a string')
102
106
  const normalized = normalizeQueryParams(collection, params)
107
+ const root = getRoot(this) || (this[ROOT_ID] ? this : undefined)
108
+ const scopedOptions = withQueryScopeOptions(options, root)
103
109
  if (isAggregationParams(normalized)) {
104
- return getAggregationSignal(collection, normalized, options)
110
+ return getAggregationSignal(collection, normalized, scopedOptions)
105
111
  }
106
- return getQuerySignal(collection, normalized, options)
112
+ return getQuerySignal(collection, normalized, scopedOptions)
107
113
  }
108
114
 
109
115
  subscribe (...items) {
@@ -140,9 +146,15 @@ class SignalCompat extends Signal {
140
146
  if (callback != null && typeof callback !== 'function') {
141
147
  throw Error('Signal.close() expects callback to be a function')
142
148
  }
143
- // Compatibility shim for legacy `model.close()` calls.
144
- // Teamplay uses a global root signal and does not have per-model instances to dispose.
145
- if (callback) callback()
149
+ const $root = getRoot(this) || this
150
+ const rootId = $root?.[ROOT_ID]
151
+ unregisterRootFinalizer($root)
152
+ disposeRootContext(rootId)
153
+ .then(() => callback?.())
154
+ .catch(err => {
155
+ if (callback) callback(err)
156
+ else console.error(err)
157
+ })
146
158
  }
147
159
 
148
160
  silent (value) {
@@ -542,7 +554,8 @@ class SignalCompat extends Signal {
542
554
  }
543
555
  if (typeof handler !== 'function') throw Error('Signal.on() expects a handler function')
544
556
  const normalized = normalizePattern(pattern, 'Signal.on()')
545
- return onModelEvent(eventName, normalized, handler)
557
+ const rootId = (getRoot(this) || this)?.[ROOT_ID]
558
+ return onModelEvent(rootId, eventName, normalized, handler)
546
559
  }
547
560
  if (typeof pattern !== 'function') throw Error('Signal.on() expects a handler function')
548
561
  return onCustomEvent(eventName, pattern)
@@ -572,7 +585,8 @@ class SignalCompat extends Signal {
572
585
  removeListener (eventName, handler) {
573
586
  if (arguments.length !== 2) throw Error('Signal.removeListener() expects two arguments')
574
587
  if (eventName === 'change' || eventName === 'all') {
575
- return removeModelListener(eventName, handler)
588
+ const rootId = (getRoot(this) || this)?.[ROOT_ID]
589
+ return removeModelListener(rootId, eventName, handler)
576
590
  }
577
591
  return removeCustomEventListener(eventName, handler)
578
592
  }
@@ -606,13 +620,21 @@ class SignalCompat extends Signal {
606
620
  const mirrorOnly = !!($to?.[IS_QUERY] || $to?.[IS_AGGREGATION])
607
621
  const { stop, onChange } = createRefLink($from, $to, { mirrorOnly, options })
608
622
  store.set(fromPath, { stop })
623
+ const fromRootId = (getRoot($from) || $from)?.[ROOT_ID]
624
+ const toRootId = (getRoot($to) || $to)?.[ROOT_ID]
609
625
  if (!mirrorOnly) {
610
626
  $from[REF_TARGET] = $to
611
- setRefLink(fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], { mirrorOnly: false })
627
+ setRefLink(fromRootId, fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], {
628
+ mirrorOnly: false,
629
+ fromRootId,
630
+ toRootId
631
+ })
612
632
  } else {
613
- setRefLink(fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], {
633
+ setRefLink(fromRootId, fromPath, $to.path(), $from[SEGMENTS], $to[SEGMENTS], {
614
634
  mirrorOnly: true,
615
- onChange
635
+ onChange,
636
+ fromRootId,
637
+ toRootId
616
638
  })
617
639
  if ($from[REF_TARGET]) delete $from[REF_TARGET]
618
640
  }
@@ -658,7 +680,8 @@ class SignalCompat extends Signal {
658
680
  existing.stop()
659
681
  store.delete(fromPath)
660
682
  }
661
- removeRefLink(fromPath)
683
+ const fromRootId = (getRoot($from) || $from)?.[ROOT_ID]
684
+ removeRefLink(fromRootId, fromPath)
662
685
  const $target = resolveRefSignal($from)
663
686
  if ($target !== $from) {
664
687
  setDiffDeepBypassRef($from, deepCopy($target.get()))
@@ -725,11 +748,10 @@ function createSilentSignalWrapper ($signal, enabled = true) {
725
748
  return new Proxy($signal, handler)
726
749
  }
727
750
 
728
- const REFS = Symbol('compat refs')
729
751
  function getRefStore ($signal) {
730
752
  const $root = getRoot($signal) || $signal
731
- $root[REFS] ??= new Map()
732
- return $root[REFS]
753
+ const rootId = $root?.[ROOT_ID]
754
+ return getRootContext(rootId, true).activeRefs
733
755
  }
734
756
 
735
757
  function createRefLink ($from, $to, { mirrorOnly = false } = {}) {
@@ -790,7 +812,10 @@ function readRefValue ($signal) {
790
812
  function resolveRefSignal ($signal) {
791
813
  const directTarget = resolveRefSignalSafe($signal)
792
814
  if (directTarget && directTarget !== $signal) return directTarget
793
- const resolvedSegments = resolveRefSegmentsSafe($signal[SEGMENTS])
815
+ const resolvedSegments = resolveRefSegmentsSafe(
816
+ $signal[SEGMENTS],
817
+ (getRoot($signal) || $signal)?.[ROOT_ID]
818
+ )
794
819
  if (!resolvedSegments) return $signal
795
820
  const $root = getRoot($signal) || $signal
796
821
  return resolveSignal($root, resolvedSegments)
@@ -805,21 +830,28 @@ function forwardRef ($signal, methodName, args) {
805
830
  function setDiffDeepBypassRef ($signal, value) {
806
831
  const segments = $signal[SEGMENTS]
807
832
  if (isPublicCollection(segments[0])) return Signal.prototype.set.call($signal, value)
808
- return _setReplace(segments, value)
833
+ return setReplacePrivateData(getOwningRootId($signal), segments, value)
809
834
  }
810
835
 
811
836
  function mirrorRefMutationFromTarget (targetSegments, value) {
812
837
  if (!Array.isArray(targetSegments) || targetSegments.length === 0) return
813
838
  const updates = []
814
- for (const link of getRefLinks().values()) {
839
+ for (const link of getAllRefLinks()) {
815
840
  if (!isPathPrefix(link.toSegments, targetSegments)) continue
816
841
  const suffix = targetSegments.slice(link.toSegments.length)
817
- updates.push({ segments: link.fromSegments.concat(suffix), value: deepCopy(value) })
842
+ updates.push({
843
+ fromRootId: link.fromRootId,
844
+ segments: link.fromSegments.concat(suffix),
845
+ value: deepCopy(value)
846
+ })
818
847
  }
819
848
  if (!updates.length) return
820
- const $root = getRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ })
821
849
  runInModelEventsSilentContext(() => {
822
850
  for (const update of updates) {
851
+ const $root = getRootSignal({
852
+ rootId: update.fromRootId || GLOBAL_ROOT_ID,
853
+ rootFunction: universal$
854
+ })
823
855
  const $target = resolveSignal($root, update.segments)
824
856
  setDiffDeepBypassRef($target, update.value)
825
857
  }
@@ -891,7 +923,10 @@ function resolveSignal ($signal, segments) {
891
923
  function resolveSignalWithRefs ($signal, relativeSegments) {
892
924
  const baseSegments = Array.isArray($signal?.[SEGMENTS]) ? $signal[SEGMENTS] : []
893
925
  const absoluteSegments = baseSegments.concat(relativeSegments)
894
- const resolvedSegments = resolveRefSegmentsSafe(absoluteSegments)
926
+ const resolvedSegments = resolveRefSegmentsSafe(
927
+ absoluteSegments,
928
+ (getRoot($signal) || $signal)?.[ROOT_ID]
929
+ )
895
930
  if (!resolvedSegments) return resolveSignal($signal, relativeSegments)
896
931
 
897
932
  // Signals created through root functions can carry a raw root in [ROOT].
@@ -1018,7 +1053,7 @@ function setReplacePrivateCompatSync ($signal, value) {
1018
1053
  if (isPublicDocPath(segments)) {
1019
1054
  value = normalizeIdFields(value, idFields, segments[1])
1020
1055
  }
1021
- _setReplace(segments, value)
1056
+ setReplacePrivateData(getOwningRootId($signal), segments, value)
1022
1057
  mirrorRefMutationFromTarget(segments, value)
1023
1058
  }
1024
1059
 
@@ -1027,7 +1062,7 @@ function delPrivateCompatSync ($signal) {
1027
1062
  if (segments.length === 0) throw Error('Can\'t delete the root signal data')
1028
1063
  const idFields = getIdFieldsForSegments(segments)
1029
1064
  if (isIdFieldPath(segments, idFields)) return
1030
- _del(segments)
1065
+ delPrivateData(getOwningRootId($signal), segments)
1031
1066
  }
1032
1067
 
1033
1068
  function deepEqualCompat (left, right) {
@@ -1074,7 +1109,7 @@ async function setReplaceOnSignal ($signal, value) {
1074
1109
  return result
1075
1110
  }
1076
1111
  if (publicOnly) throw Error(ERRORS.publicOnly)
1077
- const result = _setReplace(segments, value)
1112
+ const result = setReplacePrivateData(getOwningRootId($signal), segments, value)
1078
1113
  mirrorRefMutationFromTarget(segments, value)
1079
1114
  return result
1080
1115
  }
@@ -1094,7 +1129,7 @@ async function incrementOnSignal ($signal, byNumber) {
1094
1129
  return currentValue + byNumber
1095
1130
  }
1096
1131
  if (publicOnly) throw Error(ERRORS.publicOnly)
1097
- _setReplace(segments, currentValue + byNumber)
1132
+ setReplacePrivateData(getOwningRootId($signal), segments, currentValue + byNumber)
1098
1133
  return currentValue + byNumber
1099
1134
  }
1100
1135
 
@@ -1127,7 +1162,7 @@ async function arrayPushOnSignal ($signal, value) {
1127
1162
  if (isIdFieldPath(segments, idFields)) return
1128
1163
  if (isPublicCollection(segments[0])) return _arrayPushPublic(segments, value)
1129
1164
  if (publicOnly) throw Error(ERRORS.publicOnly)
1130
- return _arrayPush(segments, value)
1165
+ return arrayPushPrivateData(getOwningRootId($signal), segments, value)
1131
1166
  }
1132
1167
 
1133
1168
  async function arrayUnshiftOnSignal ($signal, value) {
@@ -1136,7 +1171,7 @@ async function arrayUnshiftOnSignal ($signal, value) {
1136
1171
  if (isIdFieldPath(segments, idFields)) return
1137
1172
  if (isPublicCollection(segments[0])) return _arrayUnshiftPublic(segments, value)
1138
1173
  if (publicOnly) throw Error(ERRORS.publicOnly)
1139
- return _arrayUnshift(segments, value)
1174
+ return arrayUnshiftPrivateData(getOwningRootId($signal), segments, value)
1140
1175
  }
1141
1176
 
1142
1177
  async function arrayInsertOnSignal ($signal, index, values) {
@@ -1145,7 +1180,7 @@ async function arrayInsertOnSignal ($signal, index, values) {
1145
1180
  if (isIdFieldPath(segments, idFields)) return
1146
1181
  if (isPublicCollection(segments[0])) return _arrayInsertPublic(segments, index, values)
1147
1182
  if (publicOnly) throw Error(ERRORS.publicOnly)
1148
- return _arrayInsert(segments, index, values)
1183
+ return arrayInsertPrivateData(getOwningRootId($signal), segments, index, values)
1149
1184
  }
1150
1185
 
1151
1186
  async function arrayPopOnSignal ($signal) {
@@ -1154,7 +1189,7 @@ async function arrayPopOnSignal ($signal) {
1154
1189
  if (isIdFieldPath(segments, idFields)) return
1155
1190
  if (isPublicCollection(segments[0])) return _arrayPopPublic(segments)
1156
1191
  if (publicOnly) throw Error(ERRORS.publicOnly)
1157
- return _arrayPop(segments)
1192
+ return arrayPopPrivateData(getOwningRootId($signal), segments)
1158
1193
  }
1159
1194
 
1160
1195
  async function arrayShiftOnSignal ($signal) {
@@ -1163,7 +1198,7 @@ async function arrayShiftOnSignal ($signal) {
1163
1198
  if (isIdFieldPath(segments, idFields)) return
1164
1199
  if (isPublicCollection(segments[0])) return _arrayShiftPublic(segments)
1165
1200
  if (publicOnly) throw Error(ERRORS.publicOnly)
1166
- return _arrayShift(segments)
1201
+ return arrayShiftPrivateData(getOwningRootId($signal), segments)
1167
1202
  }
1168
1203
 
1169
1204
  async function arrayRemoveOnSignal ($signal, index, howMany) {
@@ -1172,7 +1207,7 @@ async function arrayRemoveOnSignal ($signal, index, howMany) {
1172
1207
  if (isIdFieldPath(segments, idFields)) return
1173
1208
  if (isPublicCollection(segments[0])) return _arrayRemovePublic(segments, index, howMany)
1174
1209
  if (publicOnly) throw Error(ERRORS.publicOnly)
1175
- return _arrayRemove(segments, index, howMany)
1210
+ return arrayRemovePrivateData(getOwningRootId($signal), segments, index, howMany)
1176
1211
  }
1177
1212
 
1178
1213
  async function arrayMoveOnSignal ($signal, from, to, howMany) {
@@ -1181,7 +1216,7 @@ async function arrayMoveOnSignal ($signal, from, to, howMany) {
1181
1216
  if (isIdFieldPath(segments, idFields)) return
1182
1217
  if (isPublicCollection(segments[0])) return _arrayMovePublic(segments, from, to, howMany)
1183
1218
  if (publicOnly) throw Error(ERRORS.publicOnly)
1184
- return _arrayMove(segments, from, to, howMany)
1219
+ return arrayMovePrivateData(getOwningRootId($signal), segments, from, to, howMany)
1185
1220
  }
1186
1221
 
1187
1222
  async function stringInsertOnSignal ($signal, index, text) {
@@ -1190,7 +1225,7 @@ async function stringInsertOnSignal ($signal, index, text) {
1190
1225
  if (isIdFieldPath(segments, idFields)) return
1191
1226
  if (isPublicCollection(segments[0])) return _stringInsertPublic(segments, index, text)
1192
1227
  if (publicOnly) throw Error(ERRORS.publicOnly)
1193
- return _stringInsertLocal(segments, index, text)
1228
+ return stringInsertPrivateData(getOwningRootId($signal), segments, index, text)
1194
1229
  }
1195
1230
 
1196
1231
  async function stringRemoveOnSignal ($signal, index, howMany) {
@@ -1199,7 +1234,12 @@ async function stringRemoveOnSignal ($signal, index, howMany) {
1199
1234
  if (isIdFieldPath(segments, idFields)) return
1200
1235
  if (isPublicCollection(segments[0])) return _stringRemovePublic(segments, index, howMany)
1201
1236
  if (publicOnly) throw Error(ERRORS.publicOnly)
1202
- return _stringRemoveLocal(segments, index, howMany)
1237
+ return stringRemovePrivateData(getOwningRootId($signal), segments, index, howMany)
1238
+ }
1239
+
1240
+ function getOwningRootId ($signal) {
1241
+ const $root = getRoot($signal) || $signal
1242
+ return $root?.[ROOT_ID]
1203
1243
  }
1204
1244
 
1205
1245
  function shallowCopy (value) {
@@ -1246,6 +1286,17 @@ function isAggregationParams (params) {
1246
1286
  return Boolean(params?.$aggregate || params?.$aggregationName)
1247
1287
  }
1248
1288
 
1289
+ function withQueryScopeOptions (options, $root) {
1290
+ if (!options || typeof options !== 'object') {
1291
+ if (!$root) return options
1292
+ return { root: $root }
1293
+ }
1294
+
1295
+ const nextOptions = { ...options }
1296
+ if (nextOptions.root == null && $root) nextOptions.root = $root
1297
+ return nextOptions
1298
+ }
1299
+
1249
1300
  function withFetchOnly (fn) {
1250
1301
  const prevFetchOnly = fetchOnly
1251
1302
  setFetchOnly(true)
@@ -54,9 +54,9 @@ export function useOn (eventName, patternOrHandler, handler, deps) {
54
54
  return
55
55
  }
56
56
  if (!isModelEventsEnabled()) return
57
- const listener = onModelEvent(eventName, normalizedPattern, handler)
57
+ const listener = onModelEvent(undefined, eventName, normalizedPattern, handler)
58
58
  return () => {
59
- removeModelListener(eventName, listener)
59
+ removeModelListener(undefined, eventName, listener)
60
60
  }
61
61
  }, [eventName, patternOrHandler, handler, deps, normalizedPattern, isCustom])
62
62
  }
@@ -1,11 +1,10 @@
1
- import { getRefLinks } from './refRegistry.js'
1
+ import { getRefLinks, getRefRootIds } from './refRegistry.js'
2
2
  import { isCompatEnv } from '../compatEnv.js'
3
3
  import { isSilentContextActive, isModelEventsSilentContextActive } from './silentContext.js'
4
+ import { normalizeRootId } from '../rootScope.js'
5
+ import { getRootContext, getRootContexts } from '../rootContext.js'
4
6
 
5
- const modelListeners = {
6
- change: new Map(),
7
- all: new Map()
8
- }
7
+ const MODEL_EVENT_NAMES = ['change', 'all']
9
8
 
10
9
  export function isModelEventsEnabled () {
11
10
  return isCompatEnv()
@@ -20,10 +19,10 @@ export function normalizePattern (pattern, methodName) {
20
19
  return pattern.split('.').filter(Boolean).join('.')
21
20
  }
22
21
 
23
- export function onModelEvent (eventName, pattern, handler) {
22
+ export function onModelEvent (rootId, eventName, pattern, handler) {
24
23
  if (typeof handler !== 'function') throw Error('Model event handler must be a function')
25
- if (!modelListeners[eventName]) throw Error(`Unsupported model event: ${eventName}`)
26
- const store = modelListeners[eventName]
24
+ if (!MODEL_EVENT_NAMES.includes(eventName)) throw Error(`Unsupported model event: ${eventName}`)
25
+ const store = getModelEventRootStore(eventName, rootId, true)
27
26
  const normalized = normalizePattern(pattern)
28
27
  let entry = store.get(normalized)
29
28
  if (!entry) {
@@ -38,8 +37,8 @@ export function onModelEvent (eventName, pattern, handler) {
38
37
  return handler
39
38
  }
40
39
 
41
- export function removeModelListener (eventName, handler) {
42
- const store = modelListeners[eventName]
40
+ export function removeModelListener (rootId, eventName, handler) {
41
+ const store = getModelEventRootStore(eventName, rootId)
43
42
  if (!store) return
44
43
  for (const [pattern, entry] of store) {
45
44
  entry.handlers.delete(handler)
@@ -51,39 +50,44 @@ export function emitModelChange (path, value, prevValue, meta) {
51
50
  if (!isModelEventsEnabled()) return
52
51
  if (isSilentContextActive() || isModelEventsSilentContextActive()) return
53
52
  const initialSegments = splitPath(path)
54
- const visited = new Set()
55
- const queue = [initialSegments]
56
53
  const eventName = meta?.eventName || 'change'
54
+ const rootIds = getTargetRootIds(meta?.rootId)
55
+
56
+ for (const rootId of rootIds) {
57
+ const visited = new Set()
58
+ const queue = [initialSegments]
57
59
 
58
- while (queue.length) {
59
- const segments = queue.shift()
60
- const key = segments.join('.')
61
- if (visited.has(key)) continue
62
- visited.add(key)
60
+ while (queue.length) {
61
+ const segments = queue.shift()
62
+ const key = segments.join('.')
63
+ if (visited.has(key)) continue
64
+ visited.add(key)
63
65
 
64
- emitForEvent('change', segments, value, prevValue, meta)
65
- emitForEvent('all', segments, value, prevValue, meta, eventName)
66
+ emitForEvent(rootId, 'change', segments, value, prevValue, meta)
67
+ emitForEvent(rootId, 'all', segments, value, prevValue, meta, eventName)
66
68
 
67
- for (const link of getRefLinks().values()) {
68
- if (!isPathPrefix(link.toSegments, segments)) continue
69
- if (link.mirrorOnly && typeof link.onChange === 'function') {
70
- link.onChange()
69
+ for (const link of getRefLinks(rootId).values()) {
70
+ if (!isPathPrefix(link.toSegments, segments)) continue
71
+ if (link.mirrorOnly && typeof link.onChange === 'function') {
72
+ link.onChange()
73
+ }
74
+ const suffix = segments.slice(link.toSegments.length)
75
+ const nextSegments = link.fromSegments.concat(suffix)
76
+ const nextKey = nextSegments.join('.')
77
+ if (!visited.has(nextKey)) queue.push(nextSegments)
71
78
  }
72
- const suffix = segments.slice(link.toSegments.length)
73
- const nextSegments = link.fromSegments.concat(suffix)
74
- const nextKey = nextSegments.join('.')
75
- if (!visited.has(nextKey)) queue.push(nextSegments)
76
79
  }
77
80
  }
78
81
  }
79
82
 
80
83
  export function __resetModelEventsForTests () {
81
- modelListeners.change.clear()
82
- modelListeners.all.clear()
84
+ for (const context of getRootContexts()) {
85
+ context.resetModelListeners()
86
+ }
83
87
  }
84
88
 
85
- function emitForEvent (eventName, pathSegments, value, prevValue, meta, resolvedEventName = eventName) {
86
- const store = modelListeners[eventName]
89
+ function emitForEvent (rootId, eventName, pathSegments, value, prevValue, meta, resolvedEventName = eventName) {
90
+ const store = getModelEventRootStore(eventName, rootId)
87
91
  if (!store || store.size === 0) return
88
92
  for (const entry of store.values()) {
89
93
  const captures = matchPattern(entry.segments, pathSegments)
@@ -103,6 +107,29 @@ function splitPattern (pattern) {
103
107
  return pattern.split('.').filter(Boolean)
104
108
  }
105
109
 
110
+ function getModelEventRootStore (eventName, rootId, create = false) {
111
+ return getRootContext(normalizeRootId(rootId), create)?.getModelEventStore(eventName, create)
112
+ }
113
+
114
+ function getModelEventRootIds () {
115
+ const rootIds = new Set()
116
+ for (const context of getRootContexts()) {
117
+ for (const store of Object.values(context.modelListeners)) {
118
+ if (store.size) rootIds.add(context.rootId)
119
+ }
120
+ }
121
+ return rootIds
122
+ }
123
+
124
+ function getTargetRootIds (rootId) {
125
+ if (rootId != null) return [normalizeRootId(rootId)]
126
+ const rootIds = new Set([
127
+ ...getModelEventRootIds(),
128
+ ...getRefRootIds()
129
+ ])
130
+ return rootIds
131
+ }
132
+
106
133
  function splitPath (path) {
107
134
  if (Array.isArray(path)) return path.map(segment => String(segment))
108
135
  if (!path) return []