@dxos/echo 0.10.0 → 0.11.0
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/Annotation.mjs +3 -0
- package/dist/lib/Blob.mjs +160 -0
- package/dist/lib/Blob.mjs.map +1 -0
- package/dist/lib/Database.mjs +2 -0
- package/dist/lib/Entity.mjs +231 -0
- package/dist/lib/Entity.mjs.map +1 -0
- package/dist/lib/Err.mjs +105 -0
- package/dist/lib/Err.mjs.map +1 -0
- package/dist/lib/Feed.mjs +338 -0
- package/dist/lib/Feed.mjs.map +1 -0
- package/dist/lib/Filter.mjs +380 -0
- package/dist/lib/Filter.mjs.map +1 -0
- package/dist/lib/Format.mjs +47 -0
- package/dist/lib/Format.mjs.map +1 -0
- package/dist/lib/JsonSchema.mjs +21 -0
- package/dist/lib/JsonSchema.mjs.map +1 -0
- package/dist/lib/Key.mjs +15 -0
- package/dist/lib/Key.mjs.map +1 -0
- package/dist/lib/Migration.mjs +38 -0
- package/dist/lib/Migration.mjs.map +1 -0
- package/dist/lib/Obj.mjs +660 -0
- package/dist/lib/Obj.mjs.map +1 -0
- package/dist/lib/Order.mjs +65 -0
- package/dist/lib/Order.mjs.map +1 -0
- package/dist/lib/Query.mjs +336 -0
- package/dist/lib/Query.mjs.map +1 -0
- package/dist/lib/QueryResult.mjs +7 -0
- package/dist/lib/QueryResult.mjs.map +1 -0
- package/dist/lib/Ref.mjs +44 -0
- package/dist/lib/Ref.mjs.map +1 -0
- package/dist/lib/Registry.mjs +35 -0
- package/dist/lib/Registry.mjs.map +1 -0
- package/dist/lib/Relation.mjs +397 -0
- package/dist/lib/Relation.mjs.map +1 -0
- package/dist/lib/Scope.mjs +51 -0
- package/dist/lib/Scope.mjs.map +1 -0
- package/dist/lib/Tag.mjs +70 -0
- package/dist/lib/Tag.mjs.map +1 -0
- package/dist/lib/Text.mjs +82 -0
- package/dist/lib/Text.mjs.map +1 -0
- package/dist/lib/Type.mjs +371 -0
- package/dist/lib/Type.mjs.map +1 -0
- package/dist/lib/chunk-Annotation.mjs +221 -0
- package/dist/lib/chunk-Annotation.mjs.map +1 -0
- package/dist/lib/chunk-Database.mjs +918 -0
- package/dist/lib/chunk-Database.mjs.map +1 -0
- package/dist/lib/chunk-atom-snapshot.mjs +249 -0
- package/dist/lib/chunk-atom-snapshot.mjs.map +1 -0
- package/dist/lib/chunk-internal.mjs +2775 -0
- package/dist/lib/chunk-internal.mjs.map +1 -0
- package/dist/lib/chunk-json-schema.mjs +660 -0
- package/dist/lib/chunk-json-schema.mjs.map +1 -0
- package/dist/lib/chunk-number.mjs +164 -0
- package/dist/lib/chunk-number.mjs.map +1 -0
- package/dist/lib/chunk-ref-array.mjs +31 -0
- package/dist/lib/chunk-ref-array.mjs.map +1 -0
- package/dist/lib/chunk-ref.mjs +590 -0
- package/dist/lib/chunk-ref.mjs.map +1 -0
- package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
- package/dist/lib/chunk-select.mjs +186 -0
- package/dist/lib/chunk-select.mjs.map +1 -0
- package/dist/lib/chunk-src.mjs +234 -0
- package/dist/lib/chunk-src.mjs.map +1 -0
- package/dist/lib/chunk-util.mjs +140 -0
- package/dist/lib/chunk-util.mjs.map +1 -0
- package/dist/lib/index.mjs +24 -0
- package/dist/lib/internal.mjs +10 -0
- package/dist/lib/testing.mjs +346 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/Aggregate.d.ts +86 -0
- package/dist/types/src/Aggregate.d.ts.map +1 -0
- package/dist/types/src/Blob.d.ts +153 -0
- package/dist/types/src/Blob.d.ts.map +1 -0
- package/dist/types/src/Blob.test.d.ts +2 -0
- package/dist/types/src/Blob.test.d.ts.map +1 -0
- package/dist/types/src/Database.d.ts +153 -4
- package/dist/types/src/Database.d.ts.map +1 -1
- package/dist/types/src/Err.d.ts +116 -0
- package/dist/types/src/Err.d.ts.map +1 -1
- package/dist/types/src/Feed.d.ts +116 -28
- package/dist/types/src/Feed.d.ts.map +1 -1
- package/dist/types/src/Feed.test.d.ts +2 -0
- package/dist/types/src/Feed.test.d.ts.map +1 -0
- package/dist/types/src/Filter.d.ts +14 -6
- package/dist/types/src/Filter.d.ts.map +1 -1
- package/dist/types/src/Hypergraph.d.ts +18 -0
- package/dist/types/src/Hypergraph.d.ts.map +1 -1
- package/dist/types/src/Json.d.ts.map +1 -1
- package/dist/types/src/Obj.d.ts +14 -1
- package/dist/types/src/Obj.d.ts.map +1 -1
- package/dist/types/src/Order.d.ts +9 -3
- package/dist/types/src/Order.d.ts.map +1 -1
- package/dist/types/src/Query.d.ts +107 -26
- package/dist/types/src/Query.d.ts.map +1 -1
- package/dist/types/src/Relation.d.ts +6 -0
- package/dist/types/src/Relation.d.ts.map +1 -1
- package/dist/types/src/Type.d.ts.map +1 -1
- package/dist/types/src/View.d.ts +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/api.d.ts +6 -0
- package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/model.d.ts +3 -2
- package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
- package/dist/types/src/internal/Filter/match.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +34 -8
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/Query/pretty.d.ts.map +1 -1
- package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/make-object.d.ts +13 -0
- package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/symbols.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/typed-handler.d.ts +12 -0
- package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/index.d.ts +1 -0
- package/dist/types/src/internal/common/types/index.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/model-symbols.d.ts +5 -0
- package/dist/types/src/internal/common/types/model-symbols.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/projection.d.ts +32 -0
- package/dist/types/src/internal/common/types/projection.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +40 -35
- package/src/Aggregate.ts +105 -0
- package/src/Blob.test.ts +45 -0
- package/src/Blob.ts +205 -0
- package/src/Database.ts +207 -17
- package/src/Err.ts +36 -0
- package/src/Feed.test.ts +188 -0
- package/src/Feed.ts +238 -62
- package/src/Filter.ts +28 -10
- package/src/Hypergraph.ts +18 -0
- package/src/Json.ts +1 -0
- package/src/Obj.test.ts +2 -2
- package/src/Obj.ts +21 -1
- package/src/Order.ts +16 -9
- package/src/Query.test.ts +319 -2
- package/src/Query.ts +165 -51
- package/src/Relation.ts +14 -0
- package/src/Type.ts +5 -6
- package/src/index.ts +2 -0
- package/src/internal/Entity/api.ts +11 -1
- package/src/internal/Entity/model.ts +3 -0
- package/src/internal/Filter/match.ts +30 -1
- package/src/internal/JsonSchema/json-schema-type.ts +14 -2
- package/src/internal/JsonSchema/json-schema.test.ts +28 -1
- package/src/internal/JsonSchema/json-schema.ts +33 -6
- package/src/internal/Obj/atoms.ts +4 -2
- package/src/internal/Obj/json-serializer.test.ts +162 -2
- package/src/internal/Obj/json-serializer.ts +6 -0
- package/src/internal/Query/pretty.ts +19 -1
- package/src/internal/Query/query-result-effect.ts +2 -4
- package/src/internal/Ref/atoms.ts +1 -1
- package/src/internal/common/proxy/change-context.ts +1 -1
- package/src/internal/common/proxy/make-object.ts +42 -3
- package/src/internal/common/proxy/reactive.ts +5 -0
- package/src/internal/common/proxy/symbols.ts +1 -0
- package/src/internal/common/proxy/typed-handler.ts +71 -29
- package/src/internal/common/types/index.ts +1 -0
- package/src/internal/common/types/model-symbols.ts +6 -0
- package/src/internal/common/types/projection.ts +45 -0
- package/src/testing/test-data.ts +14 -14
- package/dist/lib/neutral/Annotation.mjs +0 -90
- package/dist/lib/neutral/Annotation.mjs.map +0 -7
- package/dist/lib/neutral/Database.mjs +0 -42
- package/dist/lib/neutral/Database.mjs.map +0 -7
- package/dist/lib/neutral/Entity.mjs +0 -76
- package/dist/lib/neutral/Entity.mjs.map +0 -7
- package/dist/lib/neutral/Err.mjs +0 -16
- package/dist/lib/neutral/Err.mjs.map +0 -7
- package/dist/lib/neutral/Feed.mjs +0 -50
- package/dist/lib/neutral/Feed.mjs.map +0 -7
- package/dist/lib/neutral/Filter.mjs +0 -72
- package/dist/lib/neutral/Filter.mjs.map +0 -7
- package/dist/lib/neutral/Format.mjs +0 -82
- package/dist/lib/neutral/Format.mjs.map +0 -7
- package/dist/lib/neutral/JsonSchema.mjs +0 -18
- package/dist/lib/neutral/JsonSchema.mjs.map +0 -7
- package/dist/lib/neutral/Key.mjs +0 -16
- package/dist/lib/neutral/Key.mjs.map +0 -7
- package/dist/lib/neutral/Migration.mjs +0 -18
- package/dist/lib/neutral/Migration.mjs.map +0 -7
- package/dist/lib/neutral/Obj.mjs +0 -128
- package/dist/lib/neutral/Obj.mjs.map +0 -7
- package/dist/lib/neutral/Order.mjs +0 -16
- package/dist/lib/neutral/Order.mjs.map +0 -7
- package/dist/lib/neutral/Query.mjs +0 -38
- package/dist/lib/neutral/Query.mjs.map +0 -7
- package/dist/lib/neutral/QueryResult.mjs +0 -2
- package/dist/lib/neutral/QueryResult.mjs.map +0 -7
- package/dist/lib/neutral/Ref.mjs +0 -25
- package/dist/lib/neutral/Ref.mjs.map +0 -7
- package/dist/lib/neutral/Registry.mjs +0 -14
- package/dist/lib/neutral/Registry.mjs.map +0 -7
- package/dist/lib/neutral/Relation.mjs +0 -97
- package/dist/lib/neutral/Relation.mjs.map +0 -7
- package/dist/lib/neutral/Scope.mjs +0 -12
- package/dist/lib/neutral/Scope.mjs.map +0 -7
- package/dist/lib/neutral/Tag.mjs +0 -32
- package/dist/lib/neutral/Tag.mjs.map +0 -7
- package/dist/lib/neutral/Text.mjs +0 -25
- package/dist/lib/neutral/Text.mjs.map +0 -7
- package/dist/lib/neutral/Type.mjs +0 -65
- package/dist/lib/neutral/Type.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2HKGAQM4.mjs +0 -1394
- package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2ZHECXUR.mjs +0 -95
- package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +0 -7
- package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs +0 -336
- package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +0 -7
- package/dist/lib/neutral/chunk-3TN2VMI4.mjs +0 -714
- package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +0 -7
- package/dist/lib/neutral/chunk-42L4HJ3Z.mjs +0 -104
- package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +0 -7
- package/dist/lib/neutral/chunk-4A2NWKRJ.mjs +0 -747
- package/dist/lib/neutral/chunk-4A2NWKRJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-7LOUAPYZ.mjs +0 -25
- package/dist/lib/neutral/chunk-7LOUAPYZ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +0 -1531
- package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BEFR7LEA.mjs +0 -170
- package/dist/lib/neutral/chunk-BEFR7LEA.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BXIKXT7Q.mjs +0 -89
- package/dist/lib/neutral/chunk-BXIKXT7Q.mjs.map +0 -7
- package/dist/lib/neutral/chunk-C57XMMBY.mjs +0 -206
- package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +0 -7
- package/dist/lib/neutral/chunk-C62JYFAD.mjs +0 -183
- package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +0 -7
- package/dist/lib/neutral/chunk-FSGX2R62.mjs +0 -48
- package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +0 -7
- package/dist/lib/neutral/chunk-HBXT3GSB.mjs +0 -459
- package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +0 -7
- package/dist/lib/neutral/chunk-IQR45HAD.mjs +0 -31
- package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-KCBW4BR6.mjs +0 -424
- package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-L3E5IXAE.mjs +0 -575
- package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-PRC4FEKJ.mjs +0 -56
- package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-SCT6FFCR.mjs +0 -351
- package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +0 -7
- package/dist/lib/neutral/chunk-TOCVVFK2.mjs +0 -75
- package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +0 -7
- package/dist/lib/neutral/chunk-U53LSQQA.mjs +0 -41
- package/dist/lib/neutral/chunk-U53LSQQA.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UGFFV2FR.mjs +0 -27
- package/dist/lib/neutral/chunk-UGFFV2FR.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UWLP4RFM.mjs +0 -7
- package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UZHCQ6NI.mjs +0 -283
- package/dist/lib/neutral/chunk-UZHCQ6NI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WISOH2XH.mjs +0 -36
- package/dist/lib/neutral/chunk-WISOH2XH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WKIT2YY5.mjs +0 -34
- package/dist/lib/neutral/chunk-WKIT2YY5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-XB6ISRAO.mjs +0 -271
- package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +0 -7
- package/dist/lib/neutral/chunk-XXL42WCB.mjs +0 -84
- package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +0 -7
- package/dist/lib/neutral/chunk-XXWK4J53.mjs +0 -117
- package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -115
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/internal/index.mjs +0 -581
- package/dist/lib/neutral/internal/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/testing/index.mjs +0 -477
- package/dist/lib/neutral/testing/index.mjs.map +0 -7
|
@@ -266,6 +266,33 @@ describe('effect-to-json', () => {
|
|
|
266
266
|
log('schema', { jsonSchema });
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
test('serialize a pair of mutually-recursive schemas (A embeds B, B embeds A)', () => {
|
|
270
|
+
// Unlike a schema that's self-referential (caught by suspendCache within one expansion), a
|
|
271
|
+
// *mutual* cycle across two distinct schemas isn't: each side starts a fresh suspendCache with
|
|
272
|
+
// no memory of the other already being in flight, so without the `inProgress` guard this
|
|
273
|
+
// recurses forever (A -> B -> A -> B -> ...).
|
|
274
|
+
interface A {
|
|
275
|
+
readonly kind: 'a';
|
|
276
|
+
readonly b?: B;
|
|
277
|
+
}
|
|
278
|
+
interface B {
|
|
279
|
+
readonly kind: 'b';
|
|
280
|
+
readonly a?: A;
|
|
281
|
+
}
|
|
282
|
+
const A: Schema.Schema<A> = Schema.Struct({
|
|
283
|
+
kind: Schema.Literal('a'),
|
|
284
|
+
b: Schema.optional(Schema.suspend((): Schema.Schema<B> => B)),
|
|
285
|
+
});
|
|
286
|
+
const B: Schema.Schema<B> = Schema.Struct({
|
|
287
|
+
kind: Schema.Literal('b'),
|
|
288
|
+
a: Schema.optional(Schema.suspend((): Schema.Schema<A> => A)),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const jsonSchema = toJsonSchema(A);
|
|
292
|
+
expect(jsonSchema.properties?.kind).toEqual({ type: 'string', enum: ['a'] });
|
|
293
|
+
expect(jsonSchema.properties?.b).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
269
296
|
test('tuple schema with description', () => {
|
|
270
297
|
const schema = Schema.Struct({
|
|
271
298
|
args: Schema.Tuple(
|
|
@@ -526,7 +553,7 @@ describe('json-to-effect', () => {
|
|
|
526
553
|
typename: 'com.example.type.project',
|
|
527
554
|
version: '0.1.0',
|
|
528
555
|
});
|
|
529
|
-
expect(getTypeIdentifierAnnotation(schema)).to.deep.eq('echo
|
|
556
|
+
expect(getTypeIdentifierAnnotation(schema)).to.deep.eq('echo:///01JERV1HQCQZDQ4NVCJ42QB38F');
|
|
530
557
|
});
|
|
531
558
|
|
|
532
559
|
test('symbol annotations get compared', () => {
|
|
@@ -120,8 +120,11 @@ export const toJsonSchema = (
|
|
|
120
120
|
return jsonSchema;
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
const _toJsonSchemaAST = (
|
|
124
|
-
|
|
123
|
+
const _toJsonSchemaAST = (
|
|
124
|
+
ast: SchemaAST.AST,
|
|
125
|
+
inProgress: Set<SchemaAST.AST> = new Set(),
|
|
126
|
+
): Types.DeepMutable<JsonSchemaType> => {
|
|
127
|
+
const withRefinements = withEchoRefinements(ast, '#', new Map(), inProgress);
|
|
125
128
|
const jsonSchema = JSONSchema.fromAST(withRefinements, {
|
|
126
129
|
definitions: {},
|
|
127
130
|
}) as Types.DeepMutable<JsonSchemaType>;
|
|
@@ -133,6 +136,9 @@ const withEchoRefinements = (
|
|
|
133
136
|
ast: SchemaAST.AST,
|
|
134
137
|
path: string | undefined,
|
|
135
138
|
suspendCache = new Map<SchemaAST.AST, string>(),
|
|
139
|
+
// Suspended ASTs whose expansion is currently in flight — unlike `suspendCache`, this is shared
|
|
140
|
+
// across the fresh caches each new expansion starts with, so it also catches mutual cycles.
|
|
141
|
+
inProgress: Set<SchemaAST.AST> = new Set(),
|
|
136
142
|
): SchemaAST.AST => {
|
|
137
143
|
if (path) {
|
|
138
144
|
suspendCache.set(ast, path);
|
|
@@ -144,21 +150,41 @@ const withEchoRefinements = (
|
|
|
144
150
|
const suspendedAst = ast.f();
|
|
145
151
|
const cachedPath = suspendCache.get(suspendedAst);
|
|
146
152
|
if (cachedPath) {
|
|
147
|
-
recursiveResult = new SchemaAST.Suspend(() => withEchoRefinements(suspendedAst, path, suspendCache), {
|
|
153
|
+
recursiveResult = new SchemaAST.Suspend(() => withEchoRefinements(suspendedAst, path, suspendCache, inProgress), {
|
|
148
154
|
[SchemaAST.JSONSchemaAnnotationId]: {
|
|
149
155
|
$ref: cachedPath,
|
|
150
156
|
},
|
|
151
157
|
});
|
|
158
|
+
} else if (inProgress.has(suspendedAst)) {
|
|
159
|
+
// Reached via a *different* suspended type already being expanded (e.g. two mutually-recursive
|
|
160
|
+
// schemas, A embedding B and B embedding A) — `suspendCache` alone won't catch this, since each
|
|
161
|
+
// fresh expansion below starts from an empty one. Stop with a permissive "any" placeholder
|
|
162
|
+
// instead of expanding again. The `type` key is required: effect's JSONSchema.fromAST only
|
|
163
|
+
// treats this annotation as a full override (vs. merging it onto a recomputed, and for a bare
|
|
164
|
+
// Suspend unsupported, structural schema) when it sees type/oneOf/anyOf/$ref.
|
|
165
|
+
recursiveResult = new SchemaAST.Suspend(() => withEchoRefinements(suspendedAst, path, suspendCache, inProgress), {
|
|
166
|
+
[SchemaAST.JSONSchemaAnnotationId]: {
|
|
167
|
+
$id: '/schemas/any',
|
|
168
|
+
type: ['string', 'number', 'boolean', 'object', 'array', 'null'],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
152
171
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
inProgress.add(suspendedAst);
|
|
173
|
+
const jsonSchema = _toJsonSchemaAST(suspendedAst, inProgress);
|
|
174
|
+
inProgress.delete(suspendedAst);
|
|
175
|
+
recursiveResult = new SchemaAST.Suspend(() => withEchoRefinements(suspendedAst, path, suspendCache, inProgress), {
|
|
155
176
|
[SchemaAST.JSONSchemaAnnotationId]: jsonSchema,
|
|
156
177
|
});
|
|
157
178
|
}
|
|
158
179
|
} else if (SchemaAST.isTypeLiteral(ast)) {
|
|
159
180
|
// Add property order annotations
|
|
160
181
|
recursiveResult = SchemaEx.mapAst(ast, (ast, key) =>
|
|
161
|
-
withEchoRefinements(
|
|
182
|
+
withEchoRefinements(
|
|
183
|
+
ast,
|
|
184
|
+
path && typeof key === 'string' ? `${path}/${key}` : undefined,
|
|
185
|
+
suspendCache,
|
|
186
|
+
inProgress,
|
|
187
|
+
),
|
|
162
188
|
);
|
|
163
189
|
recursiveResult = addJsonSchemaFields(recursiveResult, {
|
|
164
190
|
propertyOrder: [...ast.propertySignatures.map((p) => p.name)] as string[],
|
|
@@ -172,6 +198,7 @@ const withEchoRefinements = (
|
|
|
172
198
|
ast,
|
|
173
199
|
path && (typeof key === 'string' || typeof key === 'number') ? `${path}/${key}` : undefined,
|
|
174
200
|
suspendCache,
|
|
201
|
+
inProgress,
|
|
175
202
|
),
|
|
176
203
|
);
|
|
177
204
|
}
|
|
@@ -62,14 +62,16 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
|
|
|
62
62
|
return Atom.make<Obj.Snapshot<T> | undefined>((get) => {
|
|
63
63
|
let unsubscribeTarget: (() => void) | undefined;
|
|
64
64
|
|
|
65
|
-
const setupTargetSubscription = (target: T): Obj.Snapshot<T> => {
|
|
65
|
+
const setupTargetSubscription = (target: T): Obj.Snapshot<T> | undefined => {
|
|
66
66
|
unsubscribeTarget?.();
|
|
67
67
|
unsubscribeTarget = subscribe(target, () => {
|
|
68
68
|
// Deleted objects resolve to undefined so callers don't need to inspect isDeleted.
|
|
69
69
|
// getSnapshot adds SnapshotKindId brand at runtime; cast bridges static types.
|
|
70
70
|
get.setSelf(isDeleted(target) ? undefined : (getSnapshot(target) as unknown as Obj.Snapshot<T>));
|
|
71
71
|
});
|
|
72
|
-
|
|
72
|
+
// Guard the initial value too: an already-deleted target must resolve to undefined, not leak a
|
|
73
|
+
// snapshot until the next update.
|
|
74
|
+
return isDeleted(target) ? undefined : (getSnapshot(target) as unknown as Obj.Snapshot<T>);
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
get.addFinalizer(() => {
|
|
@@ -8,18 +8,27 @@ import { describe, expect, test } from 'vitest';
|
|
|
8
8
|
import { DXN, EID } from '@dxos/keys';
|
|
9
9
|
|
|
10
10
|
import * as Obj from '../../Obj';
|
|
11
|
+
import * as Relation from '../../Relation';
|
|
11
12
|
import { TestSchema } from '../../testing';
|
|
12
13
|
import * as Type from '../../Type';
|
|
13
14
|
import { getTypename, getTypeURI } from '../Annotation';
|
|
14
15
|
import { getMetaChecked } from '../common/api';
|
|
15
|
-
import {
|
|
16
|
+
import { makeDecodedEntityLive } from '../common/proxy';
|
|
17
|
+
import { type AnyEntity, ATTR_TYPE, EntityKind, KindId, TypeId, getSchema } from '../common/types';
|
|
16
18
|
import { MetaId } from '../common/types/model-symbols';
|
|
17
19
|
import { RelationSourceId, RelationTargetId, getObjectEchoUri } from '../Entity';
|
|
18
20
|
import * as JsonSchema from '../JsonSchema';
|
|
19
|
-
import { Ref, StaticRefResolver } from '../Ref';
|
|
21
|
+
import { Ref, type RefResolver, StaticRefResolver } from '../Ref';
|
|
20
22
|
import { createObject } from './create-object';
|
|
21
23
|
import { objectFromJSON, objectToJSON } from './json-serializer';
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Decode JSON and rewrap as a live reactive proxy — mirrors the feed hydration path
|
|
27
|
+
* (`objectFromJSON` then {@link makeDecodedEntityLive}).
|
|
28
|
+
*/
|
|
29
|
+
const decodeLive = async (json: unknown, options?: { refResolver?: RefResolver }): Promise<AnyEntity> =>
|
|
30
|
+
makeDecodedEntityLive(await objectFromJSON(json, options));
|
|
31
|
+
|
|
23
32
|
describe('Object JSON serializer', () => {
|
|
24
33
|
test('should serialize and deserialize object', async () => {
|
|
25
34
|
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
@@ -159,6 +168,157 @@ describe('Object JSON serializer', () => {
|
|
|
159
168
|
});
|
|
160
169
|
});
|
|
161
170
|
|
|
171
|
+
describe('live decode', () => {
|
|
172
|
+
test('decodes as a live proxy, not a plain snapshot', async () => {
|
|
173
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
174
|
+
const contactJson = objectToJSON(contact);
|
|
175
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
176
|
+
|
|
177
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
178
|
+
|
|
179
|
+
expect(Obj.instanceOf(TestSchema.Person)(live)).toBe(true);
|
|
180
|
+
expect(live.name).toBe('Alice');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('Obj.update mutates synchronously and notifies subscribers', async () => {
|
|
184
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
185
|
+
const contactJson = objectToJSON(contact);
|
|
186
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
187
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
188
|
+
|
|
189
|
+
let notified = 0;
|
|
190
|
+
const unsubscribe = Obj.subscribe(live, () => {
|
|
191
|
+
notified++;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
Obj.update(live, (live) => {
|
|
195
|
+
live.name = 'Bob';
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(live.name).toBe('Bob');
|
|
199
|
+
expect(notified).toBe(1);
|
|
200
|
+
unsubscribe();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('direct assignment outside Obj.update throws', async () => {
|
|
204
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
205
|
+
const contactJson = objectToJSON(contact);
|
|
206
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
207
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
208
|
+
|
|
209
|
+
expect(() => {
|
|
210
|
+
Reflect.set(live, 'name', 'Bob');
|
|
211
|
+
}).toThrow();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Obj.getSnapshot returns a non-live snapshot unaffected by later updates', async () => {
|
|
215
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
216
|
+
const contactJson = objectToJSON(contact);
|
|
217
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
218
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
219
|
+
|
|
220
|
+
const snapshot = Obj.getSnapshot(live);
|
|
221
|
+
Obj.update(live, (live) => {
|
|
222
|
+
live.name = 'Bob';
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(snapshot.name).toBe('Alice');
|
|
226
|
+
expect(live.name).toBe('Bob');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('meta is mutable within a change context', async () => {
|
|
230
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
231
|
+
const contactJson = objectToJSON(contact);
|
|
232
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
233
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
234
|
+
|
|
235
|
+
Obj.update(live, (live) => {
|
|
236
|
+
Obj.getMeta(live).keys.push({ id: 'abc', source: 'example.com' });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(Obj.getMeta(live).keys).toEqual([{ id: 'abc', source: 'example.com' }]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('live decode of a relation', async () => {
|
|
243
|
+
const alice = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
244
|
+
const org = Obj.make(TestSchema.Organization, { name: 'Acme' });
|
|
245
|
+
const relation = Relation.make(TestSchema.EmployedBy, {
|
|
246
|
+
[Relation.Source]: alice,
|
|
247
|
+
[Relation.Target]: org,
|
|
248
|
+
role: 'Engineer',
|
|
249
|
+
});
|
|
250
|
+
const relationJson = objectToJSON(relation as any);
|
|
251
|
+
const refResolver = new StaticRefResolver()
|
|
252
|
+
.addSchema(TestSchema.EmployedBy)
|
|
253
|
+
.addSchema(TestSchema.Person)
|
|
254
|
+
.addSchema(TestSchema.Organization)
|
|
255
|
+
.addObject(alice)
|
|
256
|
+
.addObject(org);
|
|
257
|
+
|
|
258
|
+
const live = (await decodeLive(relationJson, { refResolver })) as any;
|
|
259
|
+
|
|
260
|
+
let notified = 0;
|
|
261
|
+
const unsubscribe = Obj.subscribe(live, () => {
|
|
262
|
+
notified++;
|
|
263
|
+
});
|
|
264
|
+
Relation.update(live, (live) => {
|
|
265
|
+
live.role = 'Senior Engineer';
|
|
266
|
+
});
|
|
267
|
+
expect(live.role).toBe('Senior Engineer');
|
|
268
|
+
expect(notified).toBe(1);
|
|
269
|
+
unsubscribe();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('schemaless (unresolvable type) items stay plain, non-proxy snapshots', async () => {
|
|
273
|
+
const contact = createObject(TestSchema.Person, { name: 'Alice' });
|
|
274
|
+
const contactJson = objectToJSON(contact);
|
|
275
|
+
|
|
276
|
+
const live: any = await decodeLive(contactJson);
|
|
277
|
+
|
|
278
|
+
expect(getSchema(live)).toBeUndefined();
|
|
279
|
+
expect(live.name).toBe('Alice');
|
|
280
|
+
// No schema means `Obj.update` cannot validate mutations — direct assignment remains a
|
|
281
|
+
// silent no-op-safe plain-object write, not a throw.
|
|
282
|
+
live.name = 'Bob';
|
|
283
|
+
expect(live.name).toBe('Bob');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('a malformed decoded shape surfaces a clear error rather than a silent bad proxy', async () => {
|
|
287
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
288
|
+
const contactJson: any = objectToJSON(contact);
|
|
289
|
+
// Corrupt a field so it violates the schema; live decode must not swallow this.
|
|
290
|
+
contactJson.age = 'not-a-number';
|
|
291
|
+
const refResolver = new StaticRefResolver().addSchema(TestSchema.Person);
|
|
292
|
+
|
|
293
|
+
await expect(decodeLive(contactJson, { refResolver })).rejects.toThrow();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('nested array fields become reactive on live decode', async () => {
|
|
297
|
+
const task1 = Obj.make(TestSchema.Task, { title: 'Task 1' });
|
|
298
|
+
const contact = Obj.make(TestSchema.Person, { name: 'Alice', tasks: [Ref.make(task1)] });
|
|
299
|
+
const contactJson = objectToJSON(contact);
|
|
300
|
+
const refResolver = new StaticRefResolver()
|
|
301
|
+
.addSchema(TestSchema.Person)
|
|
302
|
+
.addSchema(TestSchema.Task)
|
|
303
|
+
.addObject(task1);
|
|
304
|
+
|
|
305
|
+
const live = (await decodeLive(contactJson, { refResolver })) as TestSchema.Person;
|
|
306
|
+
|
|
307
|
+
let notified = 0;
|
|
308
|
+
const unsubscribe = Obj.subscribe(live, () => {
|
|
309
|
+
notified++;
|
|
310
|
+
});
|
|
311
|
+
const task2 = Obj.make(TestSchema.Task, { title: 'Task 2' });
|
|
312
|
+
Obj.update(live, (live) => {
|
|
313
|
+
live.tasks!.push(Ref.make(task2));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(live.tasks).toHaveLength(2);
|
|
317
|
+
expect(notified).toBe(1);
|
|
318
|
+
unsubscribe();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
162
322
|
describe('Uint8Array', () => {
|
|
163
323
|
const Blob = Type.makeObject(DXN.make('com.example.type.blob', '0.1.0'))(
|
|
164
324
|
Schema.Struct({
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
ATTR_SELF_URI,
|
|
34
34
|
ATTR_SELF_URI_LEGACY,
|
|
35
35
|
ObjectDatabaseId,
|
|
36
|
+
ObjectDeletedId,
|
|
36
37
|
type ObjectJSON,
|
|
37
38
|
RelationSourceDXNId,
|
|
38
39
|
RelationSourceId,
|
|
@@ -175,6 +176,10 @@ export const objectFromJSON = async (
|
|
|
175
176
|
defineHiddenProperty(obj, ObjectDatabaseId, database);
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
// Carry the deletion marker so a tombstone snapshot (e.g. a feed item removed via `Feed.remove`)
|
|
180
|
+
// hydrates with `Obj.isDeleted === true` rather than as a live object.
|
|
181
|
+
defineHiddenProperty(obj, ObjectDeletedId, jsonData[ATTR_DELETED] === true);
|
|
182
|
+
|
|
178
183
|
assertObjectModel(obj);
|
|
179
184
|
invariant((obj as any)[ATTR_TYPE] === undefined, 'Invalid object model');
|
|
180
185
|
invariant((obj as any)[ATTR_META] === undefined, 'Invalid object model');
|
|
@@ -183,6 +188,7 @@ export const objectFromJSON = async (
|
|
|
183
188
|
invariant((obj as any)[ATTR_SELF_URI_LEGACY] === undefined, 'Invalid object model');
|
|
184
189
|
invariant((obj as any)[ATTR_RELATION_SOURCE] === undefined, 'Invalid object model');
|
|
185
190
|
invariant((obj as any)[ATTR_RELATION_TARGET] === undefined, 'Invalid object model');
|
|
191
|
+
|
|
186
192
|
return obj;
|
|
187
193
|
};
|
|
188
194
|
|
|
@@ -50,6 +50,8 @@ export const prettyFilter = (filter: QueryAST.Filter): string => {
|
|
|
50
50
|
return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
|
|
51
51
|
case 'in':
|
|
52
52
|
return `Filter.in([${filter.values.map((v) => JSON.stringify(v)).join(', ')}])`;
|
|
53
|
+
case 'in-query':
|
|
54
|
+
return `Filter.in(${prettyQuery(filter.subquery)}.project(${JSON.stringify(filter.property)}))`;
|
|
53
55
|
case 'contains':
|
|
54
56
|
return `Filter.contains(${JSON.stringify(filter.value)})`;
|
|
55
57
|
case 'tag':
|
|
@@ -113,7 +115,7 @@ export const prettyQuery = (query: QueryAST.Query): string => {
|
|
|
113
115
|
case 'order': {
|
|
114
116
|
const orders = query.order.map((o) => {
|
|
115
117
|
if (o.kind === 'natural') {
|
|
116
|
-
return
|
|
118
|
+
return `Order.natural(${JSON.stringify(o.direction)})`;
|
|
117
119
|
} else if (o.kind === 'rank') {
|
|
118
120
|
return `Order.rank(${JSON.stringify(o.direction)})`;
|
|
119
121
|
} else if (o.kind === 'timestamp') {
|
|
@@ -158,5 +160,21 @@ export const prettyQuery = (query: QueryAST.Query): string => {
|
|
|
158
160
|
}
|
|
159
161
|
case 'limit':
|
|
160
162
|
return `${prettyQuery(query.query)}.limit(${query.limit})`;
|
|
163
|
+
case 'skip':
|
|
164
|
+
return `${prettyQuery(query.query)}.skip(${query.skip})`;
|
|
165
|
+
case 'aggregate': {
|
|
166
|
+
const aggregates = query.aggregates.map((aggregate) => {
|
|
167
|
+
const arg =
|
|
168
|
+
aggregate.kind === 'items'
|
|
169
|
+
? aggregate.limit !== undefined
|
|
170
|
+
? `{ limit: ${aggregate.limit} }`
|
|
171
|
+
: ''
|
|
172
|
+
: aggregate.kind === 'count'
|
|
173
|
+
? ''
|
|
174
|
+
: JSON.stringify(aggregate.property);
|
|
175
|
+
return `${JSON.stringify(aggregate.name)}: Aggregate.${aggregate.kind}(${arg})`;
|
|
176
|
+
});
|
|
177
|
+
return `${prettyQuery(query.query)}.aggregate({ ${aggregates.join(', ')} })`;
|
|
178
|
+
}
|
|
161
179
|
}
|
|
162
180
|
};
|
|
@@ -6,8 +6,6 @@ import * as Effect from 'effect/Effect';
|
|
|
6
6
|
import * as Effectable from 'effect/Effectable';
|
|
7
7
|
import * as Option from 'effect/Option';
|
|
8
8
|
|
|
9
|
-
import { EffectEx } from '@dxos/effect';
|
|
10
|
-
|
|
11
9
|
import type * as QueryResult from '../../QueryResult';
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -19,9 +17,9 @@ export const makeQueryResultEffect = <T, E, R>(
|
|
|
19
17
|
eff: Effect.Effect<QueryResult.QueryResult<T>, E, R>,
|
|
20
18
|
): QueryResult.QueryResultEffect<T, E, R> => {
|
|
21
19
|
return {
|
|
22
|
-
run: Effect.flatMap(eff, (result) =>
|
|
20
|
+
run: Effect.flatMap(eff, (result) => Effect.promise(() => result.run())),
|
|
23
21
|
first: Effect.flatMap(eff, (result) =>
|
|
24
|
-
|
|
22
|
+
Effect.promise(async () => Option.fromNullable(await result.firstOrUndefined())),
|
|
25
23
|
),
|
|
26
24
|
|
|
27
25
|
// Effect internals: the result is itself an Effect, so it carries the commit prototype.
|
|
@@ -124,7 +124,7 @@ export const executeChange = (
|
|
|
124
124
|
batchEvents(() => callback(proxy));
|
|
125
125
|
} finally {
|
|
126
126
|
exitContext();
|
|
127
|
-
// Fire primary notification.
|
|
127
|
+
// Fire the primary notification (a real change).
|
|
128
128
|
if (hasPendingNotifications(contextKey)) {
|
|
129
129
|
clearPendingNotifications(contextKey);
|
|
130
130
|
(eventTarget as any)[EventId]?.emit();
|
|
@@ -7,13 +7,19 @@ import type * as Schema from 'effect/Schema';
|
|
|
7
7
|
import { EntityId } from '@dxos/keys';
|
|
8
8
|
|
|
9
9
|
import { getTypeAnnotation } from '../../Annotation/annotations';
|
|
10
|
-
import { type AnyProperties, KindId, ParentId, SchemaKindId, StaticTypeSchemaSlot } from '../types';
|
|
10
|
+
import { type AnyProperties, KindId, ParentId, SchemaId, SchemaKindId, StaticTypeSchemaSlot } from '../types';
|
|
11
11
|
import { type EntityMeta, EntityMetaSchema } from '../types/meta';
|
|
12
12
|
import { MetaId } from '../types/model-symbols';
|
|
13
13
|
import { defineHiddenProperty } from './define-hidden-property';
|
|
14
14
|
import { attachTypedJsonSerializer } from './json-serializer';
|
|
15
|
-
import { createProxy, getProxyTarget, isValidProxyTarget } from './proxy-utils';
|
|
16
|
-
import {
|
|
15
|
+
import { createProxy, getProxyTarget, isProxy, isValidProxyTarget } from './proxy-utils';
|
|
16
|
+
import {
|
|
17
|
+
TypedReactiveHandler,
|
|
18
|
+
TypeSource,
|
|
19
|
+
prepareDecodedTypedTarget,
|
|
20
|
+
prepareTypedTarget,
|
|
21
|
+
setMetaOwner,
|
|
22
|
+
} from './typed-handler';
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
*
|
|
@@ -96,6 +102,39 @@ const createReactiveObject = <T extends AnyProperties>(
|
|
|
96
102
|
return proxy;
|
|
97
103
|
};
|
|
98
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Rewrap an object decoded from JSON (via `objectFromJSON`) into a live reactive proxy, so that
|
|
107
|
+
* `Obj.update` synchronously mutates it and `Obj.subscribe`/atoms fire — the same contract as
|
|
108
|
+
* automerge-backed objects. Unlike {@link makeObject}, the target already carries `SchemaId`/
|
|
109
|
+
* `TypeId` stamped by `setSchema`/`setTypename` (both `configurable: false`), so this does not
|
|
110
|
+
* re-derive or re-stamp them — it only validates against the schema, reactifies nested arrays,
|
|
111
|
+
* rewraps `[MetaId]` as a live nested proxy, and wraps the whole object in a proxy.
|
|
112
|
+
*
|
|
113
|
+
* Objects whose type could not be resolved at decode time (no `SchemaId`) are returned unchanged —
|
|
114
|
+
* they remain plain snapshots and `Obj.update` on them stays a no-op, since there is no schema to
|
|
115
|
+
* validate mutations against.
|
|
116
|
+
*/
|
|
117
|
+
export const makeDecodedEntityLive = <T extends AnyProperties>(obj: T): T => {
|
|
118
|
+
const schema = (obj as any)[SchemaId] as Schema.Schema<T, any> | undefined;
|
|
119
|
+
if (!schema) {
|
|
120
|
+
return obj;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
prepareDecodedTypedTarget(obj, schema);
|
|
124
|
+
|
|
125
|
+
const meta = (obj as any)[MetaId];
|
|
126
|
+
if (meta != null && typeof meta === 'object' && !isProxy(meta)) {
|
|
127
|
+
prepareTypedTarget(meta, EntityMetaSchema);
|
|
128
|
+
const metaProxy = createProxy(meta, TypedReactiveHandler.instance);
|
|
129
|
+
defineHiddenProperty(obj, MetaId, metaProxy);
|
|
130
|
+
const metaTarget = getProxyTarget(metaProxy);
|
|
131
|
+
setMetaOwner(metaTarget, obj);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
attachTypedJsonSerializer(obj);
|
|
135
|
+
return createProxy(obj, TypedReactiveHandler.instance);
|
|
136
|
+
};
|
|
137
|
+
|
|
99
138
|
/**
|
|
100
139
|
* Set ID on ECHO object targets during creation.
|
|
101
140
|
* Used for objects with schema and the ones explicitly marked as Expando.
|
|
@@ -68,6 +68,11 @@ export type ChangeCallback<T> = (mutableObj: Mutable<T>) => void;
|
|
|
68
68
|
export const change = <T>(obj: T, callback: ChangeCallback<T>): void => {
|
|
69
69
|
// Check proxy first (allows handler to intercept), then fall back to target.
|
|
70
70
|
// This order is important for EchoReactiveHandler which handles ChangeId in the proxy trap.
|
|
71
|
+
//
|
|
72
|
+
// The direct-callback fallback (no `[ChangeId]`) now only applies to true snapshots and
|
|
73
|
+
// schemaless feed items (unresolvable type at decode time, see `makeDecodedEntityLive`) — live
|
|
74
|
+
// feed-backed objects carry a `[ChangeId]` (via `TypedReactiveHandler`) same as any other typed
|
|
75
|
+
// reactive object, so they take the `changeFn` branch above like automerge-backed objects do.
|
|
71
76
|
const changeFn = (obj as any)[ChangeId];
|
|
72
77
|
if (changeFn) {
|
|
73
78
|
changeFn(callback);
|
|
@@ -264,10 +264,17 @@ const compactMetadataToInstanceState = (target: ProxyTarget): void => {
|
|
|
264
264
|
}
|
|
265
265
|
const state = Object.create(TypedObjectPrototype);
|
|
266
266
|
for (const symbol of Object.getOwnPropertySymbols(target)) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
|
|
267
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, symbol)!;
|
|
268
|
+
Object.defineProperty(state, symbol, descriptor);
|
|
269
|
+
// Non-configurable symbols (e.g. `SchemaId`/`TypeId`, locked by `setSchema`/`setTypename` on
|
|
270
|
+
// objects decoded from JSON) cannot be deleted — leave them as an own property on the target,
|
|
271
|
+
// shadowing the copy on `state`; reads still resolve correctly since own properties take
|
|
272
|
+
// precedence over the prototype chain.
|
|
273
|
+
if (descriptor.configurable) {
|
|
274
|
+
// Dynamic delete over arbitrary symbol keys: `ProxyTarget` declares these as required, so the
|
|
275
|
+
// statically-typed view cannot express deleting them; the symbol set is only known at runtime.
|
|
276
|
+
delete (target as any)[symbol];
|
|
277
|
+
}
|
|
271
278
|
}
|
|
272
279
|
Object.setPrototypeOf(target, state);
|
|
273
280
|
};
|
|
@@ -295,7 +302,11 @@ export class TypedReactiveHandler implements ReactiveHandler<ProxyTarget> {
|
|
|
295
302
|
defineHiddenProperty(target, EventId, new Event());
|
|
296
303
|
}
|
|
297
304
|
|
|
298
|
-
|
|
305
|
+
// Default to not-deleted, but preserve a deletion marker already stamped on the target (e.g. by
|
|
306
|
+
// `objectFromJSON` when hydrating a tombstone snapshot).
|
|
307
|
+
if (!(ObjectDeletedId in target)) {
|
|
308
|
+
defineHiddenProperty(target, ObjectDeletedId, false);
|
|
309
|
+
}
|
|
299
310
|
|
|
300
311
|
// Only set owners if this is a root object (no existing owner).
|
|
301
312
|
// Nested objects already have owners set by their root's initialization.
|
|
@@ -606,31 +617,43 @@ export type TypeSource = { readonly [StaticTypeSchemaSlot]?: Schema.Schema.AnyNo
|
|
|
606
617
|
|
|
607
618
|
/**
|
|
608
619
|
* Recursively set AST on all potential proxy targets.
|
|
620
|
+
*
|
|
621
|
+
* @param skipOwnStamp Skip stamping `TypeId`/`SchemaId` on `obj` itself (still recurses into
|
|
622
|
+
* children, which always get stamped). Used for objects decoded from JSON, whose own `TypeId`/
|
|
623
|
+
* `SchemaId` are already set by `setTypename`/`setSchema` as `configurable: false` — redefining
|
|
624
|
+
* them here (with `configurable: true`) would throw.
|
|
609
625
|
*/
|
|
610
|
-
const setSchemaProperties = (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
626
|
+
const setSchemaProperties = (
|
|
627
|
+
obj: any,
|
|
628
|
+
schema: Schema.Schema.AnyNoContext,
|
|
629
|
+
typeSource?: TypeSource,
|
|
630
|
+
skipOwnStamp = false,
|
|
631
|
+
) => {
|
|
632
|
+
if (!skipOwnStamp) {
|
|
633
|
+
const schemaType = getSchemaURI(schema);
|
|
634
|
+
if (schemaType != null) {
|
|
635
|
+
defineHiddenProperty(obj, TypeId, schemaType);
|
|
636
|
+
}
|
|
615
637
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
638
|
+
if (typeSource != null) {
|
|
639
|
+
// Keep a back-reference to the type entity so `Obj.getType` /
|
|
640
|
+
// `Relation.getType` / `Entity.getType` can return it.
|
|
641
|
+
defineHiddenProperty(obj, TypeEntityId, typeSource);
|
|
642
|
+
|
|
643
|
+
// Install `SchemaId` as a getter that reads through the entity's static
|
|
644
|
+
// schema slot. The three entity shapes (static / persisted / in-memory
|
|
645
|
+
// pre-persist) each populate the slot via their own get-trap path, so
|
|
646
|
+
// `Type.update` / `Type.addFields` mutations propagate into validation
|
|
647
|
+
// for objects created via `Obj.make(typeEntity, ...)` without this file
|
|
648
|
+
// having to rebuild from `jsonSchema` itself.
|
|
649
|
+
Object.defineProperty(obj, SchemaId, {
|
|
650
|
+
get: () => typeSource[StaticTypeSchemaSlot] ?? schema,
|
|
651
|
+
enumerable: false,
|
|
652
|
+
configurable: true,
|
|
653
|
+
});
|
|
654
|
+
} else {
|
|
655
|
+
defineHiddenProperty(obj, SchemaId, schema);
|
|
656
|
+
}
|
|
634
657
|
}
|
|
635
658
|
|
|
636
659
|
if (Array.isArray(obj)) {
|
|
@@ -661,6 +684,15 @@ const setSchemaProperties = (obj: any, schema: Schema.Schema.AnyNoContext, typeS
|
|
|
661
684
|
// whose encoded form differs (e.g. refs encode as `{ '/': uri }`) are valid here.
|
|
662
685
|
export const prepareTypedTarget = <T>(target: T, schema: Schema.Schema<T, any>, typeSource?: TypeSource) => {
|
|
663
686
|
// log.info('prepareTypedTarget', { target, schema });
|
|
687
|
+
validateAndReactifyTarget(target, schema);
|
|
688
|
+
setSchemaProperties(target, schema, typeSource);
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Validate a target against its schema and convert nested arrays to `ReactiveArray`. Shared by
|
|
693
|
+
* {@link prepareTypedTarget} and {@link prepareDecodedTypedTarget}.
|
|
694
|
+
*/
|
|
695
|
+
export const validateAndReactifyTarget = <T>(target: T, schema: Schema.Schema<T, any>) => {
|
|
664
696
|
if (!SchemaAST.isTypeLiteral(schema.ast)) {
|
|
665
697
|
throw new Error('schema has to describe an object type');
|
|
666
698
|
}
|
|
@@ -669,7 +701,17 @@ export const prepareTypedTarget = <T>(target: T, schema: Schema.Schema<T, any>,
|
|
|
669
701
|
const _ = Schema.asserts(schema)(target);
|
|
670
702
|
SchemaValidator.assertExactProperties(schema, target, (path) => getDeep(target, path));
|
|
671
703
|
makeArraysReactive(target);
|
|
672
|
-
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Like {@link prepareTypedTarget}, for a target whose own `SchemaId`/`TypeId` are already stamped
|
|
708
|
+
* and locked `configurable: false` — e.g. an object decoded from JSON via `setSchema`/`setTypename`
|
|
709
|
+
* (see `objectFromJSON`). Validates and reactifies as usual, then stamps `SchemaId`/`TypeId` on
|
|
710
|
+
* nested records/arrays only, leaving the target's own (locked) stamps untouched.
|
|
711
|
+
*/
|
|
712
|
+
export const prepareDecodedTypedTarget = <T>(target: T, schema: Schema.Schema<T, any>) => {
|
|
713
|
+
validateAndReactifyTarget(target, schema);
|
|
714
|
+
setSchemaProperties(target, schema, undefined, true);
|
|
673
715
|
};
|
|
674
716
|
|
|
675
717
|
const makeArraysReactive = (target: any) => {
|