@dxos/echo 0.8.4-main.74a063c4e0 → 0.8.4-main.765dc60934

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 (248) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +3 -3
  3. package/dist/lib/neutral/Annotation.mjs +5 -3
  4. package/dist/lib/neutral/Database.mjs +6 -4
  5. package/dist/lib/neutral/Entity.mjs +16 -14
  6. package/dist/lib/neutral/Err.mjs +1 -1
  7. package/dist/lib/neutral/Extension.mjs +1 -1
  8. package/dist/lib/neutral/Feed.mjs +21 -17
  9. package/dist/lib/neutral/Filter.mjs +15 -11
  10. package/dist/lib/neutral/Format.mjs +3 -3
  11. package/dist/lib/neutral/JsonSchema.mjs +8 -8
  12. package/dist/lib/neutral/Key.mjs +1 -1
  13. package/dist/lib/neutral/Migration.mjs +17 -0
  14. package/dist/lib/neutral/Migration.mjs.map +7 -0
  15. package/dist/lib/neutral/Obj.mjs +14 -14
  16. package/dist/lib/neutral/Order.mjs +1 -1
  17. package/dist/lib/neutral/Query.mjs +17 -17
  18. package/dist/lib/neutral/QueryResult.mjs +1 -1
  19. package/dist/lib/neutral/Ref.mjs +9 -7
  20. package/dist/lib/neutral/Relation.mjs +15 -15
  21. package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
  22. package/dist/lib/neutral/Tag.mjs +14 -14
  23. package/dist/lib/neutral/Type.mjs +10 -10
  24. package/dist/lib/neutral/{chunk-GZQTCRJB.mjs → chunk-44HT3MEC.mjs} +2 -2
  25. package/dist/lib/neutral/{chunk-ZIXGDU6F.mjs → chunk-5SL5LDLD.mjs} +4 -2
  26. package/dist/lib/neutral/chunk-5SL5LDLD.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-ANHVGJI4.mjs → chunk-7RVZT53K.mjs} +1 -1
  28. package/dist/lib/neutral/{chunk-MYMU4TWK.mjs → chunk-7VWWUTRQ.mjs} +38 -17
  29. package/dist/lib/neutral/chunk-7VWWUTRQ.mjs.map +7 -0
  30. package/dist/lib/neutral/{chunk-66GBFH43.mjs → chunk-A6IYRT5A.mjs} +17 -45
  31. package/dist/lib/neutral/chunk-A6IYRT5A.mjs.map +7 -0
  32. package/dist/lib/neutral/{chunk-R72KFH2X.mjs → chunk-APHSOTIX.mjs} +2 -2
  33. package/dist/lib/neutral/{chunk-XZJUNTML.mjs → chunk-APJKDGFL.mjs} +36 -6
  34. package/dist/lib/neutral/chunk-APJKDGFL.mjs.map +7 -0
  35. package/dist/lib/neutral/{chunk-BNCCGLJN.mjs → chunk-BICZKPQG.mjs} +1 -1
  36. package/dist/lib/neutral/{chunk-QWAOTFCY.mjs → chunk-BMB7IHGB.mjs} +16 -66
  37. package/dist/lib/neutral/chunk-BMB7IHGB.mjs.map +7 -0
  38. package/dist/lib/neutral/{chunk-KPY6FC5I.mjs → chunk-BVOWYVTD.mjs} +17 -12
  39. package/dist/lib/neutral/chunk-BVOWYVTD.mjs.map +7 -0
  40. package/dist/lib/neutral/chunk-FIWO2FZK.mjs +36 -0
  41. package/dist/lib/neutral/chunk-FIWO2FZK.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-WMFVS4KR.mjs → chunk-G54OX4IX.mjs} +102 -480
  43. package/dist/lib/neutral/chunk-G54OX4IX.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-7QQMUDE6.mjs → chunk-GMS5PKGQ.mjs} +59 -12
  45. package/dist/lib/neutral/chunk-GMS5PKGQ.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs → chunk-GWFFC34K.mjs} +1 -1
  47. package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs.map → chunk-GWFFC34K.mjs.map} +1 -1
  48. package/dist/lib/neutral/{chunk-EBVB5NOH.mjs → chunk-I2DARWPX.mjs} +15 -20
  49. package/dist/lib/neutral/chunk-I2DARWPX.mjs.map +7 -0
  50. package/dist/lib/neutral/{chunk-FNEFSO2C.mjs → chunk-MLS7U7AT.mjs} +12 -65
  51. package/dist/lib/neutral/{chunk-FNEFSO2C.mjs.map → chunk-MLS7U7AT.mjs.map} +2 -2
  52. package/dist/lib/neutral/{chunk-TBKX6JQO.mjs → chunk-N4B7FHQT.mjs} +1 -1
  53. package/dist/lib/neutral/{chunk-46QNGNTY.mjs → chunk-N7VOEPSV.mjs} +6 -3
  54. package/dist/lib/neutral/{chunk-46QNGNTY.mjs.map → chunk-N7VOEPSV.mjs.map} +3 -3
  55. package/dist/lib/neutral/{chunk-MZXWVR3T.mjs → chunk-PSZBLH53.mjs} +6 -22
  56. package/dist/lib/neutral/{chunk-MZXWVR3T.mjs.map → chunk-PSZBLH53.mjs.map} +2 -2
  57. package/dist/lib/neutral/{chunk-XZM7FERR.mjs → chunk-QBIKZ3TY.mjs} +4 -4
  58. package/dist/lib/neutral/{chunk-E5PBQJWV.mjs → chunk-QRZ2I3ZM.mjs} +2 -2
  59. package/dist/lib/neutral/{chunk-2U2BZDLU.mjs → chunk-SCPFDS2E.mjs} +12 -8
  60. package/dist/lib/neutral/chunk-SCPFDS2E.mjs.map +7 -0
  61. package/dist/lib/neutral/{chunk-VCAP4OOT.mjs → chunk-SKDTKHDC.mjs} +21 -5
  62. package/dist/lib/neutral/chunk-SKDTKHDC.mjs.map +7 -0
  63. package/dist/lib/neutral/{chunk-6URFBQJH.mjs → chunk-TNBK56IN.mjs} +11 -22
  64. package/dist/lib/neutral/chunk-TNBK56IN.mjs.map +7 -0
  65. package/dist/lib/neutral/{chunk-DQYLD2RB.mjs → chunk-TRPZU2HV.mjs} +2 -2
  66. package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
  67. package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
  68. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs → chunk-ZISMEVKD.mjs} +1 -1
  69. package/dist/lib/neutral/{chunk-UBEZSGXY.mjs.map → chunk-ZISMEVKD.mjs.map} +2 -2
  70. package/dist/lib/neutral/index.mjs +33 -27
  71. package/dist/lib/neutral/internal/index.mjs +11 -9
  72. package/dist/lib/neutral/meta.json +1 -1
  73. package/dist/lib/neutral/testing/index.mjs +183 -128
  74. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  75. package/dist/types/src/Annotation.d.ts +1 -1
  76. package/dist/types/src/Annotation.d.ts.map +1 -1
  77. package/dist/types/src/Collection.d.ts.map +1 -1
  78. package/dist/types/src/Database.d.ts +6 -1
  79. package/dist/types/src/Database.d.ts.map +1 -1
  80. package/dist/types/src/Dataset.d.ts +1 -1
  81. package/dist/types/src/Entity.d.ts +15 -9
  82. package/dist/types/src/Entity.d.ts.map +1 -1
  83. package/dist/types/src/Err.d.ts +18 -18
  84. package/dist/types/src/Err.d.ts.map +1 -1
  85. package/dist/types/src/Extension.d.ts +4 -4
  86. package/dist/types/src/Extension.d.ts.map +1 -1
  87. package/dist/types/src/Feed.d.ts +37 -2
  88. package/dist/types/src/Feed.d.ts.map +1 -1
  89. package/dist/types/src/Filter.d.ts +34 -0
  90. package/dist/types/src/Filter.d.ts.map +1 -1
  91. package/dist/types/src/Hypergraph.d.ts +3 -3
  92. package/dist/types/src/Hypergraph.d.ts.map +1 -1
  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 +69 -0
  99. package/dist/types/src/Migration.d.ts.map +1 -0
  100. package/dist/types/src/Obj.d.ts +23 -21
  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 +5 -1
  104. package/dist/types/src/Query.d.ts.map +1 -1
  105. package/dist/types/src/Ref.d.ts +1 -0
  106. package/dist/types/src/Ref.d.ts.map +1 -1
  107. package/dist/types/src/Relation.d.ts +15 -15
  108. package/dist/types/src/Relation.d.ts.map +1 -1
  109. package/dist/types/src/Tag.d.ts +2 -2
  110. package/dist/types/src/Tag.d.ts.map +1 -1
  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/exemplars.test.d.ts +2 -0
  115. package/dist/types/src/exemplars.test.d.ts.map +1 -0
  116. package/dist/types/src/index.d.ts +2 -0
  117. package/dist/types/src/index.d.ts.map +1 -1
  118. package/dist/types/src/internal/Annotation/annotations.d.ts +11 -2
  119. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  120. package/dist/types/src/internal/Annotation/sorting.d.ts.map +1 -1
  121. package/dist/types/src/internal/Annotation/util.d.ts +1 -1
  122. package/dist/types/src/internal/Annotation/util.d.ts.map +1 -1
  123. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  124. package/dist/types/src/internal/Entity/model.d.ts +2 -0
  125. package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
  126. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  127. package/dist/types/src/internal/Entity/version.d.ts.map +1 -1
  128. package/dist/types/src/internal/Format/date.d.ts.map +1 -1
  129. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  130. package/dist/types/src/internal/Format/number.d.ts.map +1 -1
  131. package/dist/types/src/internal/Format/object.d.ts.map +1 -1
  132. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  133. package/dist/types/src/internal/JsonSchema/json-schema-normalize.d.ts.map +1 -1
  134. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +28 -28
  135. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
  136. package/dist/types/src/internal/JsonSchema/json-schema.d.ts +1 -1
  137. package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
  138. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  139. package/dist/types/src/internal/Obj/common.d.ts.map +1 -1
  140. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  141. package/dist/types/src/internal/Obj/deleted.d.ts.map +1 -1
  142. package/dist/types/src/internal/Obj/ids.d.ts +1 -1
  143. package/dist/types/src/internal/Obj/ids.d.ts.map +1 -1
  144. package/dist/types/src/internal/Obj/json-serializer.d.ts +2 -1
  145. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  146. package/dist/types/src/internal/Obj/set-value.d.ts +1 -1
  147. package/dist/types/src/internal/Obj/set-value.d.ts.map +1 -1
  148. package/dist/types/src/internal/Obj/snapshot.d.ts.map +1 -1
  149. package/dist/types/src/internal/Query.d.ts.map +1 -1
  150. package/dist/types/src/internal/Ref/ref.d.ts +13 -0
  151. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  152. package/dist/types/src/internal/Type/compose.d.ts.map +1 -1
  153. package/dist/types/src/internal/Type/echo-schema.d.ts +1 -1
  154. package/dist/types/src/internal/Type/echo-schema.d.ts.map +1 -1
  155. package/dist/types/src/internal/Type/manipulation.d.ts.map +1 -1
  156. package/dist/types/src/internal/common/api/meta.d.ts +3 -3
  157. package/dist/types/src/internal/common/api/meta.d.ts.map +1 -1
  158. package/dist/types/src/internal/common/proxy/change-context.d.ts +1 -1
  159. package/dist/types/src/internal/common/proxy/change-context.d.ts.map +1 -1
  160. package/dist/types/src/internal/common/proxy/define-hidden-property.d.ts.map +1 -1
  161. package/dist/types/src/internal/common/proxy/errors.d.ts +1 -1
  162. package/dist/types/src/internal/common/proxy/errors.d.ts.map +1 -1
  163. package/dist/types/src/internal/common/proxy/event-batch.d.ts.map +1 -1
  164. package/dist/types/src/internal/common/proxy/json-serializer.d.ts.map +1 -1
  165. package/dist/types/src/internal/common/proxy/ownership.d.ts.map +1 -1
  166. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  167. package/dist/types/src/internal/common/proxy/reactive-array.d.ts +1 -1
  168. package/dist/types/src/internal/common/proxy/reactive.d.ts +1 -1
  169. package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
  170. package/dist/types/src/internal/common/proxy/reactive.test.d.ts +2 -0
  171. package/dist/types/src/internal/common/proxy/reactive.test.d.ts.map +1 -0
  172. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  173. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  174. package/dist/types/src/internal/common/types/base.d.ts.map +1 -1
  175. package/dist/types/src/internal/common/types/entity.d.ts +3 -3
  176. package/dist/types/src/internal/common/types/meta.d.ts +10 -0
  177. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  178. package/dist/types/src/internal/common/types/version.d.ts +1 -1
  179. package/dist/types/src/testing/test-data.d.ts +8 -8
  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 +19 -14
  186. package/src/Annotation.ts +1 -0
  187. package/src/Collection.ts +1 -1
  188. package/src/Database.ts +36 -14
  189. package/src/Entity.ts +16 -9
  190. package/src/Extension.ts +3 -3
  191. package/src/Feed.ts +54 -2
  192. package/src/Filter.ts +61 -0
  193. package/src/Hypergraph.ts +3 -3
  194. package/src/Json.test.ts +175 -0
  195. package/src/Json.ts +102 -0
  196. package/src/Migration.ts +106 -0
  197. package/src/Obj.test.ts +23 -23
  198. package/src/Obj.ts +28 -25
  199. package/src/Query.test.ts +129 -5
  200. package/src/Query.ts +32 -7
  201. package/src/Ref.ts +2 -0
  202. package/src/Relation.ts +21 -17
  203. package/src/exemplars.test.ts +21 -0
  204. package/src/index.ts +3 -0
  205. package/src/internal/Annotation/annotations.ts +19 -7
  206. package/src/internal/Entity/model.ts +2 -0
  207. package/src/internal/Obj/ids.ts +1 -1
  208. package/src/internal/Obj/json-serializer.test.ts +2 -2
  209. package/src/internal/Obj/json-serializer.ts +17 -10
  210. package/src/internal/Obj/set-value.test.ts +24 -24
  211. package/src/internal/Obj/set-value.ts +1 -1
  212. package/src/internal/Query.ts +28 -4
  213. package/src/internal/Ref/ref.ts +17 -0
  214. package/src/internal/Type/echo-schema.ts +1 -1
  215. package/src/internal/common/README.md +1 -1
  216. package/src/internal/common/api/meta.ts +3 -3
  217. package/src/internal/common/proxy/change-context.ts +1 -1
  218. package/src/internal/common/proxy/change.test.ts +66 -66
  219. package/src/internal/common/proxy/errors.ts +2 -2
  220. package/src/internal/common/proxy/reactive-array.ts +1 -1
  221. package/src/internal/common/proxy/reactive.test.ts +54 -0
  222. package/src/internal/common/proxy/reactive.ts +11 -2
  223. package/src/internal/common/proxy/typed-handler.ts +7 -7
  224. package/src/internal/common/proxy/typed-object.test.ts +1 -1
  225. package/src/internal/common/types/meta.ts +12 -0
  226. package/src/testing/test-data.ts +157 -97
  227. package/dist/lib/neutral/chunk-2U2BZDLU.mjs.map +0 -7
  228. package/dist/lib/neutral/chunk-66GBFH43.mjs.map +0 -7
  229. package/dist/lib/neutral/chunk-6URFBQJH.mjs.map +0 -7
  230. package/dist/lib/neutral/chunk-7QQMUDE6.mjs.map +0 -7
  231. package/dist/lib/neutral/chunk-EBVB5NOH.mjs.map +0 -7
  232. package/dist/lib/neutral/chunk-KPY6FC5I.mjs.map +0 -7
  233. package/dist/lib/neutral/chunk-MYMU4TWK.mjs.map +0 -7
  234. package/dist/lib/neutral/chunk-QWAOTFCY.mjs.map +0 -7
  235. package/dist/lib/neutral/chunk-VCAP4OOT.mjs.map +0 -7
  236. package/dist/lib/neutral/chunk-WMFVS4KR.mjs.map +0 -7
  237. package/dist/lib/neutral/chunk-XZJUNTML.mjs.map +0 -7
  238. package/dist/lib/neutral/chunk-ZIXGDU6F.mjs.map +0 -7
  239. /package/dist/lib/neutral/{chunk-GZQTCRJB.mjs.map → chunk-44HT3MEC.mjs.map} +0 -0
  240. /package/dist/lib/neutral/{chunk-ANHVGJI4.mjs.map → chunk-7RVZT53K.mjs.map} +0 -0
  241. /package/dist/lib/neutral/{chunk-R72KFH2X.mjs.map → chunk-APHSOTIX.mjs.map} +0 -0
  242. /package/dist/lib/neutral/{chunk-BNCCGLJN.mjs.map → chunk-BICZKPQG.mjs.map} +0 -0
  243. /package/dist/lib/neutral/{chunk-TBKX6JQO.mjs.map → chunk-N4B7FHQT.mjs.map} +0 -0
  244. /package/dist/lib/neutral/{chunk-XZM7FERR.mjs.map → chunk-QBIKZ3TY.mjs.map} +0 -0
  245. /package/dist/lib/neutral/{chunk-E5PBQJWV.mjs.map → chunk-QRZ2I3ZM.mjs.map} +0 -0
  246. /package/dist/lib/neutral/{chunk-DQYLD2RB.mjs.map → chunk-TRPZU2HV.mjs.map} +0 -0
  247. /package/dist/lib/neutral/{chunk-UI6MWK5W.mjs.map → chunk-TTCSATUD.mjs.map} +0 -0
  248. /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,106 @@
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 { MetaId, type ObjectMeta, getSchemaDXN } from './internal';
14
+
15
+ /**
16
+ * Result returned by a migration's `transform` callback.
17
+ * The data shape matches the target schema; the optional `[Obj.Meta]` symbol key lets the
18
+ * transform update the object's meta (e.g. `key` / `version`) atomically with the data swap.
19
+ */
20
+ export type TransformResult<To extends Schema.Schema.AnyNoContext> = Omit<
21
+ Schema.Schema.Type<To>,
22
+ 'id' | Entity.KindId
23
+ > & {
24
+ [MetaId]?: Partial<ObjectMeta>;
25
+ };
26
+
27
+ type DefineObjectMigrationOptions<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
28
+ from: From;
29
+ to: To;
30
+ /**
31
+ * Pure function that converts the old object data to the new object data.
32
+ *
33
+ * The returned object may include an optional `[Obj.Meta]` entry to update the object's meta
34
+ * (e.g. registry `key` / `version`) atomically with the data swap.
35
+ */
36
+ // TODO(dmaretskyi): `id` should not be a part of the schema.
37
+ transform: (from: Schema.Schema.Type<From>, context: ObjectMigrationContext) => Promise<TransformResult<To>>;
38
+
39
+ /**
40
+ * Callback that is called after the object is migrated. Called for every object that is migrated.
41
+ *
42
+ * NOTE: Database mutations performed in this callback are not guaranteed to be idempotent.
43
+ * If multiple peers run the migration separately, the effects may be applied multiple times.
44
+ */
45
+ onMigration?: (params: OnMigrateProps<From, To>) => Promise<void>;
46
+ };
47
+
48
+ /**
49
+ * Context passed to object migration callbacks.
50
+ */
51
+ export type ObjectMigrationContext = {
52
+ db: Database.Database;
53
+ };
54
+
55
+ type OnMigrateProps<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
56
+ before: Schema.Schema.Type<From>;
57
+ object: Schema.Schema.Type<To>;
58
+ db: Database.Database;
59
+ };
60
+
61
+ /**
62
+ * Definition of a migration from one object schema version to another.
63
+ */
64
+ export type ObjectMigration = {
65
+ fromType: DXN;
66
+ toType: DXN;
67
+ fromSchema: Schema.Schema.AnyNoContext;
68
+ toSchema: Schema.Schema.AnyNoContext;
69
+ transform: (from: unknown, context: ObjectMigrationContext) => Promise<unknown>;
70
+ onMigration?: (params: OnMigrateProps<any, any>) => Promise<void>;
71
+ };
72
+
73
+ /**
74
+ * Define a migration between two object schemas.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const migration = Migration.define({
79
+ * from: ContactV1,
80
+ * to: ContactV2,
81
+ * transform: async (from) => ({ name: `${from.firstName} ${from.lastName}` }),
82
+ * onMigration: async () => {},
83
+ * });
84
+ * ```
85
+ */
86
+ export const define = <From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext>(
87
+ options: DefineObjectMigrationOptions<From, To>,
88
+ ): ObjectMigration => {
89
+ const fromType = getSchemaDXN(options.from);
90
+ if (!fromType) {
91
+ throw new Error('Invalid from schema');
92
+ }
93
+ const toType = getSchemaDXN(options.to);
94
+ if (!toType) {
95
+ throw new Error('Invalid to schema');
96
+ }
97
+
98
+ return {
99
+ fromType,
100
+ toType,
101
+ fromSchema: options.from,
102
+ toSchema: options.to,
103
+ transform: options.transform as any,
104
+ onMigration: options.onMigration as any,
105
+ };
106
+ };
package/src/Obj.test.ts CHANGED
@@ -117,7 +117,7 @@ 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, (obj) => {
120
+ Obj.update(obj, (obj) => {
121
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' });
@@ -166,8 +166,8 @@ describe('Obj', () => {
166
166
 
167
167
  const cloned = Obj.clone(original);
168
168
 
169
- Obj.change(original, (obj) => {
170
- obj.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, (obj) => {
337
- obj.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, (obj) => {
370
- obj.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');
@@ -449,8 +449,8 @@ describe('Obj', () => {
449
449
  test('returns false when values already match', () => {
450
450
  const target = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
451
451
  const source = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
452
- Obj.change(target, (mutable) => {
453
- expect(Obj.updateFrom(mutable, source)).toBe(false);
452
+ Obj.update(target, (target) => {
453
+ expect(Obj.updateFrom(target, source)).toBe(false);
454
454
  });
455
455
  });
456
456
 
@@ -463,8 +463,8 @@ describe('Obj', () => {
463
463
  name: 'New',
464
464
  properties: { a: '2', b: '3' },
465
465
  });
466
- Obj.change(target, (mutable) => {
467
- expect(Obj.updateFrom(mutable, source)).toBe(true);
466
+ Obj.update(target, (target) => {
467
+ expect(Obj.updateFrom(target, source)).toBe(true);
468
468
  });
469
469
  expect(target.name).toBe('New');
470
470
  expect(target.properties).toEqual({ a: '2', b: '3' });
@@ -487,8 +487,8 @@ describe('Obj', () => {
487
487
  employer: Ref.make(orgB),
488
488
  address: { city: 'Portland', state: 'OR', zip: '97201', coordinates: { lat: 45.5, lng: -122.6 } },
489
489
  });
490
- Obj.change(target, (mutable) => {
491
- expect(Obj.updateFrom(mutable, source)).toBe(true);
490
+ Obj.update(target, (target) => {
491
+ expect(Obj.updateFrom(target, source)).toBe(true);
492
492
  });
493
493
  expect(target.employer?.dxn.toString()).toBe(Ref.make(orgB).dxn.toString());
494
494
  expect(target.address?.city).toBe('Portland');
@@ -510,8 +510,8 @@ describe('Obj', () => {
510
510
  email: 'bob@x.test',
511
511
  tasks: [Ref.make(t1), Ref.make(t3)],
512
512
  });
513
- Obj.change(target, (mutable) => {
514
- expect(Obj.updateFrom(mutable, source)).toBe(true);
513
+ Obj.update(target, (target) => {
514
+ expect(Obj.updateFrom(target, source)).toBe(true);
515
515
  });
516
516
  expect(target.tasks?.map((r) => r.dxn.toString())).toEqual(source.tasks?.map((r) => r.dxn.toString()));
517
517
  });
@@ -519,8 +519,8 @@ describe('Obj', () => {
519
519
  test('respects include option', () => {
520
520
  const target = Obj.make(TestSchema.Organization, { name: 'Keep', properties: { x: '1' } });
521
521
  const source = Obj.make(TestSchema.Organization, { name: 'Drop', properties: { x: '2' } });
522
- Obj.change(target, (mutable) => {
523
- expect(Obj.updateFrom(mutable, source, { include: ['properties'] })).toBe(true);
522
+ Obj.update(target, (target) => {
523
+ expect(Obj.updateFrom(target, source, { include: ['properties'] })).toBe(true);
524
524
  });
525
525
  expect(target.name).toBe('Keep');
526
526
  expect(target.properties).toEqual({ x: '2' });
@@ -529,8 +529,8 @@ describe('Obj', () => {
529
529
  test('respects exclude option', () => {
530
530
  const target = Obj.make(TestSchema.Organization, { name: 'Old', properties: { x: '1' } });
531
531
  const source = Obj.make(TestSchema.Organization, { name: 'New', properties: { x: '2' } });
532
- Obj.change(target, (mutable) => {
533
- expect(Obj.updateFrom(mutable, source, { exclude: ['name'] })).toBe(true);
532
+ Obj.update(target, (target) => {
533
+ expect(Obj.updateFrom(target, source, { exclude: ['name'] })).toBe(true);
534
534
  });
535
535
  expect(target.name).toBe('Old');
536
536
  expect(target.properties).toEqual({ x: '2' });
package/src/Obj.ts CHANGED
@@ -13,7 +13,7 @@ import * as Utils from 'effect/Utils';
13
13
 
14
14
  import type { ForeignKey } from '@dxos/echo-protocol';
15
15
  import { createJsonPath } from '@dxos/effect';
16
- import { assertArgument } from '@dxos/invariant';
16
+ import { assertArgument, invariant } from '@dxos/invariant';
17
17
  import { type DXN, ObjectId } from '@dxos/keys';
18
18
  import { assumeType, deepMapValues } from '@dxos/util';
19
19
 
@@ -304,7 +304,7 @@ export const clone: <T extends Unknown>(obj: T, opts?: CloneOptions) => T = objI
304
304
 
305
305
  /**
306
306
  * Makes all properties mutable recursively.
307
- * Used to provide a mutable view of an object within `Obj.change`.
307
+ * Used to provide a mutable view of an object within `Obj.update`.
308
308
  */
309
309
  export type Mutable<T> = internal.Mutable<T>;
310
310
 
@@ -312,33 +312,33 @@ export type Mutable<T> = internal.Mutable<T>;
312
312
  * Perform mutations on an echo object within a controlled context.
313
313
  *
314
314
  * All mutations within the callback are batched and trigger a single notification
315
- * when the callback completes. Direct mutations outside of `Obj.change` will throw
315
+ * when the callback completes. Direct mutations outside of `Obj.update` will throw
316
316
  * an error for echo objects.
317
317
  *
318
318
  * This function also works with nested objects within echo objects (e.g., Template structs)
319
319
  * that are reactive at runtime.
320
320
  *
321
- * @param obj - The echo object to mutate. Use `Relation.change` for relations.
321
+ * @param obj - The echo object to mutate. Use `Relation.update` for relations.
322
322
  * @param callback - The callback that performs mutations on the object.
323
323
  *
324
324
  * @example
325
325
  * ```ts
326
326
  * const person = Obj.make(Person, { name: 'John', age: 25 });
327
327
  *
328
- * // Mutate within Obj.change
329
- * Obj.change(person, (obj) => {
328
+ * // Mutate within Obj.update
329
+ * Obj.update(person, (obj) => {
330
330
  * obj.name = 'Jane';
331
331
  * obj.age = 30;
332
332
  * });
333
333
  * // ONE notification fires here
334
334
  *
335
335
  * // Direct mutation throws
336
- * person.name = 'Bob'; // Error: Cannot modify outside Obj.change()
336
+ * person.name = 'Bob'; // Error: Cannot modify outside Obj.update()
337
337
  * ```
338
338
  *
339
- * Note: Only accepts objects. Use `Relation.change` for relations.
339
+ * Note: Only accepts objects. Use `Relation.update` for relations.
340
340
  */
341
- export const change = <T extends Unknown>(obj: T, callback: internal.ChangeCallback<T>): void => {
341
+ export const update = <T extends Unknown>(obj: T, callback: internal.ChangeCallback<T>): void => {
342
342
  internal.change(obj, callback);
343
343
  };
344
344
 
@@ -373,7 +373,7 @@ export const getValue = (obj: Unknown | Snapshot, path: readonly (string | numbe
373
373
  * whether to initialize nested data as an empty object or array.
374
374
  *
375
375
  * Similar to lodash.set and setDeep from @dxos/util, but schema-aware.
376
- * Must be called within an `Obj.change` callback.
376
+ * Must be called within an `Obj.update` callback.
377
377
  *
378
378
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
379
379
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -387,7 +387,7 @@ export const getValue = (obj: Unknown | Snapshot, path: readonly (string | numbe
387
387
  * ```ts
388
388
  * const person = Obj.make(Person, { name: 'John' });
389
389
  * // Person schema has: addresses: Schema.Array(Address)
390
- * Obj.change(person, (obj) => {
390
+ * Obj.update(person, (obj) => {
391
391
  * Obj.setValue(obj, ['addresses', 0, 'street'], '123 Main St');
392
392
  * });
393
393
  * // Creates: person.addresses = [{ street: '123 Main St' }]
@@ -470,16 +470,19 @@ export const getDXN = (entity: Unknown | Snapshot): DXN => {
470
470
  /**
471
471
  * @returns The DXN of the object's type.
472
472
  * @example dxn:com.example.type.person:1.0.0
473
+ * @throws If the object is missing its type (corrupted object).
473
474
  */
474
- // TODO(wittjosiah): Narrow types.
475
- export const getTypeDXN: (obj: unknown | undefined) => DXN | undefined = internal.getTypeDXN as any;
475
+ export const getTypeDXN = (obj: Unknown | Snapshot): DXN => {
476
+ const type = internal.getTypeDXN(obj);
477
+ invariant(type != null, 'Corrupted object: missing type.');
478
+ return type;
479
+ };
476
480
 
477
481
  /**
478
482
  * Get the schema of the object.
479
483
  * Returns the branded ECHO schema used to create the object.
480
484
  */
481
- // TODO(wittjosiah): Narrow types.
482
- export const getSchema: (obj: unknown | undefined) => Type.AnyEntity | undefined = internal.getSchema as any;
485
+ export const getSchema: (obj: Unknown | Snapshot) => Type.AnyEntity | undefined = internal.getSchema as any;
483
486
 
484
487
  /**
485
488
  * @returns The typename of the object's type.
@@ -517,7 +520,7 @@ export const Meta = internal.MetaId;
517
520
  export type ReadonlyMeta = internal.ReadonlyMeta;
518
521
 
519
522
  /**
520
- * Mutable meta type returned by `Obj.getMeta` inside an `Obj.change` callback.
523
+ * Mutable meta type returned by `Obj.getMeta` inside an `Obj.update` callback.
521
524
  */
522
525
  export type Meta = internal.Meta;
523
526
 
@@ -525,7 +528,7 @@ export type Meta = internal.Meta;
525
528
  // TODO(dmaretskyi): Allow returning undefined.
526
529
  /**
527
530
  * Get the metadata for an object.
528
- * Returns mutable meta when passed a mutable object (inside `Obj.change` callback).
531
+ * Returns mutable meta when passed a mutable object (inside `Obj.update` callback).
529
532
  * Returns read-only meta when passed a regular object or snapshot.
530
533
  *
531
534
  * @example
@@ -534,7 +537,7 @@ export type Meta = internal.Meta;
534
537
  * const meta = Obj.getMeta(person); // ReadonlyMeta
535
538
  *
536
539
  * // Mutable access inside change callback
537
- * Obj.change(person, (obj) => {
540
+ * Obj.update(person, (obj) => {
538
541
  * const meta = Obj.getMeta(obj); // ObjectMeta (mutable)
539
542
  * meta.tags ??= [];
540
543
  * meta.tags.push('important');
@@ -559,7 +562,7 @@ export const getKeys: {
559
562
 
560
563
  /**
561
564
  * Delete all keys from the object for the specified source.
562
- * Must be called within an `Obj.change` callback.
565
+ * Must be called within an `Obj.update` callback.
563
566
  *
564
567
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
565
568
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -568,7 +571,7 @@ export const deleteKeys = (entity: Mutable<Unknown>, source: string): void => in
568
571
 
569
572
  /**
570
573
  * Add a tag to the object.
571
- * Must be called within an `Obj.change` callback.
574
+ * Must be called within an `Obj.update` callback.
572
575
  *
573
576
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
574
577
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -577,7 +580,7 @@ export const addTag = (entity: Mutable<Unknown>, tag: string): void => internal.
577
580
 
578
581
  /**
579
582
  * Remove a tag from the object.
580
- * Must be called within an `Obj.change` callback.
583
+ * Must be called within an `Obj.update` callback.
581
584
  *
582
585
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
583
586
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -603,7 +606,7 @@ export const getLabel = (entity: Unknown | Snapshot): string | undefined => inte
603
606
 
604
607
  /**
605
608
  * Set the label of the object.
606
- * Must be called within an `Obj.change` callback.
609
+ * Must be called within an `Obj.update` callback.
607
610
  *
608
611
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
609
612
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -618,7 +621,7 @@ export const getDescription = (entity: Unknown | Snapshot): string | undefined =
618
621
 
619
622
  /**
620
623
  * Set the description of the object.
621
- * Must be called within an `Obj.change` callback.
624
+ * Must be called within an `Obj.update` callback.
622
625
  *
623
626
  * NOTE: TypeScript's structural typing allows readonly objects to be passed to `Mutable<T>`
624
627
  * parameters, so there is no compile-time error. Enforcement is runtime-only.
@@ -741,7 +744,7 @@ const prepareAssignValue = (value: unknown): unknown =>
741
744
  * References are compared by target DXN; other values use Effect `Equal.equals` inside a structural region,
742
745
  * with recursive comparison for arrays and plain object-shaped property bags (excluding `id`).
743
746
  *
744
- * Must be called within an `Obj.change` callback.
747
+ * Must be called within an `Obj.update` callback.
745
748
  *
746
749
  * @returns Whether any property was updated.
747
750
  */
@@ -808,7 +811,7 @@ export const toJSON = (entity: Unknown | Snapshot): JSON => objInternal.objectTo
808
811
  */
809
812
  export const fromJSON: (
810
813
  json: unknown,
811
- options?: { refResolver?: Ref.Resolver; dxn?: DXN; database?: Database.Database },
814
+ options?: { refResolver?: Ref.Resolver; dxn?: DXN; database?: Database.Database; parent?: Unknown },
812
815
  ) => Promise<Unknown> = objInternal.objectFromJSON as any;
813
816
 
814
817
  /**