@dxos/echo 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (223) hide show
  1. package/dist/lib/neutral/Annotation.mjs +3 -3
  2. package/dist/lib/neutral/Database.mjs +6 -4
  3. package/dist/lib/neutral/Entity.mjs +16 -14
  4. package/dist/lib/neutral/Err.mjs +1 -1
  5. package/dist/lib/neutral/Extension.mjs +1 -1
  6. package/dist/lib/neutral/Feed.mjs +19 -17
  7. package/dist/lib/neutral/Filter.mjs +11 -11
  8. package/dist/lib/neutral/Format.mjs +3 -3
  9. package/dist/lib/neutral/JsonSchema.mjs +8 -8
  10. package/dist/lib/neutral/Key.mjs +1 -1
  11. package/dist/lib/neutral/Migration.mjs +17 -0
  12. package/dist/lib/neutral/Migration.mjs.map +7 -0
  13. package/dist/lib/neutral/Obj.mjs +14 -14
  14. package/dist/lib/neutral/Order.mjs +1 -1
  15. package/dist/lib/neutral/Query.mjs +17 -17
  16. package/dist/lib/neutral/QueryResult.mjs +1 -1
  17. package/dist/lib/neutral/Ref.mjs +7 -7
  18. package/dist/lib/neutral/Relation.mjs +15 -15
  19. package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
  20. package/dist/lib/neutral/Tag.mjs +14 -14
  21. package/dist/lib/neutral/Type.mjs +10 -10
  22. package/dist/lib/neutral/{chunk-7SQD3FRZ.mjs → chunk-2T22UGGN.mjs} +59 -12
  23. package/dist/lib/neutral/chunk-2T22UGGN.mjs.map +7 -0
  24. package/dist/lib/neutral/{chunk-GZQTCRJB.mjs → chunk-44HT3MEC.mjs} +2 -2
  25. package/dist/lib/neutral/{chunk-WVLOCXB5.mjs → chunk-6VC3FI5E.mjs} +12 -8
  26. package/dist/lib/neutral/chunk-6VC3FI5E.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-HBJ7JT5A.mjs → chunk-7JFW72MX.mjs} +17 -5
  28. package/dist/lib/neutral/chunk-7JFW72MX.mjs.map +7 -0
  29. package/dist/lib/neutral/{chunk-ANHVGJI4.mjs → chunk-7RVZT53K.mjs} +1 -1
  30. package/dist/lib/neutral/{chunk-BNCCGLJN.mjs → chunk-BICZKPQG.mjs} +1 -1
  31. package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs +36 -0
  32. package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs.map +7 -0
  33. package/dist/lib/neutral/{chunk-OLFCVPOO.mjs → chunk-DUNXPKAC.mjs} +4 -4
  34. package/dist/lib/neutral/{chunk-R72KFH2X.mjs → chunk-FAW7PJRO.mjs} +2 -2
  35. package/dist/lib/neutral/{chunk-E5PBQJWV.mjs → chunk-FAYW32CW.mjs} +2 -2
  36. package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs → chunk-GWFFC34K.mjs} +1 -1
  37. package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs.map → chunk-GWFFC34K.mjs.map} +1 -1
  38. package/dist/lib/neutral/{chunk-T2JOLN37.mjs → chunk-I2MFJ76N.mjs} +6 -6
  39. package/dist/lib/neutral/chunk-I2MFJ76N.mjs.map +7 -0
  40. package/dist/lib/neutral/{chunk-6URFBQJH.mjs → chunk-JALF2CVV.mjs} +5 -21
  41. package/dist/lib/neutral/chunk-JALF2CVV.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-EBVB5NOH.mjs → chunk-KQUQZ3CB.mjs} +15 -20
  43. package/dist/lib/neutral/chunk-KQUQZ3CB.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-ZGVZNBBJ.mjs → chunk-LOTZLYHB.mjs} +17 -12
  45. package/dist/lib/neutral/chunk-LOTZLYHB.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-TBKX6JQO.mjs → chunk-N4B7FHQT.mjs} +1 -1
  47. package/dist/lib/neutral/{chunk-UPWIIW2V.mjs → chunk-NKXEKBP5.mjs} +6 -22
  48. package/dist/lib/neutral/{chunk-UPWIIW2V.mjs.map → chunk-NKXEKBP5.mjs.map} +2 -2
  49. package/dist/lib/neutral/{chunk-YSLSJ7QS.mjs → chunk-NSMLBSFS.mjs} +17 -45
  50. package/dist/lib/neutral/chunk-NSMLBSFS.mjs.map +7 -0
  51. package/dist/lib/neutral/{chunk-ZIXGDU6F.mjs → chunk-QBIGOSRF.mjs} +2 -2
  52. package/dist/lib/neutral/{chunk-FNEFSO2C.mjs → chunk-QBLYZ4IV.mjs} +12 -65
  53. package/dist/lib/neutral/{chunk-FNEFSO2C.mjs.map → chunk-QBLYZ4IV.mjs.map} +2 -2
  54. package/dist/lib/neutral/{chunk-5VKHCUDA.mjs → chunk-QEVM3JUP.mjs} +26 -7
  55. package/dist/lib/neutral/chunk-QEVM3JUP.mjs.map +7 -0
  56. package/dist/lib/neutral/{chunk-QWAOTFCY.mjs → chunk-REP7WWAQ.mjs} +16 -66
  57. package/dist/lib/neutral/chunk-REP7WWAQ.mjs.map +7 -0
  58. package/dist/lib/neutral/{chunk-DQYLD2RB.mjs → chunk-TRPZU2HV.mjs} +2 -2
  59. package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
  60. package/dist/lib/neutral/{chunk-46QNGNTY.mjs → chunk-TW76K7H5.mjs} +3 -3
  61. package/dist/lib/neutral/{chunk-FW7UJX25.mjs → chunk-UYJYDSD7.mjs} +67 -465
  62. package/dist/lib/neutral/chunk-UYJYDSD7.mjs.map +7 -0
  63. package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
  64. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs → chunk-ZISMEVKD.mjs} +1 -1
  65. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs.map → chunk-ZISMEVKD.mjs.map} +2 -2
  66. package/dist/lib/neutral/index.mjs +33 -27
  67. package/dist/lib/neutral/internal/index.mjs +9 -9
  68. package/dist/lib/neutral/meta.json +1 -1
  69. package/dist/lib/neutral/testing/index.mjs +28 -27
  70. package/dist/lib/neutral/testing/index.mjs.map +1 -1
  71. package/dist/types/src/Collection.d.ts.map +1 -1
  72. package/dist/types/src/Database.d.ts +5 -0
  73. package/dist/types/src/Database.d.ts.map +1 -1
  74. package/dist/types/src/Dataset.d.ts +1 -1
  75. package/dist/types/src/Entity.d.ts +15 -9
  76. package/dist/types/src/Entity.d.ts.map +1 -1
  77. package/dist/types/src/Err.d.ts +18 -18
  78. package/dist/types/src/Err.d.ts.map +1 -1
  79. package/dist/types/src/Extension.d.ts +4 -4
  80. package/dist/types/src/Extension.d.ts.map +1 -1
  81. package/dist/types/src/Feed.d.ts +12 -1
  82. package/dist/types/src/Feed.d.ts.map +1 -1
  83. package/dist/types/src/Filter.d.ts +5 -3
  84. package/dist/types/src/Filter.d.ts.map +1 -1
  85. package/dist/types/src/Json.d.ts +33 -0
  86. package/dist/types/src/Json.d.ts.map +1 -0
  87. package/dist/types/src/Json.test.d.ts +2 -0
  88. package/dist/types/src/Json.test.d.ts.map +1 -0
  89. package/dist/types/src/JsonSchema.d.ts +1 -1
  90. package/dist/types/src/Migration.d.ts +57 -0
  91. package/dist/types/src/Migration.d.ts.map +1 -0
  92. package/dist/types/src/Obj.d.ts +22 -21
  93. package/dist/types/src/Obj.d.ts.map +1 -1
  94. package/dist/types/src/Order.d.ts.map +1 -1
  95. package/dist/types/src/Query.d.ts +5 -1
  96. package/dist/types/src/Query.d.ts.map +1 -1
  97. package/dist/types/src/Ref.d.ts.map +1 -1
  98. package/dist/types/src/Relation.d.ts +15 -15
  99. package/dist/types/src/Relation.d.ts.map +1 -1
  100. package/dist/types/src/Tag.d.ts +2 -2
  101. package/dist/types/src/Tag.d.ts.map +1 -1
  102. package/dist/types/src/Type.d.ts.map +1 -1
  103. package/dist/types/src/View.d.ts +1 -1
  104. package/dist/types/src/View.d.ts.map +1 -1
  105. package/dist/types/src/index.d.ts +2 -0
  106. package/dist/types/src/index.d.ts.map +1 -1
  107. package/dist/types/src/internal/Annotation/annotations.d.ts +2 -2
  108. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  109. package/dist/types/src/internal/Annotation/sorting.d.ts.map +1 -1
  110. package/dist/types/src/internal/Annotation/util.d.ts +1 -1
  111. package/dist/types/src/internal/Annotation/util.d.ts.map +1 -1
  112. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  113. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  114. package/dist/types/src/internal/Entity/version.d.ts.map +1 -1
  115. package/dist/types/src/internal/Format/date.d.ts.map +1 -1
  116. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  117. package/dist/types/src/internal/Format/number.d.ts.map +1 -1
  118. package/dist/types/src/internal/Format/object.d.ts.map +1 -1
  119. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  120. package/dist/types/src/internal/JsonSchema/json-schema-normalize.d.ts.map +1 -1
  121. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +28 -28
  122. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
  123. package/dist/types/src/internal/JsonSchema/json-schema.d.ts +1 -1
  124. package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
  125. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  126. package/dist/types/src/internal/Obj/common.d.ts.map +1 -1
  127. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  128. package/dist/types/src/internal/Obj/deleted.d.ts.map +1 -1
  129. package/dist/types/src/internal/Obj/ids.d.ts.map +1 -1
  130. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  131. package/dist/types/src/internal/Obj/set-value.d.ts +1 -1
  132. package/dist/types/src/internal/Obj/set-value.d.ts.map +1 -1
  133. package/dist/types/src/internal/Obj/snapshot.d.ts.map +1 -1
  134. package/dist/types/src/internal/Query.d.ts.map +1 -1
  135. package/dist/types/src/internal/Ref/ref.d.ts +13 -0
  136. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  137. package/dist/types/src/internal/Type/compose.d.ts.map +1 -1
  138. package/dist/types/src/internal/Type/echo-schema.d.ts +1 -1
  139. package/dist/types/src/internal/Type/echo-schema.d.ts.map +1 -1
  140. package/dist/types/src/internal/Type/manipulation.d.ts.map +1 -1
  141. package/dist/types/src/internal/common/api/meta.d.ts +3 -3
  142. package/dist/types/src/internal/common/api/meta.d.ts.map +1 -1
  143. package/dist/types/src/internal/common/proxy/change-context.d.ts +1 -1
  144. package/dist/types/src/internal/common/proxy/change-context.d.ts.map +1 -1
  145. package/dist/types/src/internal/common/proxy/define-hidden-property.d.ts.map +1 -1
  146. package/dist/types/src/internal/common/proxy/errors.d.ts +1 -1
  147. package/dist/types/src/internal/common/proxy/errors.d.ts.map +1 -1
  148. package/dist/types/src/internal/common/proxy/event-batch.d.ts.map +1 -1
  149. package/dist/types/src/internal/common/proxy/json-serializer.d.ts.map +1 -1
  150. package/dist/types/src/internal/common/proxy/ownership.d.ts.map +1 -1
  151. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  152. package/dist/types/src/internal/common/proxy/reactive-array.d.ts +1 -1
  153. package/dist/types/src/internal/common/proxy/reactive.d.ts +1 -1
  154. package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
  155. package/dist/types/src/internal/common/proxy/reactive.test.d.ts +2 -0
  156. package/dist/types/src/internal/common/proxy/reactive.test.d.ts.map +1 -0
  157. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  158. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  159. package/dist/types/src/internal/common/types/base.d.ts.map +1 -1
  160. package/dist/types/src/internal/common/types/entity.d.ts +3 -3
  161. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  162. package/dist/types/src/internal/common/types/version.d.ts +1 -1
  163. package/dist/types/src/testing/test-data.d.ts.map +1 -1
  164. package/dist/types/src/testing/test-schema.d.ts +53 -53
  165. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  166. package/dist/types/src/testing/util.d.ts.map +1 -1
  167. package/dist/types/tsconfig.tsbuildinfo +1 -1
  168. package/package.json +18 -13
  169. package/src/Collection.ts +1 -1
  170. package/src/Database.ts +35 -13
  171. package/src/Entity.ts +16 -9
  172. package/src/Extension.ts +3 -3
  173. package/src/Feed.ts +22 -1
  174. package/src/Filter.ts +9 -5
  175. package/src/Json.test.ts +175 -0
  176. package/src/Json.ts +102 -0
  177. package/src/Migration.ts +94 -0
  178. package/src/Obj.test.ts +12 -12
  179. package/src/Obj.ts +27 -24
  180. package/src/Query.test.ts +44 -11
  181. package/src/Query.ts +20 -0
  182. package/src/Relation.ts +21 -17
  183. package/src/index.ts +3 -0
  184. package/src/internal/Annotation/annotations.ts +5 -6
  185. package/src/internal/Obj/json-serializer.test.ts +1 -1
  186. package/src/internal/Obj/set-value.test.ts +15 -15
  187. package/src/internal/Obj/set-value.ts +1 -1
  188. package/src/internal/Query.ts +3 -0
  189. package/src/internal/Ref/ref.ts +17 -0
  190. package/src/internal/Type/echo-schema.ts +1 -1
  191. package/src/internal/common/README.md +1 -1
  192. package/src/internal/common/api/meta.ts +3 -3
  193. package/src/internal/common/proxy/change-context.ts +1 -1
  194. package/src/internal/common/proxy/change.test.ts +59 -59
  195. package/src/internal/common/proxy/errors.ts +2 -2
  196. package/src/internal/common/proxy/reactive-array.ts +1 -1
  197. package/src/internal/common/proxy/reactive.test.ts +54 -0
  198. package/src/internal/common/proxy/reactive.ts +11 -2
  199. package/src/internal/common/proxy/typed-handler.ts +7 -7
  200. package/src/internal/common/proxy/typed-object.test.ts +1 -1
  201. package/dist/lib/neutral/chunk-5VKHCUDA.mjs.map +0 -7
  202. package/dist/lib/neutral/chunk-6URFBQJH.mjs.map +0 -7
  203. package/dist/lib/neutral/chunk-7SQD3FRZ.mjs.map +0 -7
  204. package/dist/lib/neutral/chunk-EBVB5NOH.mjs.map +0 -7
  205. package/dist/lib/neutral/chunk-FW7UJX25.mjs.map +0 -7
  206. package/dist/lib/neutral/chunk-HBJ7JT5A.mjs.map +0 -7
  207. package/dist/lib/neutral/chunk-QWAOTFCY.mjs.map +0 -7
  208. package/dist/lib/neutral/chunk-T2JOLN37.mjs.map +0 -7
  209. package/dist/lib/neutral/chunk-WVLOCXB5.mjs.map +0 -7
  210. package/dist/lib/neutral/chunk-YSLSJ7QS.mjs.map +0 -7
  211. package/dist/lib/neutral/chunk-ZGVZNBBJ.mjs.map +0 -7
  212. /package/dist/lib/neutral/{chunk-GZQTCRJB.mjs.map → chunk-44HT3MEC.mjs.map} +0 -0
  213. /package/dist/lib/neutral/{chunk-ANHVGJI4.mjs.map → chunk-7RVZT53K.mjs.map} +0 -0
  214. /package/dist/lib/neutral/{chunk-BNCCGLJN.mjs.map → chunk-BICZKPQG.mjs.map} +0 -0
  215. /package/dist/lib/neutral/{chunk-OLFCVPOO.mjs.map → chunk-DUNXPKAC.mjs.map} +0 -0
  216. /package/dist/lib/neutral/{chunk-R72KFH2X.mjs.map → chunk-FAW7PJRO.mjs.map} +0 -0
  217. /package/dist/lib/neutral/{chunk-E5PBQJWV.mjs.map → chunk-FAYW32CW.mjs.map} +0 -0
  218. /package/dist/lib/neutral/{chunk-TBKX6JQO.mjs.map → chunk-N4B7FHQT.mjs.map} +0 -0
  219. /package/dist/lib/neutral/{chunk-ZIXGDU6F.mjs.map → chunk-QBIGOSRF.mjs.map} +0 -0
  220. /package/dist/lib/neutral/{chunk-DQYLD2RB.mjs.map → chunk-TRPZU2HV.mjs.map} +0 -0
  221. /package/dist/lib/neutral/{chunk-UI6MWK5W.mjs.map → chunk-TTCSATUD.mjs.map} +0 -0
  222. /package/dist/lib/neutral/{chunk-46QNGNTY.mjs.map → chunk-TW76K7H5.mjs.map} +0 -0
  223. /package/dist/lib/neutral/{chunk-OMUPQMLR.mjs.map → chunk-V72DY6LU.mjs.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo",
3
- "version": "0.8.4-main.9be5663bfe",
3
+ "version": "0.8.4-main.abd8ff62ef",
4
4
  "description": "ECHO API",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -73,6 +73,11 @@
73
73
  "types": "./dist/types/src/Key.d.ts",
74
74
  "default": "./dist/lib/neutral/Key.mjs"
75
75
  },
76
+ "./Migration": {
77
+ "source": "./src/Migration.ts",
78
+ "types": "./dist/types/src/Migration.d.ts",
79
+ "default": "./dist/lib/neutral/Migration.mjs"
80
+ },
76
81
  "./Obj": {
77
82
  "source": "./src/Obj.ts",
78
83
  "types": "./dist/types/src/Obj.d.ts",
@@ -130,18 +135,18 @@
130
135
  ],
131
136
  "dependencies": {
132
137
  "effect": "3.20.0",
133
- "@dxos/async": "0.8.4-main.9be5663bfe",
134
- "@dxos/context": "0.8.4-main.9be5663bfe",
135
- "@dxos/echo-protocol": "0.8.4-main.9be5663bfe",
136
- "@dxos/debug": "0.8.4-main.9be5663bfe",
137
- "@dxos/errors": "0.8.4-main.9be5663bfe",
138
- "@dxos/invariant": "0.8.4-main.9be5663bfe",
139
- "@dxos/log": "0.8.4-main.9be5663bfe",
140
- "@dxos/node-std": "0.8.4-main.9be5663bfe",
141
- "@dxos/protocols": "0.8.4-main.9be5663bfe",
142
- "@dxos/effect": "0.8.4-main.9be5663bfe",
143
- "@dxos/keys": "0.8.4-main.9be5663bfe",
144
- "@dxos/util": "0.8.4-main.9be5663bfe"
138
+ "@dxos/async": "0.8.4-main.abd8ff62ef",
139
+ "@dxos/echo-protocol": "0.8.4-main.abd8ff62ef",
140
+ "@dxos/debug": "0.8.4-main.abd8ff62ef",
141
+ "@dxos/effect": "0.8.4-main.abd8ff62ef",
142
+ "@dxos/errors": "0.8.4-main.abd8ff62ef",
143
+ "@dxos/invariant": "0.8.4-main.abd8ff62ef",
144
+ "@dxos/log": "0.8.4-main.abd8ff62ef",
145
+ "@dxos/keys": "0.8.4-main.abd8ff62ef",
146
+ "@dxos/node-std": "0.8.4-main.abd8ff62ef",
147
+ "@dxos/util": "0.8.4-main.abd8ff62ef",
148
+ "@dxos/protocols": "0.8.4-main.abd8ff62ef",
149
+ "@dxos/context": "0.8.4-main.abd8ff62ef"
145
150
  },
146
151
  "publishConfig": {
147
152
  "access": "public"
package/src/Collection.ts CHANGED
@@ -25,7 +25,7 @@ export const Collection = Schema.Struct({
25
25
  }),
26
26
  Annotation.IconAnnotation.set({
27
27
  icon: 'ph--folder--regular',
28
- hue: 'neutral',
28
+ hue: 'amber',
29
29
  }),
30
30
  );
31
31
 
package/src/Database.ts CHANGED
@@ -229,24 +229,28 @@ export const resolve: {
229
229
  }
230
230
  invariant(!schema || isInstanceOf(schema, object), 'Object type mismatch.');
231
231
  return object as any;
232
- })) as any;
232
+ }).pipe(Effect.withSpan('Database.resolve'))) as any;
233
233
 
234
234
  /**
235
235
  * Loads an object reference.
236
236
  */
237
- export const load: <T>(ref: Ref<T>) => Effect.Effect<T, Err.ObjectNotFoundError, never> = Effect.fn(function* (ref) {
238
- const object = yield* promiseWithCauseCapture(() => ref.tryLoad());
239
- if (!object) {
240
- return yield* Effect.fail(new Err.ObjectNotFoundError(ref.dxn));
241
- }
242
- return object;
243
- });
237
+ export const load: <T>(ref: Ref<T>) => Effect.Effect<T, Err.ObjectNotFoundError, never> = Effect.fn('Database.load')(
238
+ function* (ref) {
239
+ const object = yield* promiseWithCauseCapture(() => ref.tryLoad());
240
+ if (!object) {
241
+ return yield* Effect.fail(new Err.ObjectNotFoundError(ref.dxn));
242
+ }
243
+ return object;
244
+ },
245
+ );
244
246
 
245
247
  /**
246
248
  * Loads an object reference option.
247
249
  */
248
250
  // TODO(dmaretskyi): Do we need this -- you can just use `Effect.catchTag` in calling code instead.
249
- export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, never, never> = Effect.fn(function* (ref) {
251
+ export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, never, never> = Effect.fn(
252
+ 'Database.loadOption',
253
+ )(function* (ref) {
250
254
  const object = yield* load(ref).pipe(Effect.catchTag('ObjectNotFoundError', () => Effect.succeed(undefined)));
251
255
 
252
256
  return Option.fromNullable(object);
@@ -257,21 +261,23 @@ export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, nev
257
261
  * @see {@link Database.add}
258
262
  */
259
263
  export const add = <T extends Entity.Unknown>(obj: T): Effect.Effect<T, never, Service> =>
260
- Service.pipe(Effect.map(({ db }) => db.add(obj)));
264
+ Service.pipe(Effect.map(({ db }) => db.add(obj))).pipe(Effect.withSpan('Database.add'));
261
265
 
262
266
  /**
263
267
  * Removes an object from the database.
264
268
  * @see {@link Database.remove}
265
269
  */
266
270
  export const remove = <T extends Entity.Unknown>(obj: T): Effect.Effect<void, never, Service> =>
267
- Service.pipe(Effect.map(({ db }) => db.remove(obj)));
271
+ Service.pipe(Effect.map(({ db }) => db.remove(obj))).pipe(Effect.withSpan('Database.remove'));
268
272
 
269
273
  /**
270
274
  * Flushes pending changes to disk.
271
275
  * @see {@link Database.flush}
272
276
  */
273
277
  export const flush = (opts?: FlushOptions) =>
274
- Service.pipe(Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.flush(opts))));
278
+ Service.pipe(Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.flush(opts)))).pipe(
279
+ Effect.withSpan('Database.flush'),
280
+ );
275
281
 
276
282
  /**
277
283
  * Creates a `QueryResult` object that can be subscribed to.
@@ -292,7 +298,10 @@ export const runQuery: {
292
298
  <Q extends Query.Any>(query: Q): Effect.Effect<Query.Type<Q>[], never, Service>;
293
299
  <F extends Filter.Any>(filter: F): Effect.Effect<Filter.Type<F>[], never, Service>;
294
300
  } = (queryOrFilter: Query.Any | Filter.Any) =>
295
- query(queryOrFilter as any).pipe(Effect.flatMap((queryResult) => promiseWithCauseCapture(() => queryResult.run())));
301
+ query(queryOrFilter as any).pipe(
302
+ Effect.flatMap((queryResult) => promiseWithCauseCapture(() => queryResult.run())),
303
+ Effect.withSpan('Database.runQuery'),
304
+ );
296
305
 
297
306
  /**
298
307
  * Executes the query once and returns the first result as or None.
@@ -305,6 +314,19 @@ export const runQueryFirst: {
305
314
  Effect.flatMap((queryResult) =>
306
315
  promiseWithCauseCapture(async () => Option.fromNullable(await queryResult.firstOrUndefined())),
307
316
  ),
317
+ Effect.withSpan('Database.runQueryFirst'),
318
+ );
319
+
320
+ /**
321
+ * Persists schemas in the database so they replicate to other clients.
322
+ * @see {@link SchemaRegistry.SchemaRegistry.register}
323
+ */
324
+ export const registerSchema = (
325
+ input: SchemaRegistry.RegisterSchemaInput[],
326
+ ): Effect.Effect<Type.RuntimeType[], never, Service> =>
327
+ Service.pipe(
328
+ Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.schemaRegistry.register(input))),
329
+ Effect.withSpan('Database.registerSchema'),
308
330
  );
309
331
 
310
332
  /**
package/src/Entity.ts CHANGED
@@ -9,6 +9,7 @@ import type { DXN, ObjectId } from '@dxos/keys';
9
9
 
10
10
  import * as internal from './internal';
11
11
  import type * as Relation from './Relation';
12
+ import type * as Type from './Type';
12
13
 
13
14
  // Re-export KindId and SnapshotKindId from internal.
14
15
  export const KindId = internal.KindId;
@@ -131,6 +132,12 @@ export const getDXN = (entity: Unknown | Snapshot): DXN => internal.getDXN(entit
131
132
  */
132
133
  export const getTypeDXN = internal.getTypeDXN;
133
134
 
135
+ /**
136
+ * Get the schema of an entity.
137
+ * Returns the branded ECHO schema used to create the entity.
138
+ */
139
+ export const getSchema: (entity: Unknown | Snapshot) => Type.AnyEntity | undefined = internal.getSchema as any;
140
+
134
141
  /**
135
142
  * Get the typename of an entity's type.
136
143
  */
@@ -191,7 +198,7 @@ export const subscribe = (entity: Unknown, callback: () => void): (() => void) =
191
198
  //
192
199
 
193
200
  /**
194
- * Used to provide a mutable view of an entity within `Entity.change`.
201
+ * Used to provide a mutable view of an entity within `Entity.update`.
195
202
  */
196
203
  export type Mutable<T> = internal.Mutable<T>;
197
204
 
@@ -199,7 +206,7 @@ export type Mutable<T> = internal.Mutable<T>;
199
206
  * Perform mutations on an entity (object or relation) within a change context.
200
207
  *
201
208
  * Entities are read-only by default. Mutations are batched and notifications fire
202
- * when the callback completes. Direct mutations outside of `Entity.change` will throw
209
+ * when the callback completes. Direct mutations outside of `Entity.update` will throw
203
210
  * at runtime.
204
211
  *
205
212
  * @param entity - The echo entity (object or relation) to mutate.
@@ -207,30 +214,30 @@ export type Mutable<T> = internal.Mutable<T>;
207
214
  *
208
215
  * @example
209
216
  * ```typescript
210
- * // Mutate within Entity.change
211
- * Entity.change(entity, (obj) => {
217
+ * // Mutate within Entity.update
218
+ * Entity.update(entity, (obj) => {
212
219
  * obj.name = 'Updated';
213
220
  * obj.count = 42;
214
221
  * });
215
222
  *
216
223
  * // Direct mutation throws
217
- * entity.name = 'Bob'; // Error: Cannot modify outside Entity.change()
224
+ * entity.name = 'Bob'; // Error: Cannot modify outside Entity.update()
218
225
  * ```
219
226
  *
220
- * Note: For type-specific operations, prefer `Obj.change` or `Relation.change`.
227
+ * Note: For type-specific operations, prefer `Obj.update` or `Relation.update`.
221
228
  */
222
- export const change = <T extends Unknown>(entity: T, callback: internal.ChangeCallback<T>): void => {
229
+ export const update = <T extends Unknown>(entity: T, callback: internal.ChangeCallback<T>): void => {
223
230
  internal.change(entity, callback);
224
231
  };
225
232
 
226
233
  /**
227
234
  * Add a tag to an entity.
228
- * Must be called within an `Entity.change`, `Obj.change`, or `Relation.change` callback.
235
+ * Must be called within an `Entity.update`, `Obj.update`, or `Relation.update` callback.
229
236
  */
230
237
  export const addTag = (entity: Mutable<Unknown>, tag: string): void => internal.addTag(entity, tag);
231
238
 
232
239
  /**
233
240
  * Remove a tag from an entity.
234
- * Must be called within an `Entity.change`, `Obj.change`, or `Relation.change` callback.
241
+ * Must be called within an `Entity.update`, `Obj.update`, or `Relation.update` callback.
235
242
  */
236
243
  export const removeTag = (entity: Mutable<Unknown>, tag: string): void => internal.removeTag(entity, tag);
package/src/Extension.ts CHANGED
@@ -43,7 +43,7 @@ export interface Extension<T> extends Record<
43
43
  * email: 'john@example.com',
44
44
  * });
45
45
  *
46
- * Obj.change(obj, (obj) => {
46
+ * Obj.update(obj, (obj) => {
47
47
  * Extension.set(obj.extensions, ColorExtension, 'red');
48
48
  * });
49
49
  *
@@ -103,10 +103,10 @@ export const get: {
103
103
  /**
104
104
  * Set the value of an extension in a set of values.
105
105
  *
106
- * Can also be used within Obj.change callback:
106
+ * Can also be used within Obj.update callback:
107
107
  *
108
108
  * ```ts
109
- * Obj.change(obj, (obj) => {
109
+ * Obj.update(obj, (obj) => {
110
110
  * Extension.set(obj.extensions, ColorExtension, 'red');
111
111
  * });
112
112
  * ```
package/src/Feed.ts CHANGED
@@ -10,7 +10,7 @@ import * as Layer from 'effect/Layer';
10
10
  import type * as Option from 'effect/Option';
11
11
  import * as Schema from 'effect/Schema';
12
12
 
13
- import { DXN } from '@dxos/keys';
13
+ import { DXN, type ObjectId } from '@dxos/keys';
14
14
 
15
15
  import * as Annotation from './Annotation';
16
16
  import type * as Entity from './Entity';
@@ -109,6 +109,27 @@ export const getQueueDxn = (feed: Feed): DXN | undefined => {
109
109
  return new DXN(DXN.kind.QUEUE, [feed.namespace ?? 'data', self.spaceId, self.echoId]);
110
110
  };
111
111
 
112
+ /**
113
+ * Creates a Feed object from a queue DXN, inferring the feed's id and namespace from the DXN parts.
114
+ *
115
+ * The resulting Feed, when added to the same space as the queue, will have a queue DXN
116
+ * equal to the input (see `Feed.getQueueDxn`). Useful when migrating `Ref(Queue)` fields to
117
+ * `Ref(Feed.Feed)`.
118
+ *
119
+ * @remarks Unsafe because the caller must ensure the queue DXN's space matches the database
120
+ * the feed is added to; the feed id is set from the queue id, bypassing id generation.
121
+ */
122
+ export const unsafeFromQueueDXN = (queueDxn: DXN): Feed => {
123
+ const parts = queueDxn.asQueueDXN();
124
+ if (!parts) {
125
+ throw new Error(`Expected a queue DXN, got: ${queueDxn.toString()}`);
126
+ }
127
+ return Obj.make(Feed, {
128
+ id: parts.queueId as ObjectId,
129
+ namespace: parts.subspaceTag === 'trace' ? 'trace' : undefined,
130
+ });
131
+ };
132
+
112
133
  //
113
134
  // Service
114
135
  //
package/src/Filter.ts CHANGED
@@ -13,8 +13,8 @@ import { type ForeignKey, type QueryAST } from '@dxos/echo-protocol';
13
13
  import { assertArgument } from '@dxos/invariant';
14
14
  import { DXN, ObjectId } from '@dxos/keys';
15
15
 
16
- import type * as Entity from './Entity';
17
16
  import * as internal from './internal';
17
+ import type * as Obj from './Obj';
18
18
  import * as Ref from './Ref';
19
19
 
20
20
  export interface Filter<T> {
@@ -346,14 +346,18 @@ export type ChildOfOptions = {
346
346
 
347
347
  /**
348
348
  * Filter objects that are children of the specified parent(s).
349
- * Accepts ECHO objects, DXN values, or arrays of either.
349
+ * Accepts ECHO objects, Refs, or arrays of either.
350
+ * Refs are resolved to DXNs without loading; objects use {@link Obj.getDXN}.
350
351
  * With transitive=true (default), also matches grandchildren and beyond.
351
352
  */
352
- export const childOf = (parents: Entity.Unknown | DXN | (Entity.Unknown | DXN)[], options?: ChildOfOptions): Any => {
353
+ export const childOf = (
354
+ parents: Obj.Unknown | Ref.Unknown | readonly (Obj.Unknown | Ref.Unknown)[],
355
+ options?: ChildOfOptions,
356
+ ): Any => {
353
357
  const items = Array.isArray(parents) ? parents : [parents];
354
358
  const dxns = items.map((item) => {
355
- if (item instanceof DXN) {
356
- return item.toString();
359
+ if (Ref.isRef(item)) {
360
+ return item.dxn.toString();
357
361
  }
358
362
  return internal.getDXN(item).toString();
359
363
  });
@@ -0,0 +1,175 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, test } from 'vitest';
6
+
7
+ import { DXN, ObjectId } from '@dxos/keys';
8
+ import { safeStringify } from '@dxos/util';
9
+
10
+ import * as Database from './Database';
11
+ import * as Json from './Json';
12
+
13
+ /** Mint a random ECHO object id usable as both a stub-db key and a DXN payload. */
14
+ const newId = (): string => ObjectId.random();
15
+
16
+ /** Build a fake encoded ref for a local-space object id. */
17
+ const encodeRef = (id: string): { '/': string } => ({ '/': DXN.fromLocalObjectId(id).toString() });
18
+
19
+ /** Minimal stub: `createRefReplacer` only touches `db.getObjectById`. */
20
+ const makeStubDb = (objects: Record<string, unknown>): Database.Database => {
21
+ return {
22
+ getObjectById: (id: string) => objects[id],
23
+ } as unknown as Database.Database;
24
+ };
25
+
26
+ /**
27
+ * Run a value through the replacer's public contract — `JSON.stringify(value, replacer)` —
28
+ * then re-parse the result so tests can assert on plain JS shapes. This mirrors the way
29
+ * the JSON highlighter invokes the replacer in production.
30
+ */
31
+ const stringifyWith = (replacer: Json.JsonReplacer, value: unknown): unknown =>
32
+ JSON.parse(JSON.stringify(value, replacer));
33
+
34
+ describe('createRefReplacer', () => {
35
+ test('passes plain values through unchanged', () => {
36
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
37
+ const subject = { a: 1, b: 'two', c: [3, { d: 4 }] };
38
+ expect(stringifyWith(replacer, subject)).toEqual(subject);
39
+ });
40
+
41
+ test('inlines refs at default depth (1)', () => {
42
+ const id = newId();
43
+ const target = { name: 'inlined' };
44
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [id]: target }) });
45
+ const subject = { ref: encodeRef(id) };
46
+ expect(stringifyWith(replacer, subject)).toEqual({ ref: target });
47
+ });
48
+
49
+ test('does not follow refs when depth is 0', () => {
50
+ const id = newId();
51
+ const target = { name: 'inlined' };
52
+ const ref = encodeRef(id);
53
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [id]: target }), depth: 0 });
54
+ expect(stringifyWith(replacer, { ref })).toEqual({ ref });
55
+ });
56
+
57
+ test('inlines refs across multiple levels up to depth', () => {
58
+ const innerId = newId();
59
+ const middleId = newId();
60
+ const inner = { name: 'inner' };
61
+ const middle = { ref: encodeRef(innerId) };
62
+ const outer = { ref: encodeRef(middleId) };
63
+ const db = makeStubDb({ [innerId]: inner, [middleId]: middle });
64
+
65
+ expect(stringifyWith(Json.createRefReplacer({ db, depth: 1 }), outer)).toEqual({
66
+ ref: { ref: encodeRef(innerId) },
67
+ });
68
+
69
+ expect(stringifyWith(Json.createRefReplacer({ db, depth: 2 }), outer)).toEqual({
70
+ ref: { ref: inner },
71
+ });
72
+ });
73
+
74
+ test('leaves refs encoded when the target is missing in the db', () => {
75
+ const ref = encodeRef(newId());
76
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
77
+ expect(stringifyWith(replacer, { ref })).toEqual({ ref });
78
+ });
79
+
80
+ test('leaves non-DXN single-key { "/": string } objects untouched', () => {
81
+ // Same `{ '/': string }` shape is used by other IPLD-style refs (e.g. CIDs); those should
82
+ // not crash the replacer and should pass through verbatim.
83
+ const cidLike = { '/': 'bafybeibwzifw7izxykxz' };
84
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
85
+ expect(stringifyWith(replacer, { ref: cidLike })).toEqual({ ref: cidLike });
86
+ });
87
+
88
+ test('leaves malformed dxn strings untouched', () => {
89
+ const malformed = { '/': 'dxn:not-a-real-dxn' };
90
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
91
+ expect(stringifyWith(replacer, { ref: malformed })).toEqual({ ref: malformed });
92
+ });
93
+
94
+ test('leaves non-echo dxns untouched (e.g. type DXN)', () => {
95
+ // Type DXNs share the `dxn:` prefix but `asEchoDXN()` returns undefined.
96
+ const typeRef = { '/': DXN.fromTypename('com.example.Thing').toString() };
97
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
98
+ expect(stringifyWith(replacer, { ref: typeRef })).toEqual({ ref: typeRef });
99
+ });
100
+
101
+ test('inlines refs inside arrays', () => {
102
+ const idA = newId();
103
+ const idB = newId();
104
+ const a = { name: 'a' };
105
+ const b = { name: 'b' };
106
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [idA]: a, [idB]: b }) });
107
+ expect(stringifyWith(replacer, { items: [encodeRef(idA), encodeRef(idB), { plain: true }] })).toEqual({
108
+ items: [a, b, { plain: true }],
109
+ });
110
+ });
111
+
112
+ test('walks nested objects recursively', () => {
113
+ const innerId = newId();
114
+ const inner = { name: 'inner' };
115
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }) });
116
+ const subject = { outer: { mid: { ref: encodeRef(innerId) } } };
117
+ expect(stringifyWith(replacer, subject)).toEqual({ outer: { mid: { ref: inner } } });
118
+ });
119
+
120
+ test('a single replacer invocation does not recurse on its own', () => {
121
+ // The replacer is per-call; JSON.stringify drives the tree walk. Calling the replacer
122
+ // directly on a cyclic input must therefore return without touching the cycle.
123
+ const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
124
+ const node: any = { name: 'self' };
125
+ node.self = node;
126
+
127
+ expect(() => replacer('', node)).not.toThrow();
128
+ expect(replacer('', node)).toBe(node);
129
+ });
130
+
131
+ test('invokes `toJSON` on resolved targets so refs in the target are re-walked', () => {
132
+ // Simulates the ECHO-object branch: `db.getObjectById` returns a live proxy, the replacer
133
+ // calls `.toJSON()` to get the encoded form, then continues walking that form. A ref nested
134
+ // inside the target should be inlined when there's depth budget remaining.
135
+ const outerId = newId();
136
+ const innerId = newId();
137
+ const inner = { name: 'inner' };
138
+ const target = {
139
+ toJSON: () => ({ nestedRef: encodeRef(innerId) }),
140
+ };
141
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [outerId]: target, [innerId]: inner }), depth: 2 });
142
+ expect(stringifyWith(replacer, { ref: encodeRef(outerId) })).toEqual({
143
+ ref: { nestedRef: inner },
144
+ });
145
+ });
146
+
147
+ test('depth budget counts ref hops, not tree depth — a ref deep in a plain tree still resolves', () => {
148
+ // A ref nested under arbitrarily many plain objects is one ref hop from the root, so
149
+ // `depth: 1` resolves it. `depth: 0` leaves it encoded.
150
+ const innerId = newId();
151
+ const inner = { name: 'inner' };
152
+ const subject = { a: { b: { c: { d: { ref: encodeRef(innerId) } } } } };
153
+
154
+ const inlining = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }), depth: 1 });
155
+ expect(stringifyWith(inlining, subject)).toEqual({ a: { b: { c: { d: { ref: inner } } } } });
156
+
157
+ const passthrough = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }), depth: 0 });
158
+ expect(stringifyWith(passthrough, subject)).toEqual(subject);
159
+ });
160
+
161
+ test('inlines refs when invoked through safeStringify (production path)', () => {
162
+ // `JsonHighlighter` runs the replacer through `@dxos/util/safeStringify`, whose inner
163
+ // wrapper short-circuits the root call without forwarding it to the user's filter. The
164
+ // replacer must therefore work on a per-call basis — not as a one-shot root tree walk.
165
+ // This regression-tests that integration: the `content` ref must inline.
166
+ const targetId = newId();
167
+ const target = { toJSON: () => ({ name: 'README content' }) };
168
+ const document = { id: '01ABC', name: 'README', content: encodeRef(targetId) };
169
+
170
+ const replacer = Json.createRefReplacer({ db: makeStubDb({ [targetId]: target }) });
171
+ const out = JSON.parse(safeStringify(document, replacer, 0)!);
172
+
173
+ expect(out).toEqual({ id: '01ABC', name: 'README', content: { name: 'README content' } });
174
+ });
175
+ });
package/src/Json.ts ADDED
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { DXN } from '@dxos/keys';
6
+
7
+ import * as Database from './Database';
8
+ import * as Obj from './Obj';
9
+
10
+ /**
11
+ * `JSON.stringify` replacer signature.
12
+ *
13
+ * Defined here (rather than re-imported from a UI package) so other ECHO-aware utilities can
14
+ * share a stable signature without creating a dependency edge into the UI tree.
15
+ */
16
+ export type JsonReplacer = (key: string, value: any) => any;
17
+
18
+ export type CreateRefReplacerOptions = {
19
+ db: Database.Database;
20
+ /** How many ref hops to follow. `0` leaves all refs as-is. Default: `1`. */
21
+ depth?: number;
22
+ };
23
+
24
+ const isEncodedRef = (value: unknown): value is { '/': string } =>
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ Object.keys(value as object).length === 1 &&
28
+ typeof (value as { '/': unknown })['/'] === 'string';
29
+
30
+ const toJson = (obj: Obj.Any): unknown => (typeof (obj as any).toJSON === 'function' ? (obj as any).toJSON() : obj);
31
+
32
+ /**
33
+ * Returns a {@link JsonReplacer} that inlines ECHO ref objects (`{ "/": "dxn:echo:..." }`) up to
34
+ * `depth` ref hops. Beyond that depth refs are left in their encoded form.
35
+ *
36
+ * Implemented as a per-call `JSON.stringify` replacer (not a one-shot tree walk at root) so it
37
+ * composes with wrappers like `safeStringify` that intercept the root call. JSON.stringify
38
+ * already drives the recursion; we only need to (a) detect a ref at the current callback,
39
+ * (b) resolve and return the target if hop budget remains, and (c) tag the returned object
40
+ * with its hop count so children know how far in they are.
41
+ *
42
+ * The hop count is tracked per-object via a `WeakMap`: a ref-resolved target's children inherit
43
+ * `parentHops + 1`; a regular intermediate object's children inherit `parentHops`. This makes the
44
+ * budget count *ref hops*, not tree depth — a ref deep in a tree still resolves once when
45
+ * `depth >= 1`.
46
+ *
47
+ * Note: ECHO objects' `toJSON` runs before the replacer is invoked, so by the time we see a
48
+ * value refs are already encoded as `{ "/": "dxn:..." }`.
49
+ */
50
+ export const createRefReplacer = ({ db, depth = 1 }: CreateRefReplacerOptions): JsonReplacer => {
51
+ // Per-object hop count. Set when we return an object (via ref resolution or pass-through) so
52
+ // the child callbacks (which carry that object as `this`) can read it.
53
+ const hops = new WeakMap<object, number>();
54
+
55
+ return function (this: any, key: string, value: any) {
56
+ // Hop count for this call: hops at the parent, or 0 for the root.
57
+ const parentHops = this && typeof this === 'object' ? (hops.get(this) ?? 0) : 0;
58
+ if (isEncodedRef(value)) {
59
+ if (parentHops >= depth) {
60
+ return value;
61
+ }
62
+
63
+ // The `{ '/': string }` shape is shared with non-DXN IPLD-style refs (e.g. CIDs);
64
+ // an unparseable string would otherwise crash the whole `JSON.stringify`.
65
+ // Treat any parse miss as "leave as-is" rather than propagating.
66
+ const dxnString = value['/'];
67
+ if (!dxnString.startsWith('dxn:')) {
68
+ return value;
69
+ }
70
+
71
+ let echoId: string | undefined;
72
+ try {
73
+ echoId = DXN.parse(dxnString).asEchoDXN()?.echoId;
74
+ } catch {
75
+ return value;
76
+ }
77
+
78
+ if (!echoId) {
79
+ return value;
80
+ }
81
+ const target = db.getObjectById(echoId);
82
+ if (!target) {
83
+ return value;
84
+ }
85
+
86
+ const encoded = toJson(target);
87
+ if (encoded != null && typeof encoded === 'object') {
88
+ // Children of the resolved target are one hop deeper.
89
+ hops.set(encoded as object, parentHops + 1);
90
+ }
91
+ return encoded;
92
+ }
93
+
94
+ // Pass-through object: children inherit the parent's hop count (this branch doesn't burn
95
+ // budget).
96
+ if (value != null && typeof value === 'object') {
97
+ hops.set(value, parentHops);
98
+ }
99
+
100
+ return value;
101
+ };
102
+ };