@dxos/echo 0.8.4-main.d05673bc65 → 0.8.4-main.e00bdcdb52

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 (271) hide show
  1. package/dist/lib/neutral/Annotation.mjs +3 -3
  2. package/dist/lib/neutral/Database.mjs +8 -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 +18 -0
  6. package/dist/lib/neutral/Extension.mjs.map +7 -0
  7. package/dist/lib/neutral/Feed.mjs +21 -17
  8. package/dist/lib/neutral/Filter.mjs +21 -13
  9. package/dist/lib/neutral/Format.mjs +3 -3
  10. package/dist/lib/neutral/JsonSchema.mjs +8 -8
  11. package/dist/lib/neutral/Key.mjs +1 -1
  12. package/dist/lib/neutral/Migration.mjs +17 -0
  13. package/dist/lib/neutral/Migration.mjs.map +7 -0
  14. package/dist/lib/neutral/Obj.mjs +16 -13
  15. package/dist/lib/neutral/Order.mjs +1 -1
  16. package/dist/lib/neutral/Query.mjs +19 -16
  17. package/dist/lib/neutral/QueryResult.mjs +1 -1
  18. package/dist/lib/neutral/Ref.mjs +7 -7
  19. package/dist/lib/neutral/Relation.mjs +15 -14
  20. package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
  21. package/dist/lib/neutral/Tag.mjs +14 -13
  22. package/dist/lib/neutral/Type.mjs +10 -10
  23. package/dist/lib/neutral/{chunk-VA2F47ZC.mjs → chunk-2T22UGGN.mjs} +68 -14
  24. package/dist/lib/neutral/chunk-2T22UGGN.mjs.map +7 -0
  25. package/dist/lib/neutral/{chunk-FZO7LQO7.mjs → chunk-44HT3MEC.mjs} +2 -2
  26. package/dist/lib/neutral/{chunk-UFDK26FO.mjs → chunk-6VC3FI5E.mjs} +12 -8
  27. package/dist/lib/neutral/chunk-6VC3FI5E.mjs.map +7 -0
  28. package/dist/lib/neutral/{chunk-DYDJBZEG.mjs → chunk-7JFW72MX.mjs} +51 -21
  29. package/dist/lib/neutral/chunk-7JFW72MX.mjs.map +7 -0
  30. package/dist/lib/neutral/{chunk-ANHVGJI4.mjs → chunk-7RVZT53K.mjs} +1 -1
  31. package/dist/lib/neutral/{chunk-BNCCGLJN.mjs → chunk-BICZKPQG.mjs} +1 -1
  32. package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs +36 -0
  33. package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs.map +7 -0
  34. package/dist/lib/neutral/{chunk-724GQYEN.mjs → chunk-DUNXPKAC.mjs} +4 -4
  35. package/dist/lib/neutral/{chunk-YWXWXIE5.mjs → chunk-FAW7PJRO.mjs} +2 -2
  36. package/dist/lib/neutral/{chunk-YWXWXIE5.mjs.map → chunk-FAW7PJRO.mjs.map} +2 -2
  37. package/dist/lib/neutral/{chunk-UTBRYVQC.mjs → chunk-FAYW32CW.mjs} +2 -2
  38. package/dist/lib/neutral/chunk-GWFFC34K.mjs +50 -0
  39. package/dist/lib/neutral/chunk-GWFFC34K.mjs.map +7 -0
  40. package/dist/lib/neutral/{chunk-NXMFBIT5.mjs → chunk-I2MFJ76N.mjs} +59 -4
  41. package/dist/lib/neutral/chunk-I2MFJ76N.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-SEMVAGBM.mjs → chunk-JALF2CVV.mjs} +13 -23
  43. package/dist/lib/neutral/chunk-JALF2CVV.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-NEGC54NE.mjs → chunk-KQUQZ3CB.mjs} +17 -19
  45. package/dist/lib/neutral/chunk-KQUQZ3CB.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-NQRLRYJM.mjs → chunk-LOTZLYHB.mjs} +119 -12
  47. package/dist/lib/neutral/chunk-LOTZLYHB.mjs.map +7 -0
  48. package/dist/lib/neutral/{chunk-WYOKA6AE.mjs → chunk-N4B7FHQT.mjs} +2 -2
  49. package/dist/lib/neutral/{chunk-WYOKA6AE.mjs.map → chunk-N4B7FHQT.mjs.map} +1 -1
  50. package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs → chunk-NKXEKBP5.mjs} +6 -22
  51. package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs.map → chunk-NKXEKBP5.mjs.map} +3 -3
  52. package/dist/lib/neutral/{chunk-CIRHIEMW.mjs → chunk-NSMLBSFS.mjs} +18 -46
  53. package/dist/lib/neutral/chunk-NSMLBSFS.mjs.map +7 -0
  54. package/dist/lib/neutral/{chunk-OENWMTE6.mjs → chunk-QBIGOSRF.mjs} +2 -2
  55. package/dist/lib/neutral/{chunk-BOZZPUXE.mjs → chunk-QBLYZ4IV.mjs} +12 -65
  56. package/dist/lib/neutral/chunk-QBLYZ4IV.mjs.map +7 -0
  57. package/dist/lib/neutral/{chunk-KTAWP7GI.mjs → chunk-QEVM3JUP.mjs} +42 -7
  58. package/dist/lib/neutral/chunk-QEVM3JUP.mjs.map +7 -0
  59. package/dist/lib/neutral/{chunk-43Y5DOS6.mjs → chunk-REP7WWAQ.mjs} +16 -66
  60. package/dist/lib/neutral/chunk-REP7WWAQ.mjs.map +7 -0
  61. package/dist/lib/neutral/{chunk-B5OXLWZL.mjs → chunk-TRPZU2HV.mjs} +2 -2
  62. package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
  63. package/dist/lib/neutral/{chunk-FXEG7EOK.mjs → chunk-TW76K7H5.mjs} +3 -3
  64. package/dist/lib/neutral/{chunk-TQQZLKB7.mjs → chunk-UYJYDSD7.mjs} +193 -469
  65. package/dist/lib/neutral/chunk-UYJYDSD7.mjs.map +7 -0
  66. package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
  67. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs → chunk-ZISMEVKD.mjs} +1 -1
  68. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs.map → chunk-ZISMEVKD.mjs.map} +2 -2
  69. package/dist/lib/neutral/index.mjs +38 -28
  70. package/dist/lib/neutral/internal/index.mjs +13 -9
  71. package/dist/lib/neutral/meta.json +1 -1
  72. package/dist/lib/neutral/testing/index.mjs +30 -28
  73. package/dist/lib/neutral/testing/index.mjs.map +2 -2
  74. package/dist/types/src/Collection.d.ts.map +1 -1
  75. package/dist/types/src/Database.d.ts +13 -1
  76. package/dist/types/src/Database.d.ts.map +1 -1
  77. package/dist/types/src/Dataset.d.ts +2 -1
  78. package/dist/types/src/Dataset.d.ts.map +1 -1
  79. package/dist/types/src/Entity.d.ts +17 -11
  80. package/dist/types/src/Entity.d.ts.map +1 -1
  81. package/dist/types/src/Err.d.ts +18 -18
  82. package/dist/types/src/Err.d.ts.map +1 -1
  83. package/dist/types/src/Extension.d.ts +80 -0
  84. package/dist/types/src/Extension.d.ts.map +1 -0
  85. package/dist/types/src/Extension.test.d.ts +2 -0
  86. package/dist/types/src/Extension.test.d.ts.map +1 -0
  87. package/dist/types/src/Feed.d.ts +37 -20
  88. package/dist/types/src/Feed.d.ts.map +1 -1
  89. package/dist/types/src/Filter.d.ts +33 -4
  90. package/dist/types/src/Filter.d.ts.map +1 -1
  91. package/dist/types/src/Filter.test.d.ts +2 -0
  92. package/dist/types/src/Filter.test.d.ts.map +1 -0
  93. package/dist/types/src/Json.d.ts +33 -0
  94. package/dist/types/src/Json.d.ts.map +1 -0
  95. package/dist/types/src/Json.test.d.ts +2 -0
  96. package/dist/types/src/Json.test.d.ts.map +1 -0
  97. package/dist/types/src/JsonSchema.d.ts +1 -1
  98. package/dist/types/src/Migration.d.ts +57 -0
  99. package/dist/types/src/Migration.d.ts.map +1 -0
  100. package/dist/types/src/Obj.d.ts +41 -27
  101. package/dist/types/src/Obj.d.ts.map +1 -1
  102. package/dist/types/src/Order.d.ts.map +1 -1
  103. package/dist/types/src/Query.d.ts +13 -2
  104. package/dist/types/src/Query.d.ts.map +1 -1
  105. package/dist/types/src/Ref.d.ts.map +1 -1
  106. package/dist/types/src/Relation.d.ts +17 -18
  107. package/dist/types/src/Relation.d.ts.map +1 -1
  108. package/dist/types/src/Tag.d.ts +2 -2
  109. package/dist/types/src/Tag.d.ts.map +1 -1
  110. package/dist/types/src/Type.d.ts +3 -3
  111. package/dist/types/src/Type.d.ts.map +1 -1
  112. package/dist/types/src/View.d.ts +1 -1
  113. package/dist/types/src/View.d.ts.map +1 -1
  114. package/dist/types/src/index.d.ts +3 -0
  115. package/dist/types/src/index.d.ts.map +1 -1
  116. package/dist/types/src/internal/Annotation/annotations.d.ts +3 -2
  117. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  118. package/dist/types/src/internal/Annotation/sorting.d.ts.map +1 -1
  119. package/dist/types/src/internal/Annotation/util.d.ts +1 -1
  120. package/dist/types/src/internal/Annotation/util.d.ts.map +1 -1
  121. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  122. package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
  123. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  124. package/dist/types/src/internal/Entity/version.d.ts.map +1 -1
  125. package/dist/types/src/internal/Format/date.d.ts.map +1 -1
  126. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  127. package/dist/types/src/internal/Format/number.d.ts.map +1 -1
  128. package/dist/types/src/internal/Format/object.d.ts.map +1 -1
  129. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  130. package/dist/types/src/internal/JsonSchema/json-schema-normalize.d.ts.map +1 -1
  131. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +28 -28
  132. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
  133. package/dist/types/src/internal/JsonSchema/json-schema.d.ts +1 -1
  134. package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
  135. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  136. package/dist/types/src/internal/Obj/common.d.ts.map +1 -1
  137. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  138. package/dist/types/src/internal/Obj/deleted.d.ts.map +1 -1
  139. package/dist/types/src/internal/Obj/ids.d.ts.map +1 -1
  140. package/dist/types/src/internal/Obj/json-serializer.d.ts +4 -3
  141. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  142. package/dist/types/src/internal/Obj/set-value.d.ts +1 -1
  143. package/dist/types/src/internal/Obj/set-value.d.ts.map +1 -1
  144. package/dist/types/src/internal/Obj/snapshot.d.ts.map +1 -1
  145. package/dist/types/src/internal/Query.d.ts +10 -0
  146. package/dist/types/src/internal/Query.d.ts.map +1 -0
  147. package/dist/types/src/internal/Ref/ref-array.d.ts.map +1 -1
  148. package/dist/types/src/internal/Ref/ref.d.ts +14 -1
  149. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  150. package/dist/types/src/internal/Type/compose.d.ts.map +1 -1
  151. package/dist/types/src/internal/Type/echo-schema.d.ts +2 -2
  152. package/dist/types/src/internal/Type/echo-schema.d.ts.map +1 -1
  153. package/dist/types/src/internal/Type/manipulation.d.ts.map +1 -1
  154. package/dist/types/src/internal/common/api/meta.d.ts +3 -3
  155. package/dist/types/src/internal/common/api/meta.d.ts.map +1 -1
  156. package/dist/types/src/internal/common/proxy/change-context.d.ts +1 -1
  157. package/dist/types/src/internal/common/proxy/change-context.d.ts.map +1 -1
  158. package/dist/types/src/internal/common/proxy/define-hidden-property.d.ts.map +1 -1
  159. package/dist/types/src/internal/common/proxy/errors.d.ts +1 -1
  160. package/dist/types/src/internal/common/proxy/errors.d.ts.map +1 -1
  161. package/dist/types/src/internal/common/proxy/event-batch.d.ts.map +1 -1
  162. package/dist/types/src/internal/common/proxy/json-serializer.d.ts.map +1 -1
  163. package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
  164. package/dist/types/src/internal/common/proxy/ownership.d.ts.map +1 -1
  165. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  166. package/dist/types/src/internal/common/proxy/reactive-array.d.ts +1 -1
  167. package/dist/types/src/internal/common/proxy/reactive.d.ts +1 -1
  168. package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
  169. package/dist/types/src/internal/common/proxy/reactive.test.d.ts +2 -0
  170. package/dist/types/src/internal/common/proxy/reactive.test.d.ts.map +1 -0
  171. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  172. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  173. package/dist/types/src/internal/common/types/base.d.ts.map +1 -1
  174. package/dist/types/src/internal/common/types/entity.d.ts +4 -4
  175. package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
  176. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  177. package/dist/types/src/internal/common/types/version.d.ts +1 -1
  178. package/dist/types/src/internal/index.d.ts +1 -0
  179. package/dist/types/src/internal/index.d.ts.map +1 -1
  180. package/dist/types/src/testing/test-data.d.ts.map +1 -1
  181. package/dist/types/src/testing/test-schema.d.ts +53 -53
  182. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  183. package/dist/types/src/testing/util.d.ts.map +1 -1
  184. package/dist/types/tsconfig.tsbuildinfo +1 -1
  185. package/package.json +24 -14
  186. package/src/Collection.ts +5 -0
  187. package/src/Database.ts +49 -14
  188. package/src/Entity.ts +18 -12
  189. package/src/Extension.test.ts +235 -0
  190. package/src/Extension.ts +122 -0
  191. package/src/Feed.ts +79 -32
  192. package/src/Filter.test.ts +90 -0
  193. package/src/Filter.ts +66 -3
  194. package/src/Json.test.ts +175 -0
  195. package/src/Json.ts +102 -0
  196. package/src/Migration.ts +94 -0
  197. package/src/Obj.test.ts +105 -13
  198. package/src/Obj.ts +153 -33
  199. package/src/Query.test.ts +198 -8
  200. package/src/Query.ts +46 -1
  201. package/src/Relation.ts +24 -20
  202. package/src/Type.ts +1 -1
  203. package/src/View.ts +5 -0
  204. package/src/index.ts +4 -0
  205. package/src/internal/Annotation/annotations.test.ts +51 -2
  206. package/src/internal/Annotation/annotations.ts +19 -13
  207. package/src/internal/Annotation/sorting.ts +0 -1
  208. package/src/internal/Entity/api.ts +0 -1
  209. package/src/internal/Entity/object.ts +0 -1
  210. package/src/internal/Entity/version.ts +0 -1
  211. package/src/internal/Format/date.test.ts +0 -1
  212. package/src/internal/Format/format.test.ts +0 -1
  213. package/src/internal/JsonSchema/json-schema-type.ts +1 -1
  214. package/src/internal/JsonSchema/json-schema.test.ts +1 -2
  215. package/src/internal/JsonSchema/json-schema.ts +1 -2
  216. package/src/internal/Obj/clone.ts +1 -1
  217. package/src/internal/Obj/create-object.test.ts +2 -4
  218. package/src/internal/Obj/create-object.ts +2 -3
  219. package/src/internal/Obj/deleted.ts +1 -1
  220. package/src/internal/Obj/json-serializer.test.ts +4 -5
  221. package/src/internal/Obj/json-serializer.ts +22 -15
  222. package/src/internal/Obj/set-value.test.ts +24 -24
  223. package/src/internal/Obj/set-value.ts +1 -1
  224. package/src/internal/Query.ts +137 -0
  225. package/src/internal/Ref/ref-array.ts +0 -1
  226. package/src/internal/Ref/ref.test.ts +0 -1
  227. package/src/internal/Ref/ref.ts +18 -1
  228. package/src/internal/Type/compose.test.ts +0 -1
  229. package/src/internal/Type/echo-schema.ts +3 -4
  230. package/src/internal/Type/persistent-schema.ts +2 -2
  231. package/src/internal/common/README.md +1 -1
  232. package/src/internal/common/api/meta.ts +3 -3
  233. package/src/internal/common/proxy/change-context.ts +1 -1
  234. package/src/internal/common/proxy/change.test.ts +94 -94
  235. package/src/internal/common/proxy/errors.ts +2 -2
  236. package/src/internal/common/proxy/handler.test.ts +0 -2
  237. package/src/internal/common/proxy/make-object.ts +0 -1
  238. package/src/internal/common/proxy/ownership.ts +0 -1
  239. package/src/internal/common/proxy/reactive-array.ts +1 -1
  240. package/src/internal/common/proxy/reactive.test.ts +54 -0
  241. package/src/internal/common/proxy/reactive.ts +11 -3
  242. package/src/internal/common/proxy/typed-handler.test.ts +0 -1
  243. package/src/internal/common/proxy/typed-handler.ts +8 -10
  244. package/src/internal/common/proxy/typed-object.test.ts +2 -3
  245. package/src/internal/common/types/entity.ts +1 -1
  246. package/src/internal/common/types/meta.ts +0 -1
  247. package/src/internal/index.ts +1 -0
  248. package/src/testing/api.test.ts +0 -1
  249. package/src/testing/test-data.ts +0 -1
  250. package/dist/lib/neutral/chunk-43Y5DOS6.mjs.map +0 -7
  251. package/dist/lib/neutral/chunk-BOZZPUXE.mjs.map +0 -7
  252. package/dist/lib/neutral/chunk-CIRHIEMW.mjs.map +0 -7
  253. package/dist/lib/neutral/chunk-DYDJBZEG.mjs.map +0 -7
  254. package/dist/lib/neutral/chunk-KTAWP7GI.mjs.map +0 -7
  255. package/dist/lib/neutral/chunk-NEGC54NE.mjs.map +0 -7
  256. package/dist/lib/neutral/chunk-NQRLRYJM.mjs.map +0 -7
  257. package/dist/lib/neutral/chunk-NXMFBIT5.mjs.map +0 -7
  258. package/dist/lib/neutral/chunk-SEMVAGBM.mjs.map +0 -7
  259. package/dist/lib/neutral/chunk-TQQZLKB7.mjs.map +0 -7
  260. package/dist/lib/neutral/chunk-UFDK26FO.mjs.map +0 -7
  261. package/dist/lib/neutral/chunk-VA2F47ZC.mjs.map +0 -7
  262. /package/dist/lib/neutral/{chunk-FZO7LQO7.mjs.map → chunk-44HT3MEC.mjs.map} +0 -0
  263. /package/dist/lib/neutral/{chunk-ANHVGJI4.mjs.map → chunk-7RVZT53K.mjs.map} +0 -0
  264. /package/dist/lib/neutral/{chunk-BNCCGLJN.mjs.map → chunk-BICZKPQG.mjs.map} +0 -0
  265. /package/dist/lib/neutral/{chunk-724GQYEN.mjs.map → chunk-DUNXPKAC.mjs.map} +0 -0
  266. /package/dist/lib/neutral/{chunk-UTBRYVQC.mjs.map → chunk-FAYW32CW.mjs.map} +0 -0
  267. /package/dist/lib/neutral/{chunk-OENWMTE6.mjs.map → chunk-QBIGOSRF.mjs.map} +0 -0
  268. /package/dist/lib/neutral/{chunk-B5OXLWZL.mjs.map → chunk-TRPZU2HV.mjs.map} +0 -0
  269. /package/dist/lib/neutral/{chunk-UI6MWK5W.mjs.map → chunk-TTCSATUD.mjs.map} +0 -0
  270. /package/dist/lib/neutral/{chunk-FXEG7EOK.mjs.map → chunk-TW76K7H5.mjs.map} +0 -0
  271. /package/dist/lib/neutral/{chunk-OMUPQMLR.mjs.map → chunk-V72DY6LU.mjs.map} +0 -0
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
+ };
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import type * as Schema from 'effect/Schema';
8
+
9
+ import { type DXN } from '@dxos/keys';
10
+
11
+ import type * as Database from './Database';
12
+ import type * as Entity from './Entity';
13
+ import { getSchemaDXN } from './internal';
14
+
15
+ type DefineObjectMigrationOptions<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
16
+ from: From;
17
+ to: To;
18
+ /**
19
+ * Pure function that converts the old object data to the new object data.
20
+ */
21
+ // TODO(dmaretskyi): `id` should not be a part of the schema.
22
+ transform: (
23
+ from: Schema.Schema.Type<From>,
24
+ context: ObjectMigrationContext,
25
+ ) => Promise<Omit<Schema.Schema.Type<To>, 'id' | Entity.KindId>>;
26
+
27
+ /**
28
+ * Callback that is called after the object is migrated. Called for every object that is migrated.
29
+ *
30
+ * NOTE: Database mutations performed in this callback are not guaranteed to be idempotent.
31
+ * If multiple peers run the migration separately, the effects may be applied multiple times.
32
+ */
33
+ onMigration: (params: OnMigrateProps<From, To>) => Promise<void>;
34
+ };
35
+
36
+ /**
37
+ * Context passed to object migration callbacks.
38
+ */
39
+ export type ObjectMigrationContext = {
40
+ db: Database.Database;
41
+ };
42
+
43
+ type OnMigrateProps<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
44
+ before: Schema.Schema.Type<From>;
45
+ object: Schema.Schema.Type<To>;
46
+ db: Database.Database;
47
+ };
48
+
49
+ /**
50
+ * Definition of a migration from one object schema version to another.
51
+ */
52
+ export type ObjectMigration = {
53
+ fromType: DXN;
54
+ toType: DXN;
55
+ fromSchema: Schema.Schema.AnyNoContext;
56
+ toSchema: Schema.Schema.AnyNoContext;
57
+ transform: (from: unknown, context: ObjectMigrationContext) => Promise<unknown>;
58
+ onMigration: (params: OnMigrateProps<any, any>) => Promise<void>;
59
+ };
60
+
61
+ /**
62
+ * Define a migration between two object schemas.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const migration = Migration.define({
67
+ * from: ContactV1,
68
+ * to: ContactV2,
69
+ * transform: async (from) => ({ name: `${from.firstName} ${from.lastName}` }),
70
+ * onMigration: async () => {},
71
+ * });
72
+ * ```
73
+ */
74
+ export const define = <From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext>(
75
+ options: DefineObjectMigrationOptions<From, To>,
76
+ ): ObjectMigration => {
77
+ const fromType = getSchemaDXN(options.from);
78
+ if (!fromType) {
79
+ throw new Error('Invalid from schema');
80
+ }
81
+ const toType = getSchemaDXN(options.to);
82
+ if (!toType) {
83
+ throw new Error('Invalid to schema');
84
+ }
85
+
86
+ return {
87
+ fromType,
88
+ toType,
89
+ fromSchema: options.from,
90
+ toSchema: options.to,
91
+ transform: options.transform as any,
92
+ onMigration: options.onMigration as any,
93
+ };
94
+ };
package/src/Obj.test.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expectTypeOf, test } from 'vitest';
5
+ import { describe, expectTypeOf, test, expect } from 'vitest';
6
6
 
7
7
  import * as Entity from './Entity';
8
8
  import { SnapshotKindId } from './internal';
@@ -117,8 +117,8 @@ describe('Obj', () => {
117
117
 
118
118
  test('returns keys from reactive object and snapshot', ({ expect }) => {
119
119
  const obj = Obj.make(TestSchema.Person, { name: 'Test' });
120
- Obj.change(obj, (o) => {
121
- const meta = Obj.getMeta(o);
120
+ Obj.update(obj, (obj) => {
121
+ const meta = Obj.getMeta(obj);
122
122
  meta.keys.push({ source: SOURCE, id: 'key-1' });
123
123
  meta.keys.push({ source: SOURCE, id: 'key-2' });
124
124
  meta.keys.push({ source: 'other', id: 'key-3' });
@@ -166,8 +166,8 @@ describe('Obj', () => {
166
166
 
167
167
  const cloned = Obj.clone(original);
168
168
 
169
- Obj.change(original, (p) => {
170
- p.name = 'Bob';
169
+ Obj.update(original, (original) => {
170
+ original.name = 'Bob';
171
171
  });
172
172
 
173
173
  expect(original.name).toBe('Bob');
@@ -206,8 +206,8 @@ describe('Obj', () => {
206
206
  expect(cloned.employer?.target).toBe(person.employer?.target);
207
207
 
208
208
  // Modifying the referenced object affects both
209
- Obj.change(employer, (org) => {
210
- org.name = 'Updated DXOS';
209
+ Obj.update(employer, (employer) => {
210
+ employer.name = 'Updated DXOS';
211
211
  });
212
212
 
213
213
  expect(cloned.employer?.target?.name).toBe('Updated DXOS');
@@ -234,8 +234,8 @@ describe('Obj', () => {
234
234
  expect(cloned.employer?.target?.name).toBe(employer.name);
235
235
 
236
236
  // Modifying the original referenced object does not affect the clone
237
- Obj.change(employer, (org) => {
238
- org.name = 'Updated DXOS';
237
+ Obj.update(employer, (employer) => {
238
+ employer.name = 'Updated DXOS';
239
239
  });
240
240
 
241
241
  expect(cloned.employer?.target?.name).toBe('DXOS');
@@ -333,8 +333,8 @@ describe('Obj', () => {
333
333
  expect(cloned.address?.coordinates.lng).toBe(-122.4194);
334
334
 
335
335
  // Modifying nested properties should be independent
336
- Obj.change(person, (p) => {
337
- p.address!.city = 'New York';
336
+ Obj.update(person, (person) => {
337
+ person.address!.city = 'New York';
338
338
  });
339
339
 
340
340
  expect(cloned.address?.city).toBe('San Francisco');
@@ -366,8 +366,8 @@ describe('Obj', () => {
366
366
  expect(cloned.tasks?.[2]?.target).not.toBe(task3);
367
367
 
368
368
  // Modifying original tasks should not affect cloned ones
369
- Obj.change(task1, (t) => {
370
- t.title = 'Updated Task 1';
369
+ Obj.update(task1, (task1) => {
370
+ task1.title = 'Updated Task 1';
371
371
  });
372
372
 
373
373
  expect(cloned.tasks?.[0]?.target?.title).toBe('Task 1');
@@ -444,4 +444,96 @@ describe('Obj', () => {
444
444
  expectTypeOf<Obj.Any>().toMatchTypeOf<Obj.Unknown>();
445
445
  });
446
446
  });
447
+
448
+ describe('Obj.updateFrom', () => {
449
+ test('returns false when values already match', () => {
450
+ const target = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
451
+ const source = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
452
+ Obj.update(target, (target) => {
453
+ expect(Obj.updateFrom(target, source)).toBe(false);
454
+ });
455
+ });
456
+
457
+ test('updates scalar and nested record fields on Organization', () => {
458
+ const target = Obj.make(TestSchema.Organization, {
459
+ name: 'Old',
460
+ properties: { a: '1' },
461
+ });
462
+ const source = Obj.make(TestSchema.Organization, {
463
+ name: 'New',
464
+ properties: { a: '2', b: '3' },
465
+ });
466
+ Obj.update(target, (target) => {
467
+ expect(Obj.updateFrom(target, source)).toBe(true);
468
+ });
469
+ expect(target.name).toBe('New');
470
+ expect(target.properties).toEqual({ a: '2', b: '3' });
471
+ });
472
+
473
+ test('compares employer refs by DXN and updates Person fields', () => {
474
+ const orgA = Obj.make(TestSchema.Organization, { name: 'A' });
475
+ const orgB = Obj.make(TestSchema.Organization, { name: 'B' });
476
+ const target = Obj.make(TestSchema.Person, {
477
+ name: 'Ann',
478
+ username: 'ann',
479
+ email: 'ann@x.test',
480
+ employer: Ref.make(orgA),
481
+ address: { city: 'X', state: 'Y', zip: '1', coordinates: { lat: 0, lng: 0 } },
482
+ });
483
+ const source = Obj.make(TestSchema.Person, {
484
+ name: 'Ann',
485
+ username: 'ann',
486
+ email: 'ann@x.test',
487
+ employer: Ref.make(orgB),
488
+ address: { city: 'Portland', state: 'OR', zip: '97201', coordinates: { lat: 45.5, lng: -122.6 } },
489
+ });
490
+ Obj.update(target, (target) => {
491
+ expect(Obj.updateFrom(target, source)).toBe(true);
492
+ });
493
+ expect(target.employer?.dxn.toString()).toBe(Ref.make(orgB).dxn.toString());
494
+ expect(target.address?.city).toBe('Portland');
495
+ });
496
+
497
+ test('updates array of task refs when DXNs differ', () => {
498
+ const t1 = Obj.make(TestSchema.Task, { title: 'One' });
499
+ const t2 = Obj.make(TestSchema.Task, { title: 'Two' });
500
+ const t3 = Obj.make(TestSchema.Task, { title: 'Three' });
501
+ const target = Obj.make(TestSchema.Person, {
502
+ name: 'Bob',
503
+ username: 'bob',
504
+ email: 'bob@x.test',
505
+ tasks: [Ref.make(t1), Ref.make(t2)],
506
+ });
507
+ const source = Obj.make(TestSchema.Person, {
508
+ name: 'Bob',
509
+ username: 'bob',
510
+ email: 'bob@x.test',
511
+ tasks: [Ref.make(t1), Ref.make(t3)],
512
+ });
513
+ Obj.update(target, (target) => {
514
+ expect(Obj.updateFrom(target, source)).toBe(true);
515
+ });
516
+ expect(target.tasks?.map((r) => r.dxn.toString())).toEqual(source.tasks?.map((r) => r.dxn.toString()));
517
+ });
518
+
519
+ test('respects include option', () => {
520
+ const target = Obj.make(TestSchema.Organization, { name: 'Keep', properties: { x: '1' } });
521
+ const source = Obj.make(TestSchema.Organization, { name: 'Drop', properties: { x: '2' } });
522
+ Obj.update(target, (target) => {
523
+ expect(Obj.updateFrom(target, source, { include: ['properties'] })).toBe(true);
524
+ });
525
+ expect(target.name).toBe('Keep');
526
+ expect(target.properties).toEqual({ x: '2' });
527
+ });
528
+
529
+ test('respects exclude option', () => {
530
+ const target = Obj.make(TestSchema.Organization, { name: 'Old', properties: { x: '1' } });
531
+ const source = Obj.make(TestSchema.Organization, { name: 'New', properties: { x: '2' } });
532
+ Obj.update(target, (target) => {
533
+ expect(Obj.updateFrom(target, source, { exclude: ['name'] })).toBe(true);
534
+ });
535
+ expect(target.name).toBe('Old');
536
+ expect(target.properties).toEqual({ x: '2' });
537
+ });
538
+ });
447
539
  });