@dxos/echo 0.9.1-main.c7dcc2e112 → 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.
- package/dist/lib/neutral/Annotation.mjs +22 -8
- package/dist/lib/neutral/Database.mjs +3 -3
- package/dist/lib/neutral/Entity.mjs +11 -11
- package/dist/lib/neutral/Err.mjs +7 -3
- package/dist/lib/neutral/Feed.mjs +15 -19
- package/dist/lib/neutral/Filter.mjs +13 -11
- package/dist/lib/neutral/Format.mjs +3 -7
- package/dist/lib/neutral/JsonSchema.mjs +7 -7
- package/dist/lib/neutral/Migration.mjs +11 -11
- package/dist/lib/neutral/Obj.mjs +15 -13
- package/dist/lib/neutral/Order.mjs +1 -1
- package/dist/lib/neutral/Query.mjs +17 -17
- package/dist/lib/neutral/QueryResult.mjs +1 -1
- package/dist/lib/neutral/Ref.mjs +6 -6
- package/dist/lib/neutral/Registry.mjs +1 -1
- package/dist/lib/neutral/Relation.mjs +12 -12
- package/dist/lib/neutral/Tag.mjs +15 -15
- package/dist/lib/neutral/Text.mjs +25 -0
- package/dist/lib/neutral/Text.mjs.map +7 -0
- package/dist/lib/neutral/Type.mjs +10 -10
- package/dist/lib/neutral/chunk-2HKGAQM4.mjs +1394 -0
- package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-O6BH7EPN.mjs → chunk-2ZHECXUR.mjs} +5 -4
- package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-AFANXSGV.mjs → chunk-3KTE2Y7Q.mjs} +20 -20
- package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-WZ32BF53.mjs → chunk-3TN2VMI4.mjs} +49 -37
- package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-E7IOTOZD.mjs → chunk-42L4HJ3Z.mjs} +11 -15
- package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-K3S7D5PR.mjs → chunk-4A2NWKRJ.mjs} +5 -5
- package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +1531 -0
- package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-Z3WMRRAZ.mjs → chunk-BEFR7LEA.mjs} +11 -9
- package/dist/lib/neutral/{chunk-Z3WMRRAZ.mjs.map → chunk-BEFR7LEA.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs → chunk-BXIKXT7Q.mjs} +2 -6
- package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs.map → chunk-BXIKXT7Q.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-3JXQUMUI.mjs → chunk-C57XMMBY.mjs} +21 -20
- package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-JZ6LFQU5.mjs → chunk-C62JYFAD.mjs} +82 -8
- package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-TF5BSLF6.mjs → chunk-FSGX2R62.mjs} +3 -3
- package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-6CEISA3V.mjs → chunk-HBXT3GSB.mjs} +29 -15
- package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-WU3GIANS.mjs → chunk-IQR45HAD.mjs} +1 -1
- package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-TFEWTY5A.mjs → chunk-KCBW4BR6.mjs} +4 -8
- package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-4TPQZPLY.mjs → chunk-L3E5IXAE.mjs} +198 -30
- package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-EVK6XBXO.mjs → chunk-PRC4FEKJ.mjs} +2 -2
- package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NOL7O3BL.mjs → chunk-SCT6FFCR.mjs} +9 -4
- package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-BBFJWWAV.mjs → chunk-TOCVVFK2.mjs} +26 -2
- package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-A53NTBHE.mjs → chunk-U53LSQQA.mjs} +3 -3
- package/dist/lib/neutral/{chunk-A53NTBHE.mjs.map → chunk-U53LSQQA.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-PLDMJJJC.mjs → chunk-UGFFV2FR.mjs} +2 -2
- package/dist/lib/neutral/{chunk-35INCYOE.mjs → chunk-UWLP4RFM.mjs} +1 -1
- package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-SZ5WB6HF.mjs → chunk-UZHCQ6NI.mjs} +13 -11
- package/dist/lib/neutral/{chunk-SZ5WB6HF.mjs.map → chunk-UZHCQ6NI.mjs.map} +3 -3
- package/dist/lib/neutral/{chunk-L3OBWBV2.mjs → chunk-WKIT2YY5.mjs} +2 -2
- package/dist/lib/neutral/{chunk-HGWQR5KH.mjs → chunk-XB6ISRAO.mjs} +27 -15
- package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +7 -0
- package/dist/lib/neutral/chunk-XXL42WCB.mjs +84 -0
- package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-5GAIWRQB.mjs → chunk-XXWK4J53.mjs} +27 -42
- package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +30 -26
- package/dist/lib/neutral/internal/index.mjs +91 -69
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/testing/index.mjs +79 -60
- package/dist/lib/neutral/testing/index.mjs.map +3 -3
- package/dist/types/src/Annotation.d.ts +35 -1
- package/dist/types/src/Annotation.d.ts.map +1 -1
- package/dist/types/src/Collection.d.ts +9 -8
- package/dist/types/src/Collection.d.ts.map +1 -1
- package/dist/types/src/Database.d.ts +16 -0
- package/dist/types/src/Database.d.ts.map +1 -1
- package/dist/types/src/Dataset.d.ts +1 -16
- package/dist/types/src/Dataset.d.ts.map +1 -1
- package/dist/types/src/Err.d.ts +67 -0
- package/dist/types/src/Err.d.ts.map +1 -1
- package/dist/types/src/Feed.d.ts +56 -80
- package/dist/types/src/Feed.d.ts.map +1 -1
- package/dist/types/src/Filter.d.ts +9 -1
- package/dist/types/src/Filter.d.ts.map +1 -1
- package/dist/types/src/Hypergraph.d.ts +2 -9
- package/dist/types/src/Hypergraph.d.ts.map +1 -1
- package/dist/types/src/Migration.d.ts +1 -1
- package/dist/types/src/Migration.d.ts.map +1 -1
- package/dist/types/src/Obj.d.ts +31 -6
- package/dist/types/src/Obj.d.ts.map +1 -1
- package/dist/types/src/Order.d.ts +1 -1
- package/dist/types/src/Order.d.ts.map +1 -1
- package/dist/types/src/Query.d.ts +1 -1
- package/dist/types/src/Query.d.ts.map +1 -1
- package/dist/types/src/QueryResult.d.ts +2 -2
- package/dist/types/src/QueryResult.d.ts.map +1 -1
- package/dist/types/src/Ref.d.ts +8 -4
- package/dist/types/src/Ref.d.ts.map +1 -1
- package/dist/types/src/Registry.d.ts +1 -1
- package/dist/types/src/Relation.d.ts.map +1 -1
- package/dist/types/src/Tag.d.ts +11 -13
- package/dist/types/src/Tag.d.ts.map +1 -1
- package/dist/types/src/Text.d.ts +41 -0
- package/dist/types/src/Text.d.ts.map +1 -0
- package/dist/types/src/Text.test.d.ts +2 -0
- package/dist/types/src/Text.test.d.ts.map +1 -0
- package/dist/types/src/Type.d.ts +33 -20
- package/dist/types/src/Type.d.ts.map +1 -1
- package/dist/types/src/View.d.ts +8 -9
- package/dist/types/src/View.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/annotations.d.ts +9 -0
- package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/atoms.d.ts +14 -0
- package/dist/types/src/internal/Annotation/atoms.d.ts.map +1 -0
- package/dist/types/src/internal/Annotation/entity-dictionary.d.ts +13 -0
- package/dist/types/src/internal/Annotation/entity-dictionary.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/guard.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/model.d.ts +1 -1
- package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/object.d.ts +5 -1
- package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/relation.d.ts +7 -0
- package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/type-uri.d.ts.map +1 -1
- package/dist/types/src/internal/Filter/index.d.ts +2 -0
- package/dist/types/src/internal/Filter/index.d.ts.map +1 -0
- package/dist/types/src/internal/Filter/match.d.ts +22 -0
- package/dist/types/src/internal/Filter/match.d.ts.map +1 -0
- package/dist/types/src/internal/Format/format.d.ts +0 -2
- package/dist/types/src/internal/Format/format.d.ts.map +1 -1
- package/dist/types/src/internal/Format/index.d.ts +2 -2
- package/dist/types/src/internal/Format/index.d.ts.map +1 -1
- package/dist/types/src/internal/Format/string.d.ts +0 -8
- package/dist/types/src/internal/Format/string.d.ts.map +1 -1
- package/dist/types/src/internal/Format/types.d.ts +2 -1
- package/dist/types/src/internal/Format/types.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/atoms.d.ts +7 -3
- package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/clone.d.ts +2 -1
- package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/create-object.d.ts +3 -4
- package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/Query/index.d.ts +1 -0
- package/dist/types/src/internal/Query/index.d.ts.map +1 -1
- package/dist/types/src/internal/Query/query-result-effect.d.ts +9 -0
- package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -0
- package/dist/types/src/internal/Ref/atoms.d.ts +1 -2
- package/dist/types/src/internal/Ref/atoms.d.ts.map +1 -1
- package/dist/types/src/internal/Ref/ref.d.ts +13 -6
- package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
- package/dist/types/src/internal/common/atom-snapshot.d.ts +8 -0
- package/dist/types/src/internal/common/atom-snapshot.d.ts.map +1 -0
- package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/proxy-types.d.ts +12 -0
- package/dist/types/src/internal/common/proxy/proxy-types.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/proxy-utils.d.ts +25 -0
- package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/schema-validator.d.ts +5 -0
- package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/typed-handler.d.ts +17 -0
- package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/entity.d.ts +4 -4
- package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/meta.d.ts +2 -4
- package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
- package/dist/types/src/internal/index.d.ts +1 -0
- package/dist/types/src/internal/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-schema.d.ts +42 -171
- package/dist/types/src/testing/test-schema.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -13
- package/src/Annotation.test.ts +257 -10
- package/src/Annotation.ts +55 -6
- package/src/Collection.ts +10 -12
- package/src/Database.ts +22 -20
- package/src/Err.ts +22 -0
- package/src/Feed.ts +114 -141
- package/src/Filter.test.ts +38 -0
- package/src/Filter.ts +17 -3
- package/src/Hypergraph.ts +2 -11
- package/src/Migration.ts +1 -1
- package/src/Obj.test.ts +92 -12
- package/src/Obj.ts +36 -5
- package/src/Order.ts +3 -3
- package/src/Query.test.ts +2 -2
- package/src/Query.ts +16 -16
- package/src/QueryResult.ts +2 -2
- package/src/Ref.ts +12 -4
- package/src/Registry.ts +1 -1
- package/src/Relation.ts +1 -1
- package/src/Tag.ts +12 -16
- package/src/Text.test.ts +186 -0
- package/src/Text.ts +116 -0
- package/src/Type.test.ts +52 -24
- package/src/Type.ts +69 -31
- package/src/View.ts +22 -30
- package/src/index.ts +1 -1
- package/src/internal/Annotation/annotations.ts +15 -2
- package/src/internal/Annotation/atoms.ts +107 -0
- package/src/internal/Annotation/entity-dictionary.ts +40 -37
- package/src/internal/Entity/api.ts +1 -1
- package/src/internal/Entity/guard.ts +2 -1
- package/src/internal/Entity/model.ts +2 -2
- package/src/internal/Entity/object.ts +14 -1
- package/src/internal/Entity/relation.ts +19 -0
- package/src/internal/Entity/type-uri.ts +3 -1
- package/src/internal/Filter/index.ts +5 -0
- package/src/internal/Filter/match.ts +284 -0
- package/src/internal/Format/format.ts +0 -2
- package/src/internal/Format/index.ts +2 -2
- package/src/internal/Format/string.ts +0 -12
- package/src/internal/Format/types.ts +4 -2
- package/src/internal/JsonSchema/json-schema.test.ts +82 -54
- package/src/internal/Obj/atoms.ts +31 -18
- package/src/internal/Obj/clone.ts +61 -20
- package/src/internal/Obj/create-object.test.ts +3 -3
- package/src/internal/Obj/create-object.ts +3 -4
- package/src/internal/Obj/inspect.ts +1 -1
- package/src/internal/Obj/json-serializer.test.ts +7 -5
- package/src/internal/Obj/json-serializer.ts +1 -1
- package/src/internal/Obj/schema-validator.test.ts +53 -0
- package/src/internal/Obj/set-value.test.ts +29 -19
- package/src/internal/Query/index.ts +1 -0
- package/src/internal/Query/query-result-effect.ts +35 -0
- package/src/internal/Ref/atoms.ts +23 -3
- package/src/internal/Ref/ref.test.ts +8 -8
- package/src/internal/Ref/ref.ts +26 -20
- package/src/internal/common/atom-snapshot.ts +19 -0
- package/src/internal/common/proxy/make-object.ts +1 -1
- package/src/internal/common/proxy/proxy-types.ts +20 -0
- package/src/internal/common/proxy/proxy-utils.ts +37 -1
- package/src/internal/common/proxy/reactive-array.ts +3 -3
- package/src/internal/common/proxy/reactive.test.ts +1 -1
- package/src/internal/common/proxy/schema-validator.ts +181 -8
- package/src/internal/common/proxy/typed-handler.ts +254 -76
- package/src/internal/common/types/entity.ts +15 -7
- package/src/internal/index.ts +1 -0
- package/src/testing/test-schema.ts +75 -90
- package/dist/lib/neutral/chunk-35INCYOE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-3JXQUMUI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-4TPQZPLY.mjs.map +0 -7
- package/dist/lib/neutral/chunk-5GAIWRQB.mjs.map +0 -7
- package/dist/lib/neutral/chunk-6CEISA3V.mjs.map +0 -7
- package/dist/lib/neutral/chunk-AFANXSGV.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BBFJWWAV.mjs.map +0 -7
- package/dist/lib/neutral/chunk-E7IOTOZD.mjs.map +0 -7
- package/dist/lib/neutral/chunk-EVK6XBXO.mjs.map +0 -7
- package/dist/lib/neutral/chunk-HGWQR5KH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-JZ6LFQU5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NOL7O3BL.mjs.map +0 -7
- package/dist/lib/neutral/chunk-O6BH7EPN.mjs.map +0 -7
- package/dist/lib/neutral/chunk-TF5BSLF6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-TFEWTY5A.mjs.map +0 -7
- package/dist/lib/neutral/chunk-VQYL2OQF.mjs +0 -2353
- package/dist/lib/neutral/chunk-VQYL2OQF.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WU3GIANS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WZ32BF53.mjs.map +0 -7
- package/dist/lib/neutral/chunk-X3NUUON4.mjs +0 -184
- package/dist/lib/neutral/chunk-X3NUUON4.mjs.map +0 -7
- package/dist/types/src/exemplars.test.d.ts +0 -2
- package/dist/types/src/exemplars.test.d.ts.map +0 -1
- package/dist/types/src/hierarchy.test.d.ts +0 -2
- package/dist/types/src/hierarchy.test.d.ts.map +0 -1
- package/src/exemplars.test.ts +0 -21
- package/src/hierarchy.test.ts +0 -33
- /package/dist/lib/neutral/{chunk-K3S7D5PR.mjs.map → chunk-4A2NWKRJ.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-PLDMJJJC.mjs.map → chunk-UGFFV2FR.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-L3OBWBV2.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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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) =>
|
|
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 {
|
|
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 !==
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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 ===
|
|
349
|
-
return { echoRoot, preparedValue: value };
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
};
|