@dxos/echo 0.9.0 → 0.9.1-staging.ee54ba693a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/dist/lib/neutral/Annotation.mjs +30 -8
  2. package/dist/lib/neutral/Database.mjs +7 -3
  3. package/dist/lib/neutral/Entity.mjs +15 -11
  4. package/dist/lib/neutral/Err.mjs +7 -3
  5. package/dist/lib/neutral/Feed.mjs +15 -19
  6. package/dist/lib/neutral/Filter.mjs +13 -11
  7. package/dist/lib/neutral/Format.mjs +3 -7
  8. package/dist/lib/neutral/JsonSchema.mjs +7 -7
  9. package/dist/lib/neutral/Migration.mjs +11 -11
  10. package/dist/lib/neutral/Obj.mjs +19 -13
  11. package/dist/lib/neutral/Order.mjs +1 -1
  12. package/dist/lib/neutral/Query.mjs +17 -17
  13. package/dist/lib/neutral/QueryResult.mjs +1 -1
  14. package/dist/lib/neutral/Ref.mjs +6 -6
  15. package/dist/lib/neutral/Registry.mjs +1 -1
  16. package/dist/lib/neutral/Relation.mjs +12 -12
  17. package/dist/lib/neutral/Tag.mjs +15 -15
  18. package/dist/lib/neutral/Text.mjs +25 -0
  19. package/dist/lib/neutral/Text.mjs.map +7 -0
  20. package/dist/lib/neutral/Type.mjs +10 -10
  21. package/dist/lib/neutral/chunk-2HKGAQM4.mjs +1394 -0
  22. package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +7 -0
  23. package/dist/lib/neutral/{chunk-O6BH7EPN.mjs → chunk-2ZHECXUR.mjs} +5 -4
  24. package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +7 -0
  25. package/dist/lib/neutral/{chunk-QQIYS74I.mjs → chunk-3KTE2Y7Q.mjs} +20 -20
  26. package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-R5W6DXR4.mjs → chunk-3TN2VMI4.mjs} +72 -36
  28. package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +7 -0
  29. package/dist/lib/neutral/{chunk-5SMDBFVB.mjs → chunk-42L4HJ3Z.mjs} +11 -15
  30. package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +7 -0
  31. package/dist/lib/neutral/{chunk-7PI7C4EF.mjs → chunk-4A2NWKRJ.mjs} +5 -5
  32. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +1531 -0
  33. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +7 -0
  34. package/dist/lib/neutral/{chunk-SBVFRTST.mjs → chunk-BEFR7LEA.mjs} +20 -10
  35. package/dist/lib/neutral/chunk-BEFR7LEA.mjs.map +7 -0
  36. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs → chunk-BXIKXT7Q.mjs} +2 -6
  37. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs.map → chunk-BXIKXT7Q.mjs.map} +1 -1
  38. package/dist/lib/neutral/{chunk-UUP46KUQ.mjs → chunk-C57XMMBY.mjs} +21 -20
  39. package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +7 -0
  40. package/dist/lib/neutral/chunk-C62JYFAD.mjs +183 -0
  41. package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-MZ7K3MLL.mjs → chunk-FSGX2R62.mjs} +3 -3
  43. package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-TYGKCRMK.mjs → chunk-HBXT3GSB.mjs} +37 -15
  45. package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-WU3GIANS.mjs → chunk-IQR45HAD.mjs} +1 -1
  47. package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +7 -0
  48. package/dist/lib/neutral/{chunk-TFEWTY5A.mjs → chunk-KCBW4BR6.mjs} +4 -8
  49. package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +7 -0
  50. package/dist/lib/neutral/{chunk-WTQJHC75.mjs → chunk-L3E5IXAE.mjs} +225 -45
  51. package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +7 -0
  52. package/dist/lib/neutral/{chunk-EVK6XBXO.mjs → chunk-PRC4FEKJ.mjs} +2 -2
  53. package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +7 -0
  54. package/dist/lib/neutral/{chunk-6YDI3J37.mjs → chunk-SCT6FFCR.mjs} +9 -4
  55. package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +7 -0
  56. package/dist/lib/neutral/{chunk-BBFJWWAV.mjs → chunk-TOCVVFK2.mjs} +26 -2
  57. package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +7 -0
  58. package/dist/lib/neutral/{chunk-RIVWNMSF.mjs → chunk-U53LSQQA.mjs} +3 -3
  59. package/dist/lib/neutral/{chunk-RIVWNMSF.mjs.map → chunk-U53LSQQA.mjs.map} +1 -1
  60. package/dist/lib/neutral/{chunk-IGK6FN65.mjs → chunk-UGFFV2FR.mjs} +2 -2
  61. package/dist/lib/neutral/{chunk-35INCYOE.mjs → chunk-UWLP4RFM.mjs} +1 -1
  62. package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +7 -0
  63. package/dist/lib/neutral/{chunk-LWXVKPPW.mjs → chunk-UZHCQ6NI.mjs} +13 -11
  64. package/dist/lib/neutral/{chunk-LWXVKPPW.mjs.map → chunk-UZHCQ6NI.mjs.map} +3 -3
  65. package/dist/lib/neutral/{chunk-5SUJPHAE.mjs → chunk-WKIT2YY5.mjs} +2 -2
  66. package/dist/lib/neutral/{chunk-T6E37YIP.mjs → chunk-XB6ISRAO.mjs} +38 -18
  67. package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +7 -0
  68. package/dist/lib/neutral/chunk-XXL42WCB.mjs +84 -0
  69. package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +7 -0
  70. package/dist/lib/neutral/{chunk-ZGNNFYHS.mjs → chunk-XXWK4J53.mjs} +27 -42
  71. package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +7 -0
  72. package/dist/lib/neutral/index.mjs +30 -26
  73. package/dist/lib/neutral/internal/index.mjs +111 -69
  74. package/dist/lib/neutral/meta.json +1 -1
  75. package/dist/lib/neutral/testing/index.mjs +79 -60
  76. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  77. package/dist/types/src/Annotation.d.ts +35 -1
  78. package/dist/types/src/Annotation.d.ts.map +1 -1
  79. package/dist/types/src/Collection.d.ts +9 -8
  80. package/dist/types/src/Collection.d.ts.map +1 -1
  81. package/dist/types/src/Database.d.ts +38 -0
  82. package/dist/types/src/Database.d.ts.map +1 -1
  83. package/dist/types/src/Dataset.d.ts +1 -16
  84. package/dist/types/src/Dataset.d.ts.map +1 -1
  85. package/dist/types/src/Entity.d.ts +2 -0
  86. package/dist/types/src/Entity.d.ts.map +1 -1
  87. package/dist/types/src/Err.d.ts +67 -0
  88. package/dist/types/src/Err.d.ts.map +1 -1
  89. package/dist/types/src/Feed.d.ts +59 -80
  90. package/dist/types/src/Feed.d.ts.map +1 -1
  91. package/dist/types/src/Filter.d.ts +9 -1
  92. package/dist/types/src/Filter.d.ts.map +1 -1
  93. package/dist/types/src/Hypergraph.d.ts +2 -9
  94. package/dist/types/src/Hypergraph.d.ts.map +1 -1
  95. package/dist/types/src/Migration.d.ts +1 -1
  96. package/dist/types/src/Migration.d.ts.map +1 -1
  97. package/dist/types/src/Obj.d.ts +33 -6
  98. package/dist/types/src/Obj.d.ts.map +1 -1
  99. package/dist/types/src/Order.d.ts +1 -1
  100. package/dist/types/src/Order.d.ts.map +1 -1
  101. package/dist/types/src/Query.d.ts +1 -1
  102. package/dist/types/src/Query.d.ts.map +1 -1
  103. package/dist/types/src/QueryResult.d.ts +2 -2
  104. package/dist/types/src/QueryResult.d.ts.map +1 -1
  105. package/dist/types/src/Ref.d.ts +10 -6
  106. package/dist/types/src/Ref.d.ts.map +1 -1
  107. package/dist/types/src/Registry.d.ts +1 -1
  108. package/dist/types/src/Relation.d.ts.map +1 -1
  109. package/dist/types/src/Tag.d.ts +11 -13
  110. package/dist/types/src/Tag.d.ts.map +1 -1
  111. package/dist/types/src/Text.d.ts +41 -0
  112. package/dist/types/src/Text.d.ts.map +1 -0
  113. package/dist/types/src/Text.test.d.ts +2 -0
  114. package/dist/types/src/Text.test.d.ts.map +1 -0
  115. package/dist/types/src/Type.d.ts +38 -21
  116. package/dist/types/src/Type.d.ts.map +1 -1
  117. package/dist/types/src/View.d.ts +8 -9
  118. package/dist/types/src/View.d.ts.map +1 -1
  119. package/dist/types/src/index.d.ts +1 -0
  120. package/dist/types/src/index.d.ts.map +1 -1
  121. package/dist/types/src/internal/Annotation/annotations.d.ts +50 -0
  122. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  123. package/dist/types/src/internal/Annotation/atoms.d.ts +14 -0
  124. package/dist/types/src/internal/Annotation/atoms.d.ts.map +1 -0
  125. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts +13 -0
  126. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts.map +1 -1
  127. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  128. package/dist/types/src/internal/Entity/guard.d.ts.map +1 -1
  129. package/dist/types/src/internal/Entity/model.d.ts +1 -1
  130. package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
  131. package/dist/types/src/internal/Entity/object.d.ts +5 -1
  132. package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
  133. package/dist/types/src/internal/Entity/relation.d.ts +7 -0
  134. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  135. package/dist/types/src/internal/Entity/type-uri.d.ts.map +1 -1
  136. package/dist/types/src/internal/Filter/index.d.ts +2 -0
  137. package/dist/types/src/internal/Filter/index.d.ts.map +1 -0
  138. package/dist/types/src/internal/Filter/match.d.ts +22 -0
  139. package/dist/types/src/internal/Filter/match.d.ts.map +1 -0
  140. package/dist/types/src/internal/Format/format.d.ts +0 -2
  141. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  142. package/dist/types/src/internal/Format/index.d.ts +2 -2
  143. package/dist/types/src/internal/Format/index.d.ts.map +1 -1
  144. package/dist/types/src/internal/Format/string.d.ts +0 -8
  145. package/dist/types/src/internal/Format/string.d.ts.map +1 -1
  146. package/dist/types/src/internal/Format/types.d.ts +2 -1
  147. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  148. package/dist/types/src/internal/Obj/atoms.d.ts +12 -3
  149. package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
  150. package/dist/types/src/internal/Obj/clone.d.ts +2 -1
  151. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  152. package/dist/types/src/internal/Obj/create-object.d.ts +3 -4
  153. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  154. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  155. package/dist/types/src/internal/Query/index.d.ts +1 -0
  156. package/dist/types/src/internal/Query/index.d.ts.map +1 -1
  157. package/dist/types/src/internal/Query/query-result-effect.d.ts +9 -0
  158. package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -0
  159. package/dist/types/src/internal/Ref/atoms.d.ts +1 -2
  160. package/dist/types/src/internal/Ref/atoms.d.ts.map +1 -1
  161. package/dist/types/src/internal/Ref/index.d.ts +1 -0
  162. package/dist/types/src/internal/Ref/index.d.ts.map +1 -1
  163. package/dist/types/src/internal/Ref/ref.d.ts +79 -8
  164. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  165. package/dist/types/src/internal/Ref/strong-deps.d.ts +35 -0
  166. package/dist/types/src/internal/Ref/strong-deps.d.ts.map +1 -0
  167. package/dist/types/src/internal/common/atom-snapshot.d.ts +8 -0
  168. package/dist/types/src/internal/common/atom-snapshot.d.ts.map +1 -0
  169. package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
  170. package/dist/types/src/internal/common/proxy/proxy-types.d.ts +12 -0
  171. package/dist/types/src/internal/common/proxy/proxy-types.d.ts.map +1 -1
  172. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts +25 -0
  173. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  174. package/dist/types/src/internal/common/proxy/schema-validator.d.ts +5 -0
  175. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  176. package/dist/types/src/internal/common/proxy/typed-handler.d.ts +17 -0
  177. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  178. package/dist/types/src/internal/common/types/entity.d.ts +4 -4
  179. package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
  180. package/dist/types/src/internal/common/types/meta.d.ts +2 -4
  181. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  182. package/dist/types/src/internal/index.d.ts +1 -0
  183. package/dist/types/src/internal/index.d.ts.map +1 -1
  184. package/dist/types/src/testing/test-schema.d.ts +42 -171
  185. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  186. package/dist/types/tsconfig.tsbuildinfo +1 -1
  187. package/package.json +20 -13
  188. package/src/Annotation.test.ts +257 -10
  189. package/src/Annotation.ts +58 -4
  190. package/src/Collection.ts +10 -12
  191. package/src/Database.ts +55 -21
  192. package/src/Entity.ts +3 -1
  193. package/src/Err.ts +22 -0
  194. package/src/Feed.ts +117 -141
  195. package/src/Filter.test.ts +38 -0
  196. package/src/Filter.ts +17 -3
  197. package/src/Hypergraph.ts +2 -11
  198. package/src/Migration.ts +1 -1
  199. package/src/Obj.test.ts +92 -12
  200. package/src/Obj.ts +38 -5
  201. package/src/Order.ts +3 -3
  202. package/src/Query.test.ts +2 -2
  203. package/src/Query.ts +16 -16
  204. package/src/QueryResult.ts +2 -2
  205. package/src/Ref.ts +14 -6
  206. package/src/Registry.ts +1 -1
  207. package/src/Relation.ts +1 -1
  208. package/src/Tag.ts +13 -16
  209. package/src/Text.test.ts +186 -0
  210. package/src/Text.ts +116 -0
  211. package/src/Type.test.ts +52 -24
  212. package/src/Type.ts +85 -33
  213. package/src/View.ts +22 -30
  214. package/src/index.ts +1 -1
  215. package/src/internal/Annotation/annotations.ts +71 -2
  216. package/src/internal/Annotation/atoms.ts +107 -0
  217. package/src/internal/Annotation/entity-dictionary.ts +40 -37
  218. package/src/internal/Entity/api.ts +1 -1
  219. package/src/internal/Entity/guard.ts +2 -1
  220. package/src/internal/Entity/model.ts +2 -2
  221. package/src/internal/Entity/object.ts +14 -1
  222. package/src/internal/Entity/relation.ts +19 -0
  223. package/src/internal/Entity/type-uri.ts +3 -1
  224. package/src/internal/Filter/index.ts +5 -0
  225. package/src/internal/Filter/match.ts +284 -0
  226. package/src/internal/Format/format.ts +0 -2
  227. package/src/internal/Format/index.ts +2 -2
  228. package/src/internal/Format/string.ts +0 -12
  229. package/src/internal/Format/types.ts +4 -2
  230. package/src/internal/JsonSchema/json-schema.test.ts +82 -54
  231. package/src/internal/Obj/atoms.ts +62 -18
  232. package/src/internal/Obj/clone.ts +61 -20
  233. package/src/internal/Obj/create-object.test.ts +3 -3
  234. package/src/internal/Obj/create-object.ts +3 -4
  235. package/src/internal/Obj/inspect.ts +1 -1
  236. package/src/internal/Obj/json-serializer.test.ts +7 -5
  237. package/src/internal/Obj/json-serializer.ts +4 -4
  238. package/src/internal/Obj/schema-validator.test.ts +53 -0
  239. package/src/internal/Obj/set-value.test.ts +29 -19
  240. package/src/internal/Query/index.ts +1 -0
  241. package/src/internal/Query/query-result-effect.ts +35 -0
  242. package/src/internal/Ref/atoms.ts +23 -3
  243. package/src/internal/Ref/index.ts +1 -0
  244. package/src/internal/Ref/ref.test.ts +8 -8
  245. package/src/internal/Ref/ref.ts +122 -39
  246. package/src/internal/Ref/strong-deps.ts +107 -0
  247. package/src/internal/common/atom-snapshot.ts +19 -0
  248. package/src/internal/common/proxy/make-object.ts +1 -1
  249. package/src/internal/common/proxy/proxy-types.ts +20 -0
  250. package/src/internal/common/proxy/proxy-utils.ts +37 -1
  251. package/src/internal/common/proxy/reactive-array.ts +3 -3
  252. package/src/internal/common/proxy/reactive.test.ts +1 -1
  253. package/src/internal/common/proxy/schema-validator.ts +181 -8
  254. package/src/internal/common/proxy/typed-handler.ts +254 -76
  255. package/src/internal/common/types/entity.ts +15 -7
  256. package/src/internal/index.ts +1 -0
  257. package/src/testing/test-schema.ts +75 -90
  258. package/dist/lib/neutral/chunk-35INCYOE.mjs.map +0 -7
  259. package/dist/lib/neutral/chunk-3PBP4V4O.mjs +0 -101
  260. package/dist/lib/neutral/chunk-3PBP4V4O.mjs.map +0 -7
  261. package/dist/lib/neutral/chunk-4ZUHOTCG.mjs +0 -184
  262. package/dist/lib/neutral/chunk-4ZUHOTCG.mjs.map +0 -7
  263. package/dist/lib/neutral/chunk-5SMDBFVB.mjs.map +0 -7
  264. package/dist/lib/neutral/chunk-6YDI3J37.mjs.map +0 -7
  265. package/dist/lib/neutral/chunk-7FPIAJIV.mjs +0 -2279
  266. package/dist/lib/neutral/chunk-7FPIAJIV.mjs.map +0 -7
  267. package/dist/lib/neutral/chunk-BBFJWWAV.mjs.map +0 -7
  268. package/dist/lib/neutral/chunk-EVK6XBXO.mjs.map +0 -7
  269. package/dist/lib/neutral/chunk-MZ7K3MLL.mjs.map +0 -7
  270. package/dist/lib/neutral/chunk-O6BH7EPN.mjs.map +0 -7
  271. package/dist/lib/neutral/chunk-QQIYS74I.mjs.map +0 -7
  272. package/dist/lib/neutral/chunk-R5W6DXR4.mjs.map +0 -7
  273. package/dist/lib/neutral/chunk-SBVFRTST.mjs.map +0 -7
  274. package/dist/lib/neutral/chunk-T6E37YIP.mjs.map +0 -7
  275. package/dist/lib/neutral/chunk-TFEWTY5A.mjs.map +0 -7
  276. package/dist/lib/neutral/chunk-TYGKCRMK.mjs.map +0 -7
  277. package/dist/lib/neutral/chunk-UUP46KUQ.mjs.map +0 -7
  278. package/dist/lib/neutral/chunk-WTQJHC75.mjs.map +0 -7
  279. package/dist/lib/neutral/chunk-WU3GIANS.mjs.map +0 -7
  280. package/dist/lib/neutral/chunk-ZGNNFYHS.mjs.map +0 -7
  281. package/dist/types/src/exemplars.test.d.ts +0 -2
  282. package/dist/types/src/exemplars.test.d.ts.map +0 -1
  283. package/dist/types/src/hierarchy.test.d.ts +0 -2
  284. package/dist/types/src/hierarchy.test.d.ts.map +0 -1
  285. package/src/exemplars.test.ts +0 -21
  286. package/src/hierarchy.test.ts +0 -33
  287. /package/dist/lib/neutral/{chunk-7PI7C4EF.mjs.map → chunk-4A2NWKRJ.mjs.map} +0 -0
  288. /package/dist/lib/neutral/{chunk-IGK6FN65.mjs.map → chunk-UGFFV2FR.mjs.map} +0 -0
  289. /package/dist/lib/neutral/{chunk-5SUJPHAE.mjs.map → chunk-WKIT2YY5.mjs.map} +0 -0
@@ -6,7 +6,6 @@ import * as Schema from 'effect/Schema';
6
6
  import * as SchemaAST from 'effect/SchemaAST';
7
7
 
8
8
  import { invariant } from '@dxos/invariant';
9
- import { log } from '@dxos/log';
10
9
 
11
10
  import { SchemaId } from '../types';
12
11
 
@@ -68,8 +67,13 @@ export class SchemaValidator {
68
67
  getProperty([...propertyPath.slice(0, i), propertyName]),
69
68
  );
70
69
  if (propertyType == null) {
71
- log.warn('unknown property', { path: propertyPath, property: propertyName });
72
- continue;
70
+ const indexSignatureType = getIndexSignatureValueType(schema.ast);
71
+ if (indexSignatureType != null) {
72
+ schema = Schema.make(indexSignatureType).annotations(indexSignatureType.annotations);
73
+ continue;
74
+ }
75
+
76
+ throw new TypeError(`Unknown property: ${formatPropertyPath([...propertyPath.slice(0, i), propertyName])}`);
73
77
  }
74
78
 
75
79
  schema = Schema.make(propertyType).annotations(propertyType.annotations);
@@ -79,9 +83,101 @@ export class SchemaValidator {
79
83
  return schema;
80
84
  }
81
85
 
86
+ /**
87
+ * Rejects properties not declared on the schema. Types with index signatures allow extra keys.
88
+ */
89
+ public static assertExactProperties(
90
+ schema: Schema.Schema.AnyNoContext,
91
+ value: unknown,
92
+ getProperty: (path: KeyPath) => unknown = () => undefined,
93
+ path: KeyPath = [],
94
+ ): void {
95
+ if (!isPlainRecord(value)) {
96
+ return;
97
+ }
98
+
99
+ const typeLiteral = resolveTypeLiteral(schema.ast, (propertyName) => getProperty([...path, propertyName]));
100
+ if (typeLiteral == null) {
101
+ return;
102
+ }
103
+
104
+ const propertySignatures = SchemaAST.getPropertySignatures(typeLiteral);
105
+ const allowedKeys = new Set(propertySignatures.map((property) => String(property.name)));
106
+ const indexSignatureType = typeLiteral.indexSignatures[0]?.type;
107
+
108
+ for (const key of Object.keys(value)) {
109
+ const propertyPath = [...path, key];
110
+ if (!allowedKeys.has(key)) {
111
+ if (indexSignatureType == null) {
112
+ throw new TypeError(`Unknown property: ${formatPropertyPath(propertyPath)}`);
113
+ }
114
+
115
+ const indexSchema = Schema.make(indexSignatureType).annotations(indexSignatureType.annotations);
116
+ this.assertExactProperties(indexSchema, value[key], getProperty, propertyPath);
117
+ continue;
118
+ }
119
+
120
+ const propertySignature = propertySignatures.find((property) => String(property.name) === key);
121
+ invariant(propertySignature, 'Property signature must exist.');
122
+ const propertySchema = Schema.make(propertySignature.type).annotations(propertySignature.type.annotations);
123
+ this.assertExactProperties(propertySchema, value[key], getProperty, propertyPath);
124
+ }
125
+ }
126
+
127
+ public static getIndexedElementSchema(
128
+ schema: Schema.Schema.AnyNoContext,
129
+ index: number | string,
130
+ ): Schema.Schema.AnyNoContext | null {
131
+ const arrayAst = unwrapArray(schema.ast);
132
+ if (arrayAst != null) {
133
+ return getArrayElementSchema(arrayAst, index);
134
+ }
135
+
136
+ const unionAst = unwrapAst(
137
+ schema.ast,
138
+ (candidate) => SchemaAST.isUnion(candidate) && candidate.types.some((member) => unwrapArray(member) != null),
139
+ );
140
+ if (unionAst != null && SchemaAST.isUnion(unionAst)) {
141
+ for (const member of unionAst.types) {
142
+ const memberArrayAst = unwrapArray(member);
143
+ if (memberArrayAst != null) {
144
+ return getArrayElementSchema(memberArrayAst, index);
145
+ }
146
+ }
147
+ }
148
+
149
+ return null;
150
+ }
151
+
82
152
  public static getTargetPropertySchema(target: any, prop: string | symbol): Schema.Schema.AnyNoContext {
83
153
  const schema: Schema.Schema.AnyNoContext | undefined = (target as any)[SchemaId];
84
154
  invariant(schema, 'target has no schema');
155
+
156
+ if (Array.isArray(target)) {
157
+ if (prop === 'length') {
158
+ return Schema.Number;
159
+ }
160
+
161
+ if (typeof prop !== 'symbol') {
162
+ const indexedSchema = this.getIndexedElementSchema(schema, prop);
163
+ if (indexedSchema != null) {
164
+ return indexedSchema;
165
+ }
166
+ }
167
+
168
+ // Arrays sometimes carry the element struct as their stamped schema.
169
+ if (SchemaAST.isTypeLiteral(schema.ast)) {
170
+ return schema;
171
+ }
172
+ }
173
+
174
+ if (typeof prop === 'number' || (typeof prop === 'string' && /^\d+$/.test(prop))) {
175
+ const indexedSchema = this.getIndexedElementSchema(schema, prop);
176
+ if (indexedSchema != null) {
177
+ return indexedSchema;
178
+ }
179
+ }
180
+
85
181
  const arrayAst = unwrapArray(schema.ast);
86
182
  if (arrayAst != null) {
87
183
  return getArrayElementSchema(arrayAst, prop);
@@ -89,11 +185,15 @@ export class SchemaValidator {
89
185
 
90
186
  const propertyType = getPropertyType(schema.ast, prop.toString(), (prop) => target[prop]);
91
187
  if (propertyType == null) {
92
- return Schema.Any; // TODO(burdon): HACK.
188
+ const indexSignatureType = getIndexSignatureValueType(schema.ast);
189
+ if (indexSignatureType != null) {
190
+ return Schema.make(indexSignatureType).annotations(indexSignatureType.annotations);
191
+ }
192
+
193
+ throw new TypeError(`Unknown property: ${String(prop)}`);
93
194
  }
94
195
 
95
- invariant(propertyType, `invalid property: ${prop.toString()}`);
96
- return Schema.make(propertyType);
196
+ return Schema.make(propertyType).annotations(propertyType.annotations);
97
197
  }
98
198
  }
99
199
 
@@ -106,7 +206,8 @@ const getArrayElementSchema = (
106
206
  tupleAst: SchemaAST.TupleType,
107
207
  property: string | symbol | number,
108
208
  ): Schema.Schema.AnyNoContext => {
109
- const elementIndex = typeof property === 'string' ? parseInt(property, 10) : Number.NaN;
209
+ const elementIndex =
210
+ typeof property === 'number' ? property : typeof property === 'string' ? parseInt(property, 10) : Number.NaN;
110
211
  if (Number.isNaN(elementIndex)) {
111
212
  invariant(property === 'length', `invalid array property: ${String(property)}`);
112
213
  return Schema.Number;
@@ -150,7 +251,10 @@ const getPropertyType = (
150
251
  ): SchemaAST.AST | null => {
151
252
  const anyOrObject = unwrapAst(
152
253
  ast,
153
- (candidate) => SchemaAST.isAnyKeyword(candidate) || SchemaAST.isObjectKeyword(candidate),
254
+ (candidate) =>
255
+ SchemaAST.isAnyKeyword(candidate) ||
256
+ SchemaAST.isUnknownKeyword(candidate) ||
257
+ SchemaAST.isObjectKeyword(candidate),
154
258
  );
155
259
  if (anyOrObject != null) {
156
260
  return ast;
@@ -232,6 +336,75 @@ const unwrapAst = (rootAst: SchemaAST.AST, predicate?: (ast: SchemaAST.AST) => b
232
336
 
233
337
  const unwrapArray = (ast: SchemaAST.AST) => unwrapAst(ast, SchemaAST.isTupleType) as SchemaAST.TupleType | null;
234
338
 
339
+ const getIndexSignatureValueType = (ast: SchemaAST.AST): SchemaAST.AST | null => {
340
+ const typeLiteral = unwrapAst(ast, SchemaAST.isTypeLiteral);
341
+ if (typeLiteral == null || !SchemaAST.isTypeLiteral(typeLiteral) || typeLiteral.indexSignatures.length === 0) {
342
+ return null;
343
+ }
344
+
345
+ return unwrapAst(typeLiteral.indexSignatures[0].type);
346
+ };
347
+
348
+ const resolveTypeLiteral = (
349
+ ast: SchemaAST.AST,
350
+ getTargetPropertyFn: (propertyName: string) => unknown,
351
+ ): SchemaAST.TypeLiteral | null => {
352
+ const anyOrObject = unwrapAst(
353
+ ast,
354
+ (candidate) =>
355
+ SchemaAST.isAnyKeyword(candidate) ||
356
+ SchemaAST.isUnknownKeyword(candidate) ||
357
+ SchemaAST.isObjectKeyword(candidate),
358
+ );
359
+ if (anyOrObject != null) {
360
+ return null;
361
+ }
362
+
363
+ const typeOrDiscriminatedUnion = unwrapAst(ast, (type) => {
364
+ return (
365
+ SchemaAST.isTypeLiteral(type) ||
366
+ (SchemaAST.isUnion(type) && type.types.some((member) => SchemaAST.isTypeLiteral(member)))
367
+ );
368
+ });
369
+ if (typeOrDiscriminatedUnion == null) {
370
+ return null;
371
+ }
372
+
373
+ if (SchemaAST.isTypeLiteral(typeOrDiscriminatedUnion)) {
374
+ return typeOrDiscriminatedUnion;
375
+ }
376
+
377
+ if (!SchemaAST.isUnion(typeOrDiscriminatedUnion)) {
378
+ return null;
379
+ }
380
+
381
+ const typeAstList = typeOrDiscriminatedUnion.types.filter((type) =>
382
+ SchemaAST.isTypeLiteral(type),
383
+ ) as SchemaAST.TypeLiteral[];
384
+ if (typeAstList.length === 1) {
385
+ return typeAstList[0];
386
+ }
387
+
388
+ const typeDiscriminators = getTypeDiscriminators(typeAstList);
389
+ const targetPropertyValue = getTargetPropertyFn(String(typeDiscriminators[0].name));
390
+ const typeIndex = typeDiscriminators.findIndex(
391
+ (property) => targetPropertyValue === (property.type as SchemaAST.Literal).literal,
392
+ );
393
+ invariant(typeIndex !== -1, 'discriminator field not set on target');
394
+ return typeAstList[typeIndex];
395
+ };
396
+
397
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
398
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Uint8Array) {
399
+ return false;
400
+ }
401
+
402
+ const proto = Object.getPrototypeOf(value);
403
+ return proto === Object.prototype || proto === null;
404
+ };
405
+
406
+ const formatPropertyPath = (path: KeyPath): string => path.map(String).join('.');
407
+
235
408
  export const checkIdNotPresentOnSchema = (schema: Schema.Schema<any, any, any>) => {
236
409
  invariant(SchemaAST.isTypeLiteral(schema.ast));
237
410
  const idProperty = SchemaAST.getPropertySignatures(schema.ast).find((prop) => prop.name === 'id');
@@ -9,6 +9,7 @@ import { type InspectOptionsStylized } from 'node:util';
9
9
  import { Event } from '@dxos/async';
10
10
  import { inspectCustom } from '@dxos/debug';
11
11
  import { assertArgument, invariant } from '@dxos/invariant';
12
+ import { getDeep, setDeep } from '@dxos/util';
12
13
 
13
14
  import { getSchemaURI } from '../../Annotation/annotations';
14
15
  import { toEffectSchema } from '../../JsonSchema/json-schema';
@@ -27,7 +28,15 @@ import {
27
28
  wouldCreateCycle,
28
29
  } from './ownership';
29
30
  import { type ReactiveHandler, objectData } from './proxy-types';
30
- import { createProxy, isProxy, isValidProxyTarget, symbolIsProxy } from './proxy-utils';
31
+ import {
32
+ createProxy,
33
+ isProxy,
34
+ isReactiveRecord,
35
+ isValidProxyTarget,
36
+ normalizeSpliceRange,
37
+ symbolIsProxy,
38
+ symbolReactivePrototype,
39
+ } from './proxy-utils';
31
40
  import { ReactiveArray } from './reactive-array';
32
41
  import { SchemaValidator } from './schema-validator';
33
42
  import { ChangeId, EventId } from './symbols';
@@ -92,9 +101,10 @@ const deepCopy = <T>(value: T, visited = new Map<object, object>()): T => {
92
101
  return copy as T;
93
102
  }
94
103
 
95
- // Don't copy other class instances (objects with non-Object prototype).
104
+ // Don't copy other class instances (objects with non-Object prototype). A reactive record
105
+ // (its metadata moved onto a behaviour prototype) still counts as a plain data record here.
96
106
  const proto = Object.getPrototypeOf(actualValue);
97
- if (proto !== Object.prototype && proto !== Array.prototype && proto !== null) {
107
+ if (proto !== Array.prototype && proto !== null && !isReactiveRecord(actualValue)) {
98
108
  return value; // Return as-is, don't copy class instances.
99
109
  }
100
110
 
@@ -130,6 +140,138 @@ const copyHiddenProperties = (source: any, target: any): void => {
130
140
  }
131
141
  };
132
142
 
143
+ //
144
+ // Object layering for an in-memory typed reactive object (`TypedReactiveHandler`).
145
+ //
146
+ // Unlike the database-backed handler (echo-client/echo-prototypes.ts), here USER DATA lives as
147
+ // real OWN properties on the target — there is no document behind it. Only the per-object metadata
148
+ // is relocated onto a prototype, so swapping handlers doesn't disturb the data shape:
149
+ //
150
+ // proxy ──Proxy(target, ProxyHandlerSlot → TypedReactiveHandler)
151
+ // │ get/set/has/... traps run here
152
+ // ▼
153
+ // target the user's object. OWN enumerable props = user data ({ name, age, ... });
154
+ // │ [[Prototype]] OWN symbol props are removed by `compactMetadataToInstanceState` after init.
155
+ // ▼
156
+ // instanceState `Object.create(TypedObjectPrototype)`. Holds the relocated symbol-keyed
157
+ // │ [[Prototype]] metadata as hidden props: [EventId] (root only), [ObjectDeletedId], [SchemaId],
158
+ // │ [TypeId], [TypeEntityId], cached [StaticTypeSchemaSlot], ... (Arrays are
159
+ // │ exempt — they keep their ReactiveArray chain and are never compacted.)
160
+ // ▼
161
+ // TypedObjectPrototype shared behaviour. Carries [symbolReactivePrototype]=true (so the
162
+ // │ [[Prototype]] "plain object" gates still treat the record as data) plus the system
163
+ // │ accessors [objectData], [ChangeId], [StaticTypeSchemaSlot]; the get trap
164
+ // │ delegates to these via `isBehaviourAccessor` instead of switching.
165
+ // ▼
166
+ // Object.prototype ──▶ null a `getPrototypeOf` trap reports `Object.prototype` so consumers
167
+ // see a plain object; the real instanceState prototype stays hidden.
168
+ //
169
+ // Root vs nested: a root object owns an `[EventId]`; nested records share their root's reactivity
170
+ // and have none. That presence is the "is this an initialized root" signal (it replaced an earlier
171
+ // `[ChangeId] === true` marker).
172
+ //
173
+ // `this` in the accessors below: the get trap calls `Reflect.get(target, prop, receiver)`, so
174
+ // `this` is the PROXY (receiver) on the common path, or the raw target when read directly.
175
+ // `getRawTarget(this)` normalizes to the underlying target — needed here because user data and the
176
+ // metadata chain both hang off the raw target, and `executeChange` keys the change context by it.
177
+ //
178
+ /**
179
+ * Behaviour prototype for typed reactive records. A record's per-object metadata is moved onto an
180
+ * intermediate instance-state object whose prototype is this, so that swapping the handler that
181
+ * backs an object (e.g. an in-memory object becoming database-backed) is a prototype re-point that
182
+ * doesn't perturb the target's own-property shape. Marked as a reactive prototype so the "plain
183
+ * object" gates (`isValidProxyTarget` / `deepCopy`) still treat such targets as data records.
184
+ */
185
+ const TypedObjectPrototype: object = Object.create(Object.prototype);
186
+ defineHiddenProperty(TypedObjectPrototype, symbolReactivePrototype, true);
187
+
188
+ // The ECHO system surface is exposed as accessors on the behaviour prototype rather than as
189
+ // branches in the `get` trap. `this` is the proxy receiver (or the raw target when read directly);
190
+ // `getRawTarget` resolves either to the underlying target.
191
+ Object.defineProperties(TypedObjectPrototype, {
192
+ // TODO(burdon): Remove?
193
+ [objectData]: {
194
+ get(this: ProxyTarget) {
195
+ return toJSON(getRawTarget(this));
196
+ },
197
+ },
198
+ [ChangeId]: {
199
+ // A function that runs a mutation inside a controlled change context. Only root objects (which
200
+ // own an `EventId`) expose one; nested records share their root's reactivity and return undefined.
201
+ get(this: ProxyTarget) {
202
+ const target = getRawTarget(this);
203
+ if (!(EventId in target)) {
204
+ return undefined;
205
+ }
206
+ return (callback: (obj: any) => void) => executeChange(target, target, this, callback);
207
+ },
208
+ },
209
+ [StaticTypeSchemaSlot]: {
210
+ // Lazily rebuild the source Effect Schema from `jsonSchema` and cache it as an own (hidden)
211
+ // property so subsequent reads short-circuit this accessor; the set-trap deletes the cache when
212
+ // `jsonSchema` is mutated.
213
+ get(this: ProxyTarget) {
214
+ const target = getRawTarget(this);
215
+ const jsonSchema = (target as any).jsonSchema;
216
+ if (jsonSchema == null) {
217
+ return undefined;
218
+ }
219
+ const rebuilt = toEffectSchema(jsonSchema);
220
+ defineHiddenProperty(target, StaticTypeSchemaSlot, rebuilt);
221
+ return rebuilt;
222
+ },
223
+ },
224
+ });
225
+
226
+ /** True if `prop` resolves to an accessor on the behaviour-prototype chain (not an own property). */
227
+ const isBehaviourAccessor = (target: object, prop: symbol): boolean => {
228
+ let proto = Object.getPrototypeOf(target);
229
+ while (proto != null && proto !== Object.prototype) {
230
+ const descriptor = Object.getOwnPropertyDescriptor(proto, prop);
231
+ if (descriptor != null) {
232
+ return descriptor.get != null;
233
+ }
234
+ proto = Object.getPrototypeOf(proto);
235
+ }
236
+ return false;
237
+ };
238
+
239
+ /**
240
+ * Enforce that user-data mutations only happen inside `Obj.update()`. A root object owns an
241
+ * `EventId` once initialized; before that (and for non-root records or symbol-keyed system
242
+ * properties) mutations pass through. Returns whether the root is initialized so callers can gate
243
+ * change notifications.
244
+ */
245
+ const assertMutableWithinChange = (echoRoot: object, prop: string | symbol): boolean => {
246
+ const isInitialized = EventId in echoRoot;
247
+ if (isInitialized && typeof prop !== 'symbol' && !isInChangeContext(echoRoot)) {
248
+ throw new Error(
249
+ `Cannot modify object property "${String(prop)}" outside of Obj.update(). ` +
250
+ 'Use Obj.update(obj, (mutableObj) => { mutableObj.property = value; }) instead.',
251
+ );
252
+ }
253
+ return isInitialized;
254
+ };
255
+
256
+ /**
257
+ * Move a record's per-object metadata (symbol-keyed hidden properties) onto a fresh instance-state
258
+ * object inserted between the target and {@link TypedObjectPrototype}. The target keeps only its
259
+ * user data as own properties. Arrays are exempt — they keep their own (ReactiveArray) prototype chain.
260
+ */
261
+ const compactMetadataToInstanceState = (target: ProxyTarget): void => {
262
+ if (Array.isArray(target) || Object.getPrototypeOf(target) !== Object.prototype) {
263
+ return;
264
+ }
265
+ const state = Object.create(TypedObjectPrototype);
266
+ for (const symbol of Object.getOwnPropertySymbols(target)) {
267
+ Object.defineProperty(state, symbol, Object.getOwnPropertyDescriptor(target, symbol)!);
268
+ // Dynamic delete over arbitrary symbol keys: `ProxyTarget` declares these as required, so the
269
+ // statically-typed view cannot express deleting them; the symbol set is only known at runtime.
270
+ delete (target as any)[symbol];
271
+ }
272
+ Object.setPrototypeOf(target, state);
273
+ };
274
+
133
275
  /**
134
276
  * Typed in-memory reactive store (with Schema).
135
277
  * Reactivity is based on Event subscriptions, not signals.
@@ -155,12 +297,6 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
155
297
 
156
298
  defineHiddenProperty(target, ObjectDeletedId, false);
157
299
 
158
- // Mark root objects as having a change handler.
159
- // The actual handler is returned dynamically in get() to have access to the proxy.
160
- if (!hasOwner && !(ChangeId in target)) {
161
- defineHiddenProperty(target, ChangeId, true);
162
- }
163
-
164
300
  // Only set owners if this is a root object (no existing owner).
165
301
  // Nested objects already have owners set by their root's initialization.
166
302
  // If we re-set owners here for nested objects, we'd incorrectly point
@@ -192,47 +328,36 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
192
328
  configurable: true,
193
329
  value: this._inspect.bind(target),
194
330
  });
331
+
332
+ // Relocate per-object metadata onto an instance-state prototype, leaving only user data as own
333
+ // properties on the target. Done last so all metadata set above (and by `prepareTypedTarget`) moves.
334
+ compactMetadataToInstanceState(target);
335
+ }
336
+
337
+ /**
338
+ * Hide the internal instance-state prototype so consumers see a plain object; arrays keep their
339
+ * real (ReactiveArray) prototype.
340
+ */
341
+ getPrototypeOf(target: ProxyTarget): object | null {
342
+ return Array.isArray(target) ? Reflect.getPrototypeOf(target) : Object.prototype;
195
343
  }
196
344
 
197
345
  get(target: ProxyTarget, prop: string | symbol, receiver: any): any {
198
- switch (prop) {
199
- // TODO(burdon): Remove?
200
- case objectData: {
201
- return toJSON(target);
202
- }
203
- case ChangeId: {
204
- // Return change handler only for root objects that have been marked with ChangeId.
205
- if ((target as any)[ChangeId] !== true) {
206
- return undefined;
207
- }
208
- // Return a function that allows mutations within a controlled context.
209
- // Uses target as both the context key and event target for non-database objects.
210
- return (callback: (obj: any) => void) => executeChange(target, target, receiver, callback);
211
- }
212
- case TypeEntityId: {
213
- // The back-reference to the type entity is metadata — return the raw
214
- // value so we don't re-wrap an already-reactive `Type.Type` entity in
215
- // another proxy (which would fail the SchemaId-in-target invariant).
216
- return Reflect.get(target, prop, receiver);
217
- }
218
- case StaticTypeSchemaSlot: {
219
- // Lazily rebuild the source Effect Schema from `jsonSchema` and cache it on
220
- // the slot; the set-trap invalidates the cache when `jsonSchema` is mutated.
221
- const existing = Reflect.get(target, prop, receiver);
222
- if (existing !== undefined) {
223
- return existing;
224
- }
225
- const jsonSchema = (target as any).jsonSchema;
226
- if (jsonSchema == null) {
227
- return undefined;
228
- }
229
- const rebuilt = toEffectSchema(jsonSchema);
230
- defineHiddenProperty(target, StaticTypeSchemaSlot, rebuilt);
231
- return rebuilt;
232
- }
346
+ // The ECHO system surface (objectData, ChangeId, StaticTypeSchemaSlot, ...) lives as accessors
347
+ // on the behaviour prototype; resolve them via Reflect.get and return as-is — these are not user
348
+ // data and must never be proxy-wrapped.
349
+ if (typeof prop === 'symbol' && isBehaviourAccessor(target, prop)) {
350
+ return Reflect.get(target, prop, receiver);
351
+ }
352
+
353
+ // The back-reference to the type entity is own metadata — return the raw value so we don't
354
+ // re-wrap an already-reactive `Type.Type` entity in another proxy (which would fail the
355
+ // SchemaId-in-target invariant).
356
+ if (prop === TypeEntityId) {
357
+ return Reflect.get(target, prop, receiver);
233
358
  }
234
359
 
235
- // Handle getter properties.
360
+ // Own getter properties (e.g. the `SchemaId` slot installed by `setSchemaProperties`).
236
361
  if (Object.getOwnPropertyDescriptor(target, prop)?.get) {
237
362
  return Reflect.get(target, prop, receiver);
238
363
  }
@@ -247,19 +372,7 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
247
372
 
248
373
  set(target: ProxyTarget, prop: string | symbol, value: any, receiver: any): boolean {
249
374
  const echoRoot = getEchoRoot(target);
250
-
251
- // Check readonly enforcement - mutations only allowed within Obj.update().
252
- // Skip check if the object is still being initialized (no ChangeId handler yet).
253
- // Also skip for non-initialized root objects (those without EventId).
254
- // Skip for symbol properties (internal infrastructure, not user data).
255
- const isInitialized = ChangeId in echoRoot || EventId in echoRoot;
256
- const isSymbolProp = typeof prop === 'symbol';
257
- if (isInitialized && !isSymbolProp && !isInChangeContext(echoRoot)) {
258
- throw new Error(
259
- `Cannot modify object property "${String(prop)}" outside of Obj.update(). ` +
260
- 'Use Obj.update(obj, (mutableObj) => { mutableObj.property = value; }) instead.',
261
- );
262
- }
375
+ const isInitialized = assertMutableWithinChange(echoRoot, prop);
263
376
 
264
377
  let result: boolean = false;
265
378
  this._inSet = true;
@@ -294,7 +407,7 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
294
407
 
295
408
  // Check readonly enforcement - mutations only allowed within Obj.update().
296
409
  // Skip for symbol properties (internal infrastructure, not user data).
297
- const isInitialized = (echoRoot as any)[ChangeId] === true || EventId in echoRoot;
410
+ const isInitialized = EventId in echoRoot;
298
411
  const isSymbolProp = typeof property === 'symbol';
299
412
  if (isInitialized && !isSymbolProp && !isInChangeContext(echoRoot)) {
300
413
  throw createPropertyDeleteError(property);
@@ -309,18 +422,7 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
309
422
 
310
423
  defineProperty(target: ProxyTarget, property: string | symbol, attributes: PropertyDescriptor): boolean {
311
424
  const echoRoot = getEchoRoot(target);
312
-
313
- // Check readonly enforcement - mutations only allowed within Obj.update().
314
- // Skip check if the object is still being initialized (no ChangeId handler yet).
315
- // Skip for symbol properties (internal infrastructure, not user data).
316
- const isInitialized = ChangeId in echoRoot || EventId in echoRoot;
317
- const isSymbolProp = typeof property === 'symbol';
318
- if (isInitialized && !isSymbolProp && !isInChangeContext(echoRoot)) {
319
- throw new Error(
320
- `Cannot modify object property "${String(property)}" outside of Obj.update(). ` +
321
- 'Use Obj.update(obj, (mutableObj) => { mutableObj.property = value; }) instead.',
322
- );
323
- }
425
+ const isInitialized = assertMutableWithinChange(echoRoot, property);
324
426
 
325
427
  const { echoRoot: _, preparedValue } = this._prepareValueForAssignment(target, property, attributes.value);
326
428
  const result = Reflect.defineProperty(target, property, {
@@ -334,6 +436,65 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
334
436
  return result;
335
437
  }
336
438
 
439
+ textUpdate(target: ProxyTarget, path: readonly (string | number)[], newText: string): void {
440
+ this._applyTextMutation(target, path, () => newText);
441
+ }
442
+
443
+ textSplice(
444
+ target: ProxyTarget,
445
+ path: readonly (string | number)[],
446
+ start: number,
447
+ deleteCount: number,
448
+ insert: string,
449
+ ): string {
450
+ let removed = '';
451
+ this._applyTextMutation(target, path, (current) => {
452
+ const range = normalizeSpliceRange(current.length, start, deleteCount);
453
+ removed = current.slice(range.start, range.start + range.deleteCount);
454
+ return current.slice(0, range.start) + insert + current.slice(range.start + range.deleteCount);
455
+ });
456
+ return removed;
457
+ }
458
+
459
+ /**
460
+ * Shared read-modify-write for the in-memory string CRDT path. Mirrors the `set` trap: enforce the
461
+ * change context, then mutate and notify through the same batched notification path so reactivity fires.
462
+ *
463
+ * Writes via `setDeep` on the raw target, intentionally bypassing `_prepareValueForAssignment` /
464
+ * `_validateValue`: a string CRDT delta produces a string, and per-delta schema checks (pattern,
465
+ * maxLength) would reject valid intermediate states during incremental edits. Such constraints are
466
+ * enforced at the initial assignment or as application-level invariants, mirroring the Automerge path.
467
+ */
468
+ private _applyTextMutation(
469
+ target: ProxyTarget,
470
+ path: readonly (string | number)[],
471
+ compute: (current: string) => string,
472
+ ): void {
473
+ const echoRoot = getEchoRoot(target);
474
+ if (!isInChangeContext(echoRoot)) {
475
+ throw new Error(
476
+ `Cannot modify text at "${path.join('.')}" outside of Obj.update(). ` +
477
+ 'Use Obj.update(obj, () => { Text.splice(obj, path, ...); }) instead.',
478
+ );
479
+ }
480
+
481
+ const keyPath = [...path];
482
+ invariant(keyPath.length > 0, 'Text path must be non-empty');
483
+ const current = getDeep(target, keyPath);
484
+ if (typeof current !== 'string') {
485
+ throw new TypeError(`Text mutation target at "${keyPath.join('.')}" is not a string.`);
486
+ }
487
+
488
+ const next = compute(current);
489
+ batchEvents(() => {
490
+ // Write directly on the raw target (not the proxy) and notify through the same batched path
491
+ // the `set` trap uses, so subscribers fire once per `Obj.update`.
492
+ setDeep(target, keyPath, next);
493
+ queueNotification(echoRoot);
494
+ notifyOwnerChain(target);
495
+ });
496
+ }
497
+
337
498
  /**
338
499
  * Prepare a value for assignment to a typed object property.
339
500
  * Handles cycle detection, copy-on-assign, array conversion, validation, and ownership.
@@ -345,8 +506,8 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
345
506
  ): { echoRoot: object; preparedValue: any } {
346
507
  const echoRoot = getEchoRoot(target);
347
508
 
348
- if (prop === ParentId) {
349
- return { echoRoot, preparedValue: value }; // Short-circuit for parent assignment.
509
+ if (typeof prop === 'symbol') {
510
+ return { echoRoot, preparedValue: value };
350
511
  }
351
512
 
352
513
  // Check for cycles before assignment.
@@ -397,6 +558,7 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
397
558
  }
398
559
  const schema = SchemaValidator.getTargetPropertySchema(target, prop);
399
560
  const _ = Schema.asserts(schema)(value);
561
+ SchemaValidator.assertExactProperties(schema, value, (path) => getDeep(value, path));
400
562
  if (isValidProxyTarget(value)) {
401
563
  setSchemaProperties(value, schema);
402
564
  }
@@ -470,12 +632,27 @@ const setSchemaProperties = (obj: any, schema: Schema.Schema.AnyNoContext, typeS
470
632
  } else {
471
633
  defineHiddenProperty(obj, SchemaId, schema);
472
634
  }
635
+
636
+ if (Array.isArray(obj)) {
637
+ for (let index = 0; index < obj.length; index++) {
638
+ if (isValidProxyTarget(obj[index])) {
639
+ const elementSchema = SchemaValidator.getIndexedElementSchema(schema, index) ?? Schema.Any;
640
+ setSchemaProperties(obj[index], elementSchema);
641
+ }
642
+ }
643
+ return;
644
+ }
645
+
473
646
  for (const key in obj) {
474
647
  if (isValidProxyTarget(obj[key])) {
475
- const elementSchema = SchemaValidator.getTargetPropertySchema(obj, key);
476
- if (elementSchema != null) {
477
- setSchemaProperties(obj[key], elementSchema);
648
+ let elementSchema: Schema.Schema<any>;
649
+ try {
650
+ elementSchema = SchemaValidator.getTargetPropertySchema(obj, key);
651
+ } catch {
652
+ // Property not in schema — treat as untyped so the proxy can still wrap it.
653
+ elementSchema = Schema.Any;
478
654
  }
655
+ setSchemaProperties(obj[key], elementSchema);
479
656
  }
480
657
  }
481
658
  };
@@ -490,6 +667,7 @@ export const prepareTypedTarget = <T>(target: T, schema: Schema.Schema<T, any>,
490
667
 
491
668
  SchemaValidator.validateSchema(schema);
492
669
  const _ = Schema.asserts(schema)(target);
670
+ SchemaValidator.assertExactProperties(schema, target, (path) => getDeep(target, path));
493
671
  makeArraysReactive(target);
494
672
  setSchemaProperties(target, schema, typeSource);
495
673
  };