@dxos/echo 0.9.0 → 0.9.1-staging.ee54ba693a

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 (289) hide show
  1. package/dist/lib/neutral/Annotation.mjs +30 -8
  2. package/dist/lib/neutral/Database.mjs +7 -3
  3. package/dist/lib/neutral/Entity.mjs +15 -11
  4. package/dist/lib/neutral/Err.mjs +7 -3
  5. package/dist/lib/neutral/Feed.mjs +15 -19
  6. package/dist/lib/neutral/Filter.mjs +13 -11
  7. package/dist/lib/neutral/Format.mjs +3 -7
  8. package/dist/lib/neutral/JsonSchema.mjs +7 -7
  9. package/dist/lib/neutral/Migration.mjs +11 -11
  10. package/dist/lib/neutral/Obj.mjs +19 -13
  11. package/dist/lib/neutral/Order.mjs +1 -1
  12. package/dist/lib/neutral/Query.mjs +17 -17
  13. package/dist/lib/neutral/QueryResult.mjs +1 -1
  14. package/dist/lib/neutral/Ref.mjs +6 -6
  15. package/dist/lib/neutral/Registry.mjs +1 -1
  16. package/dist/lib/neutral/Relation.mjs +12 -12
  17. package/dist/lib/neutral/Tag.mjs +15 -15
  18. package/dist/lib/neutral/Text.mjs +25 -0
  19. package/dist/lib/neutral/Text.mjs.map +7 -0
  20. package/dist/lib/neutral/Type.mjs +10 -10
  21. package/dist/lib/neutral/chunk-2HKGAQM4.mjs +1394 -0
  22. package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +7 -0
  23. package/dist/lib/neutral/{chunk-O6BH7EPN.mjs → chunk-2ZHECXUR.mjs} +5 -4
  24. package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +7 -0
  25. package/dist/lib/neutral/{chunk-QQIYS74I.mjs → chunk-3KTE2Y7Q.mjs} +20 -20
  26. package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-R5W6DXR4.mjs → chunk-3TN2VMI4.mjs} +72 -36
  28. package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +7 -0
  29. package/dist/lib/neutral/{chunk-5SMDBFVB.mjs → chunk-42L4HJ3Z.mjs} +11 -15
  30. package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +7 -0
  31. package/dist/lib/neutral/{chunk-7PI7C4EF.mjs → chunk-4A2NWKRJ.mjs} +5 -5
  32. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +1531 -0
  33. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +7 -0
  34. package/dist/lib/neutral/{chunk-SBVFRTST.mjs → chunk-BEFR7LEA.mjs} +20 -10
  35. package/dist/lib/neutral/chunk-BEFR7LEA.mjs.map +7 -0
  36. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs → chunk-BXIKXT7Q.mjs} +2 -6
  37. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs.map → chunk-BXIKXT7Q.mjs.map} +1 -1
  38. package/dist/lib/neutral/{chunk-UUP46KUQ.mjs → chunk-C57XMMBY.mjs} +21 -20
  39. package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +7 -0
  40. package/dist/lib/neutral/chunk-C62JYFAD.mjs +183 -0
  41. package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-MZ7K3MLL.mjs → chunk-FSGX2R62.mjs} +3 -3
  43. package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-TYGKCRMK.mjs → chunk-HBXT3GSB.mjs} +37 -15
  45. package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-WU3GIANS.mjs → chunk-IQR45HAD.mjs} +1 -1
  47. package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +7 -0
  48. package/dist/lib/neutral/{chunk-TFEWTY5A.mjs → chunk-KCBW4BR6.mjs} +4 -8
  49. package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +7 -0
  50. package/dist/lib/neutral/{chunk-WTQJHC75.mjs → chunk-L3E5IXAE.mjs} +225 -45
  51. package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +7 -0
  52. package/dist/lib/neutral/{chunk-EVK6XBXO.mjs → chunk-PRC4FEKJ.mjs} +2 -2
  53. package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +7 -0
  54. package/dist/lib/neutral/{chunk-6YDI3J37.mjs → chunk-SCT6FFCR.mjs} +9 -4
  55. package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +7 -0
  56. package/dist/lib/neutral/{chunk-BBFJWWAV.mjs → chunk-TOCVVFK2.mjs} +26 -2
  57. package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +7 -0
  58. package/dist/lib/neutral/{chunk-RIVWNMSF.mjs → chunk-U53LSQQA.mjs} +3 -3
  59. package/dist/lib/neutral/{chunk-RIVWNMSF.mjs.map → chunk-U53LSQQA.mjs.map} +1 -1
  60. package/dist/lib/neutral/{chunk-IGK6FN65.mjs → chunk-UGFFV2FR.mjs} +2 -2
  61. package/dist/lib/neutral/{chunk-35INCYOE.mjs → chunk-UWLP4RFM.mjs} +1 -1
  62. package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +7 -0
  63. package/dist/lib/neutral/{chunk-LWXVKPPW.mjs → chunk-UZHCQ6NI.mjs} +13 -11
  64. package/dist/lib/neutral/{chunk-LWXVKPPW.mjs.map → chunk-UZHCQ6NI.mjs.map} +3 -3
  65. package/dist/lib/neutral/{chunk-5SUJPHAE.mjs → chunk-WKIT2YY5.mjs} +2 -2
  66. package/dist/lib/neutral/{chunk-T6E37YIP.mjs → chunk-XB6ISRAO.mjs} +38 -18
  67. package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +7 -0
  68. package/dist/lib/neutral/chunk-XXL42WCB.mjs +84 -0
  69. package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +7 -0
  70. package/dist/lib/neutral/{chunk-ZGNNFYHS.mjs → chunk-XXWK4J53.mjs} +27 -42
  71. package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +7 -0
  72. package/dist/lib/neutral/index.mjs +30 -26
  73. package/dist/lib/neutral/internal/index.mjs +111 -69
  74. package/dist/lib/neutral/meta.json +1 -1
  75. package/dist/lib/neutral/testing/index.mjs +79 -60
  76. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  77. package/dist/types/src/Annotation.d.ts +35 -1
  78. package/dist/types/src/Annotation.d.ts.map +1 -1
  79. package/dist/types/src/Collection.d.ts +9 -8
  80. package/dist/types/src/Collection.d.ts.map +1 -1
  81. package/dist/types/src/Database.d.ts +38 -0
  82. package/dist/types/src/Database.d.ts.map +1 -1
  83. package/dist/types/src/Dataset.d.ts +1 -16
  84. package/dist/types/src/Dataset.d.ts.map +1 -1
  85. package/dist/types/src/Entity.d.ts +2 -0
  86. package/dist/types/src/Entity.d.ts.map +1 -1
  87. package/dist/types/src/Err.d.ts +67 -0
  88. package/dist/types/src/Err.d.ts.map +1 -1
  89. package/dist/types/src/Feed.d.ts +59 -80
  90. package/dist/types/src/Feed.d.ts.map +1 -1
  91. package/dist/types/src/Filter.d.ts +9 -1
  92. package/dist/types/src/Filter.d.ts.map +1 -1
  93. package/dist/types/src/Hypergraph.d.ts +2 -9
  94. package/dist/types/src/Hypergraph.d.ts.map +1 -1
  95. package/dist/types/src/Migration.d.ts +1 -1
  96. package/dist/types/src/Migration.d.ts.map +1 -1
  97. package/dist/types/src/Obj.d.ts +33 -6
  98. package/dist/types/src/Obj.d.ts.map +1 -1
  99. package/dist/types/src/Order.d.ts +1 -1
  100. package/dist/types/src/Order.d.ts.map +1 -1
  101. package/dist/types/src/Query.d.ts +1 -1
  102. package/dist/types/src/Query.d.ts.map +1 -1
  103. package/dist/types/src/QueryResult.d.ts +2 -2
  104. package/dist/types/src/QueryResult.d.ts.map +1 -1
  105. package/dist/types/src/Ref.d.ts +10 -6
  106. package/dist/types/src/Ref.d.ts.map +1 -1
  107. package/dist/types/src/Registry.d.ts +1 -1
  108. package/dist/types/src/Relation.d.ts.map +1 -1
  109. package/dist/types/src/Tag.d.ts +11 -13
  110. package/dist/types/src/Tag.d.ts.map +1 -1
  111. package/dist/types/src/Text.d.ts +41 -0
  112. package/dist/types/src/Text.d.ts.map +1 -0
  113. package/dist/types/src/Text.test.d.ts +2 -0
  114. package/dist/types/src/Text.test.d.ts.map +1 -0
  115. package/dist/types/src/Type.d.ts +38 -21
  116. package/dist/types/src/Type.d.ts.map +1 -1
  117. package/dist/types/src/View.d.ts +8 -9
  118. package/dist/types/src/View.d.ts.map +1 -1
  119. package/dist/types/src/index.d.ts +1 -0
  120. package/dist/types/src/index.d.ts.map +1 -1
  121. package/dist/types/src/internal/Annotation/annotations.d.ts +50 -0
  122. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  123. package/dist/types/src/internal/Annotation/atoms.d.ts +14 -0
  124. package/dist/types/src/internal/Annotation/atoms.d.ts.map +1 -0
  125. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts +13 -0
  126. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts.map +1 -1
  127. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  128. package/dist/types/src/internal/Entity/guard.d.ts.map +1 -1
  129. package/dist/types/src/internal/Entity/model.d.ts +1 -1
  130. package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
  131. package/dist/types/src/internal/Entity/object.d.ts +5 -1
  132. package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
  133. package/dist/types/src/internal/Entity/relation.d.ts +7 -0
  134. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  135. package/dist/types/src/internal/Entity/type-uri.d.ts.map +1 -1
  136. package/dist/types/src/internal/Filter/index.d.ts +2 -0
  137. package/dist/types/src/internal/Filter/index.d.ts.map +1 -0
  138. package/dist/types/src/internal/Filter/match.d.ts +22 -0
  139. package/dist/types/src/internal/Filter/match.d.ts.map +1 -0
  140. package/dist/types/src/internal/Format/format.d.ts +0 -2
  141. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  142. package/dist/types/src/internal/Format/index.d.ts +2 -2
  143. package/dist/types/src/internal/Format/index.d.ts.map +1 -1
  144. package/dist/types/src/internal/Format/string.d.ts +0 -8
  145. package/dist/types/src/internal/Format/string.d.ts.map +1 -1
  146. package/dist/types/src/internal/Format/types.d.ts +2 -1
  147. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  148. package/dist/types/src/internal/Obj/atoms.d.ts +12 -3
  149. package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
  150. package/dist/types/src/internal/Obj/clone.d.ts +2 -1
  151. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  152. package/dist/types/src/internal/Obj/create-object.d.ts +3 -4
  153. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  154. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  155. package/dist/types/src/internal/Query/index.d.ts +1 -0
  156. package/dist/types/src/internal/Query/index.d.ts.map +1 -1
  157. package/dist/types/src/internal/Query/query-result-effect.d.ts +9 -0
  158. package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -0
  159. package/dist/types/src/internal/Ref/atoms.d.ts +1 -2
  160. package/dist/types/src/internal/Ref/atoms.d.ts.map +1 -1
  161. package/dist/types/src/internal/Ref/index.d.ts +1 -0
  162. package/dist/types/src/internal/Ref/index.d.ts.map +1 -1
  163. package/dist/types/src/internal/Ref/ref.d.ts +79 -8
  164. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  165. package/dist/types/src/internal/Ref/strong-deps.d.ts +35 -0
  166. package/dist/types/src/internal/Ref/strong-deps.d.ts.map +1 -0
  167. package/dist/types/src/internal/common/atom-snapshot.d.ts +8 -0
  168. package/dist/types/src/internal/common/atom-snapshot.d.ts.map +1 -0
  169. package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
  170. package/dist/types/src/internal/common/proxy/proxy-types.d.ts +12 -0
  171. package/dist/types/src/internal/common/proxy/proxy-types.d.ts.map +1 -1
  172. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts +25 -0
  173. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  174. package/dist/types/src/internal/common/proxy/schema-validator.d.ts +5 -0
  175. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  176. package/dist/types/src/internal/common/proxy/typed-handler.d.ts +17 -0
  177. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  178. package/dist/types/src/internal/common/types/entity.d.ts +4 -4
  179. package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
  180. package/dist/types/src/internal/common/types/meta.d.ts +2 -4
  181. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  182. package/dist/types/src/internal/index.d.ts +1 -0
  183. package/dist/types/src/internal/index.d.ts.map +1 -1
  184. package/dist/types/src/testing/test-schema.d.ts +42 -171
  185. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  186. package/dist/types/tsconfig.tsbuildinfo +1 -1
  187. package/package.json +20 -13
  188. package/src/Annotation.test.ts +257 -10
  189. package/src/Annotation.ts +58 -4
  190. package/src/Collection.ts +10 -12
  191. package/src/Database.ts +55 -21
  192. package/src/Entity.ts +3 -1
  193. package/src/Err.ts +22 -0
  194. package/src/Feed.ts +117 -141
  195. package/src/Filter.test.ts +38 -0
  196. package/src/Filter.ts +17 -3
  197. package/src/Hypergraph.ts +2 -11
  198. package/src/Migration.ts +1 -1
  199. package/src/Obj.test.ts +92 -12
  200. package/src/Obj.ts +38 -5
  201. package/src/Order.ts +3 -3
  202. package/src/Query.test.ts +2 -2
  203. package/src/Query.ts +16 -16
  204. package/src/QueryResult.ts +2 -2
  205. package/src/Ref.ts +14 -6
  206. package/src/Registry.ts +1 -1
  207. package/src/Relation.ts +1 -1
  208. package/src/Tag.ts +13 -16
  209. package/src/Text.test.ts +186 -0
  210. package/src/Text.ts +116 -0
  211. package/src/Type.test.ts +52 -24
  212. package/src/Type.ts +85 -33
  213. package/src/View.ts +22 -30
  214. package/src/index.ts +1 -1
  215. package/src/internal/Annotation/annotations.ts +71 -2
  216. package/src/internal/Annotation/atoms.ts +107 -0
  217. package/src/internal/Annotation/entity-dictionary.ts +40 -37
  218. package/src/internal/Entity/api.ts +1 -1
  219. package/src/internal/Entity/guard.ts +2 -1
  220. package/src/internal/Entity/model.ts +2 -2
  221. package/src/internal/Entity/object.ts +14 -1
  222. package/src/internal/Entity/relation.ts +19 -0
  223. package/src/internal/Entity/type-uri.ts +3 -1
  224. package/src/internal/Filter/index.ts +5 -0
  225. package/src/internal/Filter/match.ts +284 -0
  226. package/src/internal/Format/format.ts +0 -2
  227. package/src/internal/Format/index.ts +2 -2
  228. package/src/internal/Format/string.ts +0 -12
  229. package/src/internal/Format/types.ts +4 -2
  230. package/src/internal/JsonSchema/json-schema.test.ts +82 -54
  231. package/src/internal/Obj/atoms.ts +62 -18
  232. package/src/internal/Obj/clone.ts +61 -20
  233. package/src/internal/Obj/create-object.test.ts +3 -3
  234. package/src/internal/Obj/create-object.ts +3 -4
  235. package/src/internal/Obj/inspect.ts +1 -1
  236. package/src/internal/Obj/json-serializer.test.ts +7 -5
  237. package/src/internal/Obj/json-serializer.ts +4 -4
  238. package/src/internal/Obj/schema-validator.test.ts +53 -0
  239. package/src/internal/Obj/set-value.test.ts +29 -19
  240. package/src/internal/Query/index.ts +1 -0
  241. package/src/internal/Query/query-result-effect.ts +35 -0
  242. package/src/internal/Ref/atoms.ts +23 -3
  243. package/src/internal/Ref/index.ts +1 -0
  244. package/src/internal/Ref/ref.test.ts +8 -8
  245. package/src/internal/Ref/ref.ts +122 -39
  246. package/src/internal/Ref/strong-deps.ts +107 -0
  247. package/src/internal/common/atom-snapshot.ts +19 -0
  248. package/src/internal/common/proxy/make-object.ts +1 -1
  249. package/src/internal/common/proxy/proxy-types.ts +20 -0
  250. package/src/internal/common/proxy/proxy-utils.ts +37 -1
  251. package/src/internal/common/proxy/reactive-array.ts +3 -3
  252. package/src/internal/common/proxy/reactive.test.ts +1 -1
  253. package/src/internal/common/proxy/schema-validator.ts +181 -8
  254. package/src/internal/common/proxy/typed-handler.ts +254 -76
  255. package/src/internal/common/types/entity.ts +15 -7
  256. package/src/internal/index.ts +1 -0
  257. package/src/testing/test-schema.ts +75 -90
  258. package/dist/lib/neutral/chunk-35INCYOE.mjs.map +0 -7
  259. package/dist/lib/neutral/chunk-3PBP4V4O.mjs +0 -101
  260. package/dist/lib/neutral/chunk-3PBP4V4O.mjs.map +0 -7
  261. package/dist/lib/neutral/chunk-4ZUHOTCG.mjs +0 -184
  262. package/dist/lib/neutral/chunk-4ZUHOTCG.mjs.map +0 -7
  263. package/dist/lib/neutral/chunk-5SMDBFVB.mjs.map +0 -7
  264. package/dist/lib/neutral/chunk-6YDI3J37.mjs.map +0 -7
  265. package/dist/lib/neutral/chunk-7FPIAJIV.mjs +0 -2279
  266. package/dist/lib/neutral/chunk-7FPIAJIV.mjs.map +0 -7
  267. package/dist/lib/neutral/chunk-BBFJWWAV.mjs.map +0 -7
  268. package/dist/lib/neutral/chunk-EVK6XBXO.mjs.map +0 -7
  269. package/dist/lib/neutral/chunk-MZ7K3MLL.mjs.map +0 -7
  270. package/dist/lib/neutral/chunk-O6BH7EPN.mjs.map +0 -7
  271. package/dist/lib/neutral/chunk-QQIYS74I.mjs.map +0 -7
  272. package/dist/lib/neutral/chunk-R5W6DXR4.mjs.map +0 -7
  273. package/dist/lib/neutral/chunk-SBVFRTST.mjs.map +0 -7
  274. package/dist/lib/neutral/chunk-T6E37YIP.mjs.map +0 -7
  275. package/dist/lib/neutral/chunk-TFEWTY5A.mjs.map +0 -7
  276. package/dist/lib/neutral/chunk-TYGKCRMK.mjs.map +0 -7
  277. package/dist/lib/neutral/chunk-UUP46KUQ.mjs.map +0 -7
  278. package/dist/lib/neutral/chunk-WTQJHC75.mjs.map +0 -7
  279. package/dist/lib/neutral/chunk-WU3GIANS.mjs.map +0 -7
  280. package/dist/lib/neutral/chunk-ZGNNFYHS.mjs.map +0 -7
  281. package/dist/types/src/exemplars.test.d.ts +0 -2
  282. package/dist/types/src/exemplars.test.d.ts.map +0 -1
  283. package/dist/types/src/hierarchy.test.d.ts +0 -2
  284. package/dist/types/src/hierarchy.test.d.ts.map +0 -1
  285. package/src/exemplars.test.ts +0 -21
  286. package/src/hierarchy.test.ts +0 -33
  287. /package/dist/lib/neutral/{chunk-7PI7C4EF.mjs.map → chunk-4A2NWKRJ.mjs.map} +0 -0
  288. /package/dist/lib/neutral/{chunk-IGK6FN65.mjs.map → chunk-UGFFV2FR.mjs.map} +0 -0
  289. /package/dist/lib/neutral/{chunk-5SUJPHAE.mjs.map → chunk-WKIT2YY5.mjs.map} +0 -0
@@ -14,10 +14,13 @@ import type * as Entity from '../../Entity';
14
14
  import type * as Obj from '../../Obj';
15
15
  import type * as Ref from '../../Ref';
16
16
  import type * as Relation from '../../Relation';
17
+ import { getLabel } from '../Annotation';
18
+ import { snapshotForComparison } from '../common/atom-snapshot';
17
19
  import { subscribe } from '../common/proxy/reactive';
18
- import { isEntity, getDatabase } from '../Entity';
20
+ import { getDatabase, isEntity } from '../Entity';
19
21
  import { RefTypeId } from '../Ref/ref';
20
22
  import { loadRefTarget } from '../Ref/utils';
23
+ import { isDeleted } from './deleted';
21
24
  import { getSnapshot } from './snapshot';
22
25
 
23
26
  const isRef = (obj: unknown): obj is Ref.Ref<any> =>
@@ -62,8 +65,9 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
62
65
  const setupTargetSubscription = (target: T): Obj.Snapshot<T> => {
63
66
  unsubscribeTarget?.();
64
67
  unsubscribeTarget = subscribe(target, () => {
68
+ // Deleted objects resolve to undefined so callers don't need to inspect isDeleted.
65
69
  // getSnapshot adds SnapshotKindId brand at runtime; cast bridges static types.
66
- get.setSelf(getSnapshot(target) as unknown as Obj.Snapshot<T>);
70
+ get.setSelf(isDeleted(target) ? undefined : (getSnapshot(target) as unknown as Obj.Snapshot<T>));
67
71
  });
68
72
  return getSnapshot(target) as unknown as Obj.Snapshot<T>;
69
73
  };
@@ -76,19 +80,6 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
76
80
  }).pipe(Atom.keepAlive);
77
81
  });
78
82
 
79
- /**
80
- * Snapshot a value to create a new reference for comparison and React dependency tracking.
81
- */
82
- const snapshotForComparison = <V>(value: V): V => {
83
- if (Array.isArray(value)) {
84
- return [...value] as V;
85
- }
86
- if (value !== null && typeof value === 'object') {
87
- return { ...value } as V;
88
- }
89
- return value;
90
- };
91
-
92
83
  /**
93
84
  * Atom family for ECHO object properties.
94
85
  * Uses nested families: outer keyed by object, inner keyed by property key.
@@ -149,6 +140,20 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
149
140
  );
150
141
  });
151
142
 
143
+ /**
144
+ * Atom family for a property of a ref's target object.
145
+ * Resolves the ref (reactively) then projects the property atom, so it fires when the ref resolves or when
146
+ * that property changes. Yields `undefined` while the target is unresolved.
147
+ */
148
+ const refPropertyFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>) =>
149
+ Atom.family(<K extends keyof T>(key: K): Atom.Atom<T[K] | undefined> => {
150
+ return Atom.make<T[K] | undefined>((get) => {
151
+ const target = get(refWithReactiveFamily(ref));
152
+ return target ? get(propertyFamily(target)(key)) : undefined;
153
+ }).pipe(Atom.keepAlive);
154
+ }),
155
+ );
156
+
152
157
  /**
153
158
  * Atom family for any ECHO entity (obj or relation) — returns a snapshot.
154
159
  */
@@ -200,10 +205,19 @@ export const makeAtom: {
200
205
  };
201
206
 
202
207
  /**
203
- * Create a read-only atom for a specific property of a reactive object.
204
- * Only fires updates when the property value actually changes.
208
+ * Create a read-only atom for a specific property of a reactive object, or of a ref's target.
209
+ * Only fires updates when the property value actually changes. Given a ref, the atom resolves the target
210
+ * first and yields `undefined` until it loads.
205
211
  */
206
- export const makeProperty = <T extends Obj.Unknown, K extends keyof T>(obj: T, key: K): Atom.Atom<T[K]> => {
212
+ export const makeProperty: {
213
+ <T extends Obj.Unknown, K extends keyof T>(obj: T, key: K): Atom.Atom<T[K]>;
214
+ <T extends Obj.Unknown, K extends keyof T>(ref: Ref.Ref<T>, key: K): Atom.Atom<T[K] | undefined>;
215
+ } = (objOrRef: Obj.Unknown | Ref.Ref<any>, key: any): Atom.Atom<any> => {
216
+ if (isRef(objOrRef)) {
217
+ return refPropertyFamily(objOrRef as Ref.Ref<any>)(key);
218
+ }
219
+
220
+ const obj = objOrRef as Obj.Unknown;
207
221
  assertArgument(isEntity(obj), 'obj', 'Object must be a reactive object');
208
222
  return propertyFamily(obj)(key);
209
223
  };
@@ -242,3 +256,33 @@ export const makeRelation = <T extends Relation.Unknown>(relation: T): Atom.Atom
242
256
  assertArgument(isEntity(relation), 'relation', 'Must be a reactive ECHO relation');
243
257
  return relationFamily(relation);
244
258
  };
259
+
260
+ /**
261
+ * Atom family for an entity's label string.
262
+ * Fires only when the computed label actually changes, not on every entity mutation.
263
+ */
264
+ const labelAtomFamily = Atom.family(<T extends Entity.Unknown>(entity: T): Atom.Atom<string | undefined> => {
265
+ return Atom.make<string | undefined>((get) => {
266
+ let previous = getLabel(entity);
267
+
268
+ const unsubscribe = subscribe(entity, () => {
269
+ const next = getLabel(entity);
270
+ if (next !== previous) {
271
+ previous = next;
272
+ get.setSelf(next);
273
+ }
274
+ });
275
+
276
+ get.addFinalizer(() => unsubscribe());
277
+ return previous;
278
+ }).pipe(Atom.keepAlive);
279
+ });
280
+
281
+ /**
282
+ * Create a read-only atom for the label of a reactive ECHO entity.
283
+ * Re-evaluates on entity mutation; only propagates when the label string changes.
284
+ */
285
+ export const makeLabelAtom = <T extends Entity.Unknown>(entity: T): Atom.Atom<string | undefined> => {
286
+ assertArgument(isEntity(entity), 'entity', 'Must be a reactive ECHO entity');
287
+ return labelAtomFamily(entity);
288
+ };
@@ -3,20 +3,42 @@
3
3
  //
4
4
 
5
5
  import { invariant } from '@dxos/invariant';
6
- import { deepMapValues } from '@dxos/util';
6
+ import { assumeType, deepMapValues } from '@dxos/util';
7
7
 
8
8
  import type * as Obj from '../../Obj';
9
9
  import { makeObject } from '../common/proxy';
10
- import { getSchema, getStaticTypeSchema, getType } from '../common/types';
10
+ import { ParentId, getSchema, getStaticTypeSchema, getType } from '../common/types';
11
11
  import { getMeta } from '../common/types/meta';
12
+ import { type InternalObjectProps } from '../Entity/model';
12
13
  import { Ref } from '../Ref';
13
14
 
14
15
  /**
15
16
  * Clones an object or relation.
16
- * This does not clone referenced objects, only the properties in the object.
17
+ * By default this does not clone referenced objects, only the properties in the object; pass `opts.deep`
18
+ * to recurse into refs (see {@link Obj.CloneOptions}).
17
19
  * @returns A new object with the same schema and properties.
18
20
  */
19
- export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T => {
21
+ export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T =>
22
+ cloneInner(obj, opts, obj, new Map(), new Set());
23
+
24
+ /**
25
+ * Recursive clone worker threaded with two cycle-guard structures:
26
+ * - `cache`: id → already-finished clone; reused when the same object appears more than once in a DAG.
27
+ * - `inProgress`: ids of objects whose clone is currently being computed; a ref back into this set means
28
+ * a true cycle — the original ref is kept rather than recursing forever.
29
+ */
30
+ const cloneInner = <T extends Obj.Any>(
31
+ obj: T,
32
+ opts: Obj.CloneOptions | undefined,
33
+ root: Obj.Any,
34
+ cache: Map<string, Obj.Any>,
35
+ inProgress: Set<string>,
36
+ ): T => {
37
+ if (cache.has(obj.id)) {
38
+ return cache.get(obj.id) as T;
39
+ }
40
+ inProgress.add(obj.id);
41
+
20
42
  const { id, ...data } = obj;
21
43
  // Prefer cloning through the type entity so the cloned instance preserves
22
44
  // `Obj.getType` identity. Falls back to the raw schema for older instances
@@ -24,30 +46,49 @@ export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T =>
24
46
  const typeEntity = getType(obj);
25
47
  const schema = typeEntity != null ? getStaticTypeSchema(typeEntity) : getSchema(obj);
26
48
  invariant(schema != null, 'Object should have a type or schema');
27
- const props: any = deepMapValues(data, (value, recurse) => {
49
+
50
+ const mapRef = (value: unknown, recurse: (value: unknown) => unknown): unknown => {
28
51
  if (Ref.isRef(value)) {
29
- if (opts?.deep) {
30
- // TODO(dmaretskyi): Will break on circular references.
31
- return Ref.make(clone(value.target!, opts));
52
+ const target = value.target;
53
+ // A ref back to an in-progress object means a cycle — keep the original ref to break it.
54
+ if (
55
+ target != null &&
56
+ !inProgress.has(target.id) &&
57
+ (opts?.deep === 'all' || (opts?.deep === 'parent' && isChildOf(target, root)))
58
+ ) {
59
+ return Ref.make(cloneInner(target, opts, root, cache, inProgress));
32
60
  }
33
61
  return value;
34
62
  }
35
63
  return recurse(value);
36
- });
64
+ };
37
65
 
66
+ const props: any = deepMapValues(data, mapRef);
38
67
  if (opts?.retainId) {
39
68
  props.id = id;
40
69
  }
41
- const meta = deepMapValues(getMeta(obj), (value, recurse) => {
42
- if (Ref.isRef(value)) {
43
- if (opts?.deep) {
44
- // TODO(dmaretskyi): Will break on circular references.
45
- return Ref.make(clone(value.target!, opts));
46
- }
47
- return value;
48
- }
49
- return recurse(value);
50
- });
70
+ const meta = deepMapValues(getMeta(obj), mapRef);
71
+ const result = makeObject(schema, props, meta, typeEntity as object | undefined);
72
+
73
+ inProgress.delete(obj.id);
74
+ cache.set(obj.id, result);
75
+ return result;
76
+ };
51
77
 
52
- return makeObject(schema, props, meta, typeEntity as object | undefined);
78
+ /**
79
+ * Whether `entity`'s parent chain reaches `root` — i.e. it is an owned descendant that cascade-deletes with
80
+ * the root. Walks the `ParentId` back-links (a finite tree); guarded against cycles defensively.
81
+ */
82
+ const isChildOf = (entity: unknown, root: Obj.Any): boolean => {
83
+ const seen = new Set<InternalObjectProps>();
84
+ assumeType<InternalObjectProps>(entity);
85
+ let parent = entity[ParentId];
86
+ while (parent != null && !seen.has(parent)) {
87
+ if (parent.id === root.id) {
88
+ return true;
89
+ }
90
+ seen.add(parent);
91
+ parent = parent[ParentId];
92
+ }
93
+ return false;
53
94
  };
@@ -54,13 +54,13 @@ describe('create (static version)', () => {
54
54
 
55
55
  const json = JSON.parse(JSON.stringify(contact));
56
56
  expect(json).toEqual({
57
- id: contact.id,
57
+ 'id': contact.id,
58
58
  '@type': DXN.make(Type.getTypename(TestSchema.Person), Type.getVersion(TestSchema.Person)),
59
59
  '@meta': {
60
60
  keys: [],
61
61
  },
62
- name: 'Bot',
63
- email: 'bot@example.com',
62
+ 'name': 'Bot',
63
+ 'email': 'bot@example.com',
64
64
  });
65
65
  expect(objectToJSON(contact)).toStrictEqual(json);
66
66
  });
@@ -55,10 +55,9 @@ export type CreateObjectProps<T> = (T extends { id: string }
55
55
  *
56
56
  * @example
57
57
  * ```ts
58
- * const Contact = Schema.Struct({
59
- * name: Schema.String,
60
- * email: Schema.String,
61
- * }).pipe(Type.makeObject(DXN.make('com.example.type.person', '0.1.0')))
58
+ * class Contact extends Type.makeObject<Contact>(DXN.make('com.example.type.person', '0.1.0'))(
59
+ * Schema.Struct({ name: Schema.String, email: Schema.String }),
60
+ * ) {}
62
61
  *
63
62
  * const contact = createObject(Contact, {
64
63
  * name: "John",
@@ -7,7 +7,7 @@ import type { InspectOptionsStylized, inspect as inspectFn } from 'node:util';
7
7
  import { type CustomInspectFunction, inspectCustom } from '@dxos/debug';
8
8
 
9
9
  import { getTypeURI } from '../Annotation';
10
- import { ATTR_TYPE, type AnyEntity } from '../common/types';
10
+ import { type AnyEntity, ATTR_TYPE } from '../common/types';
11
11
  import { ATTR_META } from '../common/types/meta';
12
12
  import { MetaId } from '../common/types/model-symbols';
13
13
 
@@ -10,7 +10,7 @@ import { DXN, EID } from '@dxos/keys';
10
10
  import * as Obj from '../../Obj';
11
11
  import { TestSchema } from '../../testing';
12
12
  import * as Type from '../../Type';
13
- import { getTypeURI, getTypename } from '../Annotation';
13
+ import { getTypename, getTypeURI } from '../Annotation';
14
14
  import { getMetaChecked } from '../common/api';
15
15
  import { ATTR_TYPE, EntityKind, KindId, TypeId, getSchema } from '../common/types';
16
16
  import { MetaId } from '../common/types/model-symbols';
@@ -160,10 +160,12 @@ describe('Object JSON serializer', () => {
160
160
  });
161
161
 
162
162
  describe('Uint8Array', () => {
163
- const Blob = Schema.Struct({
164
- name: Schema.String,
165
- bytes: Schema.Uint8ArrayFromSelf,
166
- }).pipe(Type.makeObject(DXN.make('com.example.type.blob', '0.1.0')));
163
+ const Blob = Type.makeObject(DXN.make('com.example.type.blob', '0.1.0'))(
164
+ Schema.Struct({
165
+ name: Schema.String,
166
+ bytes: Schema.Uint8ArrayFromSelf,
167
+ }),
168
+ );
167
169
  type Blob = Type.InstanceType<typeof Blob>;
168
170
 
169
171
  test('round-trips Uint8Array field through JSON with schema', async ({ expect }) => {
@@ -15,9 +15,9 @@ import type * as Obj from '../../Obj';
15
15
  import { getTypeAnnotation, getTypeURI, setTypename } from '../Annotation';
16
16
  import { attachTypedJsonSerializer, defineHiddenProperty, typedJsonSerializer } from '../common/proxy';
17
17
  import {
18
+ type AnyEntity,
18
19
  ATTR_PARENT,
19
20
  ATTR_TYPE,
20
- type AnyEntity,
21
21
  EntityKind,
22
22
  KindId,
23
23
  ParentId,
@@ -129,8 +129,8 @@ export const objectFromJSON = async (
129
129
  const sourceDxn = jsonData[ATTR_RELATION_SOURCE] ?? raise(new TypeError('Missing relation source'));
130
130
  const targetDxn = jsonData[ATTR_RELATION_TARGET] ?? raise(new TypeError('Missing relation target'));
131
131
 
132
- const source = (await refResolver?.resolve(sourceDxn)) as AnyEntity | undefined;
133
- const target = (await refResolver?.resolve(targetDxn)) as AnyEntity | undefined;
132
+ const source = (await refResolver?.resolveLegacy(sourceDxn)) as AnyEntity | undefined;
133
+ const target = (await refResolver?.resolveLegacy(targetDxn)) as AnyEntity | undefined;
134
134
 
135
135
  defineHiddenProperty(obj, KindId, EntityKind.Relation);
136
136
  defineHiddenProperty(obj, RelationSourceDXNId, sourceDxn);
@@ -161,7 +161,7 @@ export const objectFromJSON = async (
161
161
 
162
162
  if (jsonData[ATTR_PARENT]) {
163
163
  const parentDxn = jsonData[ATTR_PARENT];
164
- const resolvedParent = (await refResolver?.resolve(parentDxn)) as Obj.Unknown | undefined;
164
+ const resolvedParent = (await refResolver?.resolveLegacy(parentDxn)) as Obj.Unknown | undefined;
165
165
  defineHiddenProperty(obj, ParentId, resolvedParent);
166
166
  } else if (parent) {
167
167
  defineHiddenProperty(obj, ParentId, parent);
@@ -129,6 +129,19 @@ describe('schema-validator', () => {
129
129
  valueToAssign: { any: 'value' },
130
130
  });
131
131
  });
132
+
133
+ test('walking into an Unknown-typed value does not throw', ({ expect }) => {
134
+ // Mirrors the annotation Dictionary (Record<string, Unknown>): nested keys/indices under an
135
+ // Unknown slot are all valid, so the validator must not reject while walking them.
136
+ const schema = Schema.Struct({ dict: Schema.Record({ key: Schema.String, value: Schema.Unknown }) });
137
+ for (const path of [
138
+ ['dict', 'someType'],
139
+ ['dict', 'someType', 'nested'],
140
+ ['dict', 'someType', '0'],
141
+ ]) {
142
+ SchemaValidator.getPropertySchema(schema, path);
143
+ }
144
+ });
132
145
  test('index signatures', () => {
133
146
  for (const value of [42, '42']) {
134
147
  validateValueToAssign({
@@ -179,4 +192,44 @@ describe('schema-validator', () => {
179
192
  }
180
193
  });
181
194
  });
195
+
196
+ describe('assertExactProperties', () => {
197
+ test('rejects extra properties on a closed struct', () => {
198
+ const Person = Schema.Struct({ name: Schema.String });
199
+ expect(() => SchemaValidator.assertExactProperties(Person, { name: 'Alice', extra: true })).to.throw(
200
+ /Unknown property: extra/,
201
+ );
202
+ });
203
+
204
+ test('allows extra properties when the schema has an index signature', () => {
205
+ const Expando = Schema.Struct({}, { key: Schema.String, value: Schema.Any });
206
+ expect(() => SchemaValidator.assertExactProperties(Expando, { custom: 'value' })).not.to.throw();
207
+ });
208
+
209
+ test('rejects extra nested properties', () => {
210
+ const schema = Schema.Struct({
211
+ nested: Schema.Struct({ field: Schema.Number }),
212
+ });
213
+ expect(() => SchemaValidator.assertExactProperties(schema, { nested: { field: 1, extra: true } })).to.throw(
214
+ /Unknown property: nested.extra/,
215
+ );
216
+ });
217
+
218
+ test('resolves nested discriminated union using target property values', () => {
219
+ const spec = Schema.Union(
220
+ Schema.Struct({ kind: Schema.Literal('feed'), feed: Schema.optional(Schema.String) }),
221
+ Schema.Struct({ kind: Schema.Literal('timer'), cron: Schema.String }),
222
+ );
223
+ const schema = Schema.Struct({ spec });
224
+ const target = { spec: { kind: 'feed' as const, feed: 'echo:/feed', extra: true } };
225
+ expect(() => SchemaValidator.assertExactProperties(schema, target, (path) => getDeep(target, path))).to.throw(
226
+ /Unknown property: spec.extra/,
227
+ );
228
+ expect(() =>
229
+ SchemaValidator.assertExactProperties(schema, { spec: { kind: 'feed' as const } }, (path) =>
230
+ getDeep({ spec: { kind: 'feed' as const } }, path),
231
+ ),
232
+ ).not.to.throw();
233
+ });
234
+ });
182
235
  });
@@ -59,10 +59,12 @@ describe('Obj.setValue', () => {
59
59
  value: Schema.optional(Schema.Number),
60
60
  });
61
61
 
62
- const Container = Schema.Struct({
63
- name: Schema.String,
64
- items: Schema.optional(Schema.Array(Item)),
65
- }).pipe(Type.makeObject(DXN.make('com.test.type.container', '0.1.0')));
62
+ const Container = Type.makeObject(DXN.make('com.test.type.container', '0.1.0'))(
63
+ Schema.Struct({
64
+ name: Schema.String,
65
+ items: Schema.optional(Schema.Array(Item)),
66
+ }),
67
+ );
66
68
 
67
69
  const container = Obj.make(Container, { name: 'box' });
68
70
 
@@ -146,9 +148,11 @@ describe('Obj.setValue', () => {
146
148
  });
147
149
 
148
150
  test('handles two-dimensional arrays', ({ expect }) => {
149
- const Matrix = Schema.Struct({
150
- values: Schema.optional(Schema.Array(Schema.Array(Schema.Number))),
151
- }).pipe(Type.makeObject(DXN.make('com.test.type.matrix', '0.1.0')));
151
+ const Matrix = Type.makeObject(DXN.make('com.test.type.matrix', '0.1.0'))(
152
+ Schema.Struct({
153
+ values: Schema.optional(Schema.Array(Schema.Array(Schema.Number))),
154
+ }),
155
+ );
152
156
 
153
157
  const matrix = Obj.make(Matrix, {});
154
158
 
@@ -194,10 +198,12 @@ describe('Obj.setValue', () => {
194
198
  value: Schema.optional(Schema.Number),
195
199
  });
196
200
 
197
- const Container = Schema.Struct({
198
- name: Schema.String,
199
- items: Schema.optional(Schema.Array(Item)),
200
- }).pipe(Type.makeObject(DXN.make('com.test.type.container', '0.1.0')));
201
+ const Container = Type.makeObject(DXN.make('com.test.type.container', '0.1.0'))(
202
+ Schema.Struct({
203
+ name: Schema.String,
204
+ items: Schema.optional(Schema.Array(Item)),
205
+ }),
206
+ );
201
207
 
202
208
  const container = Obj.make(Container, { name: 'box' });
203
209
 
@@ -216,10 +222,12 @@ describe('Obj.setValue', () => {
216
222
  completed: Schema.optional(Schema.Boolean),
217
223
  });
218
224
 
219
- const TodoList = Schema.Struct({
220
- name: Schema.String,
221
- tasks: Schema.optional(Schema.Array(Task)),
222
- }).pipe(Type.makeObject(DXN.make('com.test.type.todoList', '0.1.0')));
225
+ const TodoList = Type.makeObject(DXN.make('com.test.type.todoList', '0.1.0'))(
226
+ Schema.Struct({
227
+ name: Schema.String,
228
+ tasks: Schema.optional(Schema.Array(Task)),
229
+ }),
230
+ );
223
231
 
224
232
  const todoList = Obj.make(TodoList, { name: 'My Tasks' });
225
233
 
@@ -240,10 +248,12 @@ describe('Obj.setValue', () => {
240
248
  label: Schema.optional(Schema.String), // Optional - should not be initialized
241
249
  });
242
250
 
243
- const Container = Schema.Struct({
244
- name: Schema.String,
245
- items: Schema.optional(Schema.Array(Item)),
246
- }).pipe(Type.makeObject(DXN.make('com.test.type.container', '0.1.0')));
251
+ const Container = Type.makeObject(DXN.make('com.test.type.container', '0.1.0'))(
252
+ Schema.Struct({
253
+ name: Schema.String,
254
+ items: Schema.optional(Schema.Array(Item)),
255
+ }),
256
+ );
247
257
 
248
258
  const container = Obj.make(Container, { name: 'box' });
249
259
 
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './pretty';
6
+ export * from './query-result-effect';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Effectable from 'effect/Effectable';
7
+ import * as Option from 'effect/Option';
8
+
9
+ import { EffectEx } from '@dxos/effect';
10
+
11
+ import type * as QueryResult from '../../QueryResult';
12
+
13
+ /**
14
+ * Wraps an effect that produces a {@link QueryResult.QueryResult} into a
15
+ * {@link QueryResult.QueryResultEffect}, exposing `.run` and `.first` shorthands that execute the
16
+ * query once. Shared by `Database.query` and `Feed.query` so both surfaces chain identically.
17
+ */
18
+ export const makeQueryResultEffect = <T, E, R>(
19
+ eff: Effect.Effect<QueryResult.QueryResult<T>, E, R>,
20
+ ): QueryResult.QueryResultEffect<T, E, R> => {
21
+ return {
22
+ run: Effect.flatMap(eff, (result) => EffectEx.promiseWithCauseCapture(() => result.run())),
23
+ first: Effect.flatMap(eff, (result) =>
24
+ EffectEx.promiseWithCauseCapture(async () => Option.fromNullable(await result.firstOrUndefined())),
25
+ ),
26
+
27
+ // Effect internals: the result is itself an Effect, so it carries the commit prototype.
28
+ ...Effectable.CommitPrototype,
29
+ commit() {
30
+ return eff;
31
+ },
32
+ // Cast required: Effect's commit protocol is supplied at runtime via `CommitPrototype` and
33
+ // cannot be expressed by the object literal's static type.
34
+ } as any;
35
+ };
@@ -4,17 +4,37 @@
4
4
 
5
5
  import * as Atom from '@effect-atom/atom/Atom';
6
6
 
7
+ import { subscribe } from '../common/proxy/reactive';
8
+ import { ObjectDeletedId } from '../common/types/model-symbols';
7
9
  import type { Ref } from './ref';
8
10
  import { loadRefTarget } from './utils';
9
11
 
10
12
  /**
11
13
  * Atom family for ECHO refs.
12
14
  * Uses ref reference as key — same ref returns same atom.
13
- * This atom only updates once when the ref loads it does not subscribe to target object changes.
14
- * Use `Obj.atom(ref)` if you need reactive snapshots of ECHO objects via a ref.
15
+ * Subscribes to target object changes and resolves to undefined when the target is deleted.
15
16
  */
16
17
  export const refSimpleFamily = Atom.family(<T>(ref: Ref<T>): Atom.Atom<T | undefined> => {
17
18
  return Atom.make<T | undefined>((get) => {
18
- return loadRefTarget(ref, get, (target) => target);
19
+ let unsubscribeTarget: (() => void) | undefined;
20
+
21
+ const setupSubscription = (target: T): T | undefined => {
22
+ // Release any previous subscription before re-subscribing (loadRefTarget may call this more than once).
23
+ // T has no ECHO-proxy constraint at this generic level; `subscribe` and ObjectDeletedId
24
+ // both require the internal proxy shape that cannot be expressed statically here.
25
+ unsubscribeTarget?.();
26
+ unsubscribeTarget = subscribe(target as any, () => {
27
+ const deleted = !!(target as any)[ObjectDeletedId];
28
+ get.setSelf(deleted ? undefined : target);
29
+ });
30
+ const deleted = !!(target as any)[ObjectDeletedId];
31
+ return deleted ? undefined : target;
32
+ };
33
+
34
+ get.addFinalizer(() => {
35
+ unsubscribeTarget?.();
36
+ });
37
+
38
+ return loadRefTarget(ref, get, setupSubscription);
19
39
  });
20
40
  });
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './ref';
6
6
  export * from './ref-array';
7
+ export * from './strong-deps';
@@ -5,7 +5,7 @@
5
5
  import * as Schema from 'effect/Schema';
6
6
  import { describe, expect, test } from 'vitest';
7
7
 
8
- import { EID, EntityId, DXN } from '@dxos/keys';
8
+ import { DXN, EID, EntityId } from '@dxos/keys';
9
9
 
10
10
  import * as Type from '../../Type';
11
11
  import { EchoObjectSchema, getObjectEchoUri } from '../Entity';
@@ -45,16 +45,16 @@ describe('Ref', () => {
45
45
 
46
46
  const json = JSON.parse(JSON.stringify(contact));
47
47
  expect(json).toEqual({
48
- id: contact.id,
48
+ 'id': contact.id,
49
49
  '@type': `dxn:${Type.getTypename(Contact)}:${Type.getVersion(Contact)}`,
50
50
  '@meta': {
51
51
  keys: [],
52
52
  },
53
- name: 'John Doe',
54
- tasks: [
53
+ 'name': 'John Doe',
54
+ 'tasks': [
55
55
  {
56
56
  '/': getObjectEchoUri(task)!.toString(),
57
- target: JSON.parse(JSON.stringify(task)),
57
+ 'target': JSON.parse(JSON.stringify(task)),
58
58
  },
59
59
  ],
60
60
  });
@@ -66,13 +66,13 @@ describe('Ref', () => {
66
66
 
67
67
  const json = JSON.parse(JSON.stringify(contact));
68
68
  expect(json).toEqual({
69
- id: contact.id,
69
+ 'id': contact.id,
70
70
  '@type': `dxn:${Type.getTypename(Contact)}:${Type.getVersion(Contact)}`,
71
71
  '@meta': {
72
72
  keys: [],
73
73
  },
74
- name: 'John Doe',
75
- tasks: [{ '/': getObjectEchoUri(task)!.toString() }],
74
+ 'name': 'John Doe',
75
+ 'tasks': [{ '/': getObjectEchoUri(task)!.toString() }],
76
76
  });
77
77
  });
78
78