@dxos/echo 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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 +3 -3
- package/dist/lib/neutral/Database.mjs +6 -4
- package/dist/lib/neutral/Entity.mjs +16 -14
- package/dist/lib/neutral/Err.mjs +1 -1
- package/dist/lib/neutral/Extension.mjs +1 -1
- package/dist/lib/neutral/Feed.mjs +19 -17
- package/dist/lib/neutral/Filter.mjs +11 -11
- package/dist/lib/neutral/Format.mjs +3 -3
- package/dist/lib/neutral/JsonSchema.mjs +8 -8
- package/dist/lib/neutral/Key.mjs +1 -1
- package/dist/lib/neutral/Migration.mjs +17 -0
- package/dist/lib/neutral/Migration.mjs.map +7 -0
- package/dist/lib/neutral/Obj.mjs +14 -14
- 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 +7 -7
- package/dist/lib/neutral/Relation.mjs +15 -15
- package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
- package/dist/lib/neutral/Tag.mjs +14 -14
- package/dist/lib/neutral/Type.mjs +10 -10
- package/dist/lib/neutral/{chunk-7SQD3FRZ.mjs → chunk-2T22UGGN.mjs} +59 -12
- package/dist/lib/neutral/chunk-2T22UGGN.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-GZQTCRJB.mjs → chunk-44HT3MEC.mjs} +2 -2
- package/dist/lib/neutral/{chunk-WVLOCXB5.mjs → chunk-6VC3FI5E.mjs} +12 -8
- package/dist/lib/neutral/chunk-6VC3FI5E.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-HBJ7JT5A.mjs → chunk-7JFW72MX.mjs} +17 -5
- package/dist/lib/neutral/chunk-7JFW72MX.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-ANHVGJI4.mjs → chunk-7RVZT53K.mjs} +1 -1
- package/dist/lib/neutral/{chunk-BNCCGLJN.mjs → chunk-BICZKPQG.mjs} +1 -1
- package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs +36 -0
- package/dist/lib/neutral/chunk-CIWZ5MHQ.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-OLFCVPOO.mjs → chunk-DUNXPKAC.mjs} +4 -4
- package/dist/lib/neutral/{chunk-R72KFH2X.mjs → chunk-FAW7PJRO.mjs} +2 -2
- package/dist/lib/neutral/{chunk-E5PBQJWV.mjs → chunk-FAYW32CW.mjs} +2 -2
- package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs → chunk-GWFFC34K.mjs} +1 -1
- package/dist/lib/neutral/{chunk-YS6Q3XAD.mjs.map → chunk-GWFFC34K.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-T2JOLN37.mjs → chunk-I2MFJ76N.mjs} +6 -6
- package/dist/lib/neutral/chunk-I2MFJ76N.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-6URFBQJH.mjs → chunk-JALF2CVV.mjs} +5 -21
- package/dist/lib/neutral/chunk-JALF2CVV.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-EBVB5NOH.mjs → chunk-KQUQZ3CB.mjs} +15 -20
- package/dist/lib/neutral/chunk-KQUQZ3CB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-ZGVZNBBJ.mjs → chunk-LOTZLYHB.mjs} +17 -12
- package/dist/lib/neutral/chunk-LOTZLYHB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-TBKX6JQO.mjs → chunk-N4B7FHQT.mjs} +1 -1
- package/dist/lib/neutral/{chunk-UPWIIW2V.mjs → chunk-NKXEKBP5.mjs} +6 -22
- package/dist/lib/neutral/{chunk-UPWIIW2V.mjs.map → chunk-NKXEKBP5.mjs.map} +2 -2
- package/dist/lib/neutral/{chunk-YSLSJ7QS.mjs → chunk-NSMLBSFS.mjs} +17 -45
- package/dist/lib/neutral/chunk-NSMLBSFS.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-ZIXGDU6F.mjs → chunk-QBIGOSRF.mjs} +2 -2
- package/dist/lib/neutral/{chunk-FNEFSO2C.mjs → chunk-QBLYZ4IV.mjs} +12 -65
- package/dist/lib/neutral/{chunk-FNEFSO2C.mjs.map → chunk-QBLYZ4IV.mjs.map} +2 -2
- package/dist/lib/neutral/{chunk-5VKHCUDA.mjs → chunk-QEVM3JUP.mjs} +26 -7
- package/dist/lib/neutral/chunk-QEVM3JUP.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-QWAOTFCY.mjs → chunk-REP7WWAQ.mjs} +16 -66
- package/dist/lib/neutral/chunk-REP7WWAQ.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-DQYLD2RB.mjs → chunk-TRPZU2HV.mjs} +2 -2
- package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
- package/dist/lib/neutral/{chunk-46QNGNTY.mjs → chunk-TW76K7H5.mjs} +3 -3
- package/dist/lib/neutral/{chunk-FW7UJX25.mjs → chunk-UYJYDSD7.mjs} +67 -465
- package/dist/lib/neutral/chunk-UYJYDSD7.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
- package/dist/lib/neutral/{chunk-UBEZSGXY.mjs → chunk-ZISMEVKD.mjs} +1 -1
- package/dist/lib/neutral/{chunk-UBEZSGXY.mjs.map → chunk-ZISMEVKD.mjs.map} +2 -2
- package/dist/lib/neutral/index.mjs +33 -27
- package/dist/lib/neutral/internal/index.mjs +9 -9
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/testing/index.mjs +28 -27
- package/dist/lib/neutral/testing/index.mjs.map +1 -1
- package/dist/types/src/Collection.d.ts.map +1 -1
- package/dist/types/src/Database.d.ts +5 -0
- package/dist/types/src/Database.d.ts.map +1 -1
- package/dist/types/src/Dataset.d.ts +1 -1
- package/dist/types/src/Entity.d.ts +15 -9
- package/dist/types/src/Entity.d.ts.map +1 -1
- package/dist/types/src/Err.d.ts +18 -18
- package/dist/types/src/Err.d.ts.map +1 -1
- package/dist/types/src/Extension.d.ts +4 -4
- package/dist/types/src/Extension.d.ts.map +1 -1
- package/dist/types/src/Feed.d.ts +12 -1
- package/dist/types/src/Feed.d.ts.map +1 -1
- package/dist/types/src/Filter.d.ts +5 -3
- package/dist/types/src/Filter.d.ts.map +1 -1
- package/dist/types/src/Json.d.ts +33 -0
- package/dist/types/src/Json.d.ts.map +1 -0
- package/dist/types/src/Json.test.d.ts +2 -0
- package/dist/types/src/Json.test.d.ts.map +1 -0
- package/dist/types/src/JsonSchema.d.ts +1 -1
- package/dist/types/src/Migration.d.ts +57 -0
- package/dist/types/src/Migration.d.ts.map +1 -0
- package/dist/types/src/Obj.d.ts +22 -21
- package/dist/types/src/Obj.d.ts.map +1 -1
- package/dist/types/src/Order.d.ts.map +1 -1
- package/dist/types/src/Query.d.ts +5 -1
- package/dist/types/src/Query.d.ts.map +1 -1
- package/dist/types/src/Ref.d.ts.map +1 -1
- package/dist/types/src/Relation.d.ts +15 -15
- package/dist/types/src/Relation.d.ts.map +1 -1
- package/dist/types/src/Tag.d.ts +2 -2
- package/dist/types/src/Tag.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/View.d.ts.map +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/Annotation/annotations.d.ts +2 -2
- package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/sorting.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/util.d.ts +1 -1
- package/dist/types/src/internal/Annotation/util.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/version.d.ts.map +1 -1
- package/dist/types/src/internal/Format/date.d.ts.map +1 -1
- package/dist/types/src/internal/Format/format.d.ts.map +1 -1
- package/dist/types/src/internal/Format/number.d.ts.map +1 -1
- package/dist/types/src/internal/Format/object.d.ts.map +1 -1
- package/dist/types/src/internal/Format/types.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema-normalize.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +28 -28
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema.d.ts +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/common.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/deleted.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/ids.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/set-value.d.ts +1 -1
- package/dist/types/src/internal/Obj/set-value.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/snapshot.d.ts.map +1 -1
- package/dist/types/src/internal/Query.d.ts.map +1 -1
- package/dist/types/src/internal/Ref/ref.d.ts +13 -0
- package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
- package/dist/types/src/internal/Type/compose.d.ts.map +1 -1
- package/dist/types/src/internal/Type/echo-schema.d.ts +1 -1
- package/dist/types/src/internal/Type/echo-schema.d.ts.map +1 -1
- package/dist/types/src/internal/Type/manipulation.d.ts.map +1 -1
- package/dist/types/src/internal/common/api/meta.d.ts +3 -3
- package/dist/types/src/internal/common/api/meta.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/change-context.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/change-context.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/define-hidden-property.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/errors.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/errors.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/event-batch.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/ownership.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/reactive-array.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/reactive.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/reactive.test.d.ts +2 -0
- package/dist/types/src/internal/common/proxy/reactive.test.d.ts.map +1 -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.map +1 -1
- package/dist/types/src/internal/common/types/base.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/entity.d.ts +3 -3
- package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/version.d.ts +1 -1
- package/dist/types/src/testing/test-data.d.ts.map +1 -1
- package/dist/types/src/testing/test-schema.d.ts +53 -53
- package/dist/types/src/testing/test-schema.d.ts.map +1 -1
- package/dist/types/src/testing/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -13
- package/src/Collection.ts +1 -1
- package/src/Database.ts +35 -13
- package/src/Entity.ts +16 -9
- package/src/Extension.ts +3 -3
- package/src/Feed.ts +22 -1
- package/src/Filter.ts +9 -5
- package/src/Json.test.ts +175 -0
- package/src/Json.ts +102 -0
- package/src/Migration.ts +94 -0
- package/src/Obj.test.ts +12 -12
- package/src/Obj.ts +27 -24
- package/src/Query.test.ts +44 -11
- package/src/Query.ts +20 -0
- package/src/Relation.ts +21 -17
- package/src/index.ts +3 -0
- package/src/internal/Annotation/annotations.ts +5 -6
- package/src/internal/Obj/json-serializer.test.ts +1 -1
- package/src/internal/Obj/set-value.test.ts +15 -15
- package/src/internal/Obj/set-value.ts +1 -1
- package/src/internal/Query.ts +3 -0
- package/src/internal/Ref/ref.ts +17 -0
- package/src/internal/Type/echo-schema.ts +1 -1
- package/src/internal/common/README.md +1 -1
- package/src/internal/common/api/meta.ts +3 -3
- package/src/internal/common/proxy/change-context.ts +1 -1
- package/src/internal/common/proxy/change.test.ts +59 -59
- package/src/internal/common/proxy/errors.ts +2 -2
- package/src/internal/common/proxy/reactive-array.ts +1 -1
- package/src/internal/common/proxy/reactive.test.ts +54 -0
- package/src/internal/common/proxy/reactive.ts +11 -2
- package/src/internal/common/proxy/typed-handler.ts +7 -7
- package/src/internal/common/proxy/typed-object.test.ts +1 -1
- package/dist/lib/neutral/chunk-5VKHCUDA.mjs.map +0 -7
- package/dist/lib/neutral/chunk-6URFBQJH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-7SQD3FRZ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-EBVB5NOH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-FW7UJX25.mjs.map +0 -7
- package/dist/lib/neutral/chunk-HBJ7JT5A.mjs.map +0 -7
- package/dist/lib/neutral/chunk-QWAOTFCY.mjs.map +0 -7
- package/dist/lib/neutral/chunk-T2JOLN37.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WVLOCXB5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-YSLSJ7QS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-ZGVZNBBJ.mjs.map +0 -7
- /package/dist/lib/neutral/{chunk-GZQTCRJB.mjs.map → chunk-44HT3MEC.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-ANHVGJI4.mjs.map → chunk-7RVZT53K.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-BNCCGLJN.mjs.map → chunk-BICZKPQG.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-OLFCVPOO.mjs.map → chunk-DUNXPKAC.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-R72KFH2X.mjs.map → chunk-FAW7PJRO.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-E5PBQJWV.mjs.map → chunk-FAYW32CW.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-TBKX6JQO.mjs.map → chunk-N4B7FHQT.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-ZIXGDU6F.mjs.map → chunk-QBIGOSRF.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-DQYLD2RB.mjs.map → chunk-TRPZU2HV.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-UI6MWK5W.mjs.map → chunk-TTCSATUD.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-46QNGNTY.mjs.map → chunk-TW76K7H5.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-OMUPQMLR.mjs.map → chunk-V72DY6LU.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.abd8ff62ef",
|
|
4
4
|
"description": "ECHO API",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -73,6 +73,11 @@
|
|
|
73
73
|
"types": "./dist/types/src/Key.d.ts",
|
|
74
74
|
"default": "./dist/lib/neutral/Key.mjs"
|
|
75
75
|
},
|
|
76
|
+
"./Migration": {
|
|
77
|
+
"source": "./src/Migration.ts",
|
|
78
|
+
"types": "./dist/types/src/Migration.d.ts",
|
|
79
|
+
"default": "./dist/lib/neutral/Migration.mjs"
|
|
80
|
+
},
|
|
76
81
|
"./Obj": {
|
|
77
82
|
"source": "./src/Obj.ts",
|
|
78
83
|
"types": "./dist/types/src/Obj.d.ts",
|
|
@@ -130,18 +135,18 @@
|
|
|
130
135
|
],
|
|
131
136
|
"dependencies": {
|
|
132
137
|
"effect": "3.20.0",
|
|
133
|
-
"@dxos/async": "0.8.4-main.
|
|
134
|
-
"@dxos/
|
|
135
|
-
"@dxos/
|
|
136
|
-
"@dxos/
|
|
137
|
-
"@dxos/errors": "0.8.4-main.
|
|
138
|
-
"@dxos/invariant": "0.8.4-main.
|
|
139
|
-
"@dxos/log": "0.8.4-main.
|
|
140
|
-
"@dxos/
|
|
141
|
-
"@dxos/
|
|
142
|
-
"@dxos/
|
|
143
|
-
"@dxos/
|
|
144
|
-
"@dxos/
|
|
138
|
+
"@dxos/async": "0.8.4-main.abd8ff62ef",
|
|
139
|
+
"@dxos/echo-protocol": "0.8.4-main.abd8ff62ef",
|
|
140
|
+
"@dxos/debug": "0.8.4-main.abd8ff62ef",
|
|
141
|
+
"@dxos/effect": "0.8.4-main.abd8ff62ef",
|
|
142
|
+
"@dxos/errors": "0.8.4-main.abd8ff62ef",
|
|
143
|
+
"@dxos/invariant": "0.8.4-main.abd8ff62ef",
|
|
144
|
+
"@dxos/log": "0.8.4-main.abd8ff62ef",
|
|
145
|
+
"@dxos/keys": "0.8.4-main.abd8ff62ef",
|
|
146
|
+
"@dxos/node-std": "0.8.4-main.abd8ff62ef",
|
|
147
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef",
|
|
148
|
+
"@dxos/protocols": "0.8.4-main.abd8ff62ef",
|
|
149
|
+
"@dxos/context": "0.8.4-main.abd8ff62ef"
|
|
145
150
|
},
|
|
146
151
|
"publishConfig": {
|
|
147
152
|
"access": "public"
|
package/src/Collection.ts
CHANGED
package/src/Database.ts
CHANGED
|
@@ -229,24 +229,28 @@ export const resolve: {
|
|
|
229
229
|
}
|
|
230
230
|
invariant(!schema || isInstanceOf(schema, object), 'Object type mismatch.');
|
|
231
231
|
return object as any;
|
|
232
|
-
})) as any;
|
|
232
|
+
}).pipe(Effect.withSpan('Database.resolve'))) as any;
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* Loads an object reference.
|
|
236
236
|
*/
|
|
237
|
-
export const load: <T>(ref: Ref<T>) => Effect.Effect<T, Err.ObjectNotFoundError, never> = Effect.fn(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
237
|
+
export const load: <T>(ref: Ref<T>) => Effect.Effect<T, Err.ObjectNotFoundError, never> = Effect.fn('Database.load')(
|
|
238
|
+
function* (ref) {
|
|
239
|
+
const object = yield* promiseWithCauseCapture(() => ref.tryLoad());
|
|
240
|
+
if (!object) {
|
|
241
|
+
return yield* Effect.fail(new Err.ObjectNotFoundError(ref.dxn));
|
|
242
|
+
}
|
|
243
|
+
return object;
|
|
244
|
+
},
|
|
245
|
+
);
|
|
244
246
|
|
|
245
247
|
/**
|
|
246
248
|
* Loads an object reference option.
|
|
247
249
|
*/
|
|
248
250
|
// TODO(dmaretskyi): Do we need this -- you can just use `Effect.catchTag` in calling code instead.
|
|
249
|
-
export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, never, never> = Effect.fn(
|
|
251
|
+
export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, never, never> = Effect.fn(
|
|
252
|
+
'Database.loadOption',
|
|
253
|
+
)(function* (ref) {
|
|
250
254
|
const object = yield* load(ref).pipe(Effect.catchTag('ObjectNotFoundError', () => Effect.succeed(undefined)));
|
|
251
255
|
|
|
252
256
|
return Option.fromNullable(object);
|
|
@@ -257,21 +261,23 @@ export const loadOption: <T>(ref: Ref<T>) => Effect.Effect<Option.Option<T>, nev
|
|
|
257
261
|
* @see {@link Database.add}
|
|
258
262
|
*/
|
|
259
263
|
export const add = <T extends Entity.Unknown>(obj: T): Effect.Effect<T, never, Service> =>
|
|
260
|
-
Service.pipe(Effect.map(({ db }) => db.add(obj)));
|
|
264
|
+
Service.pipe(Effect.map(({ db }) => db.add(obj))).pipe(Effect.withSpan('Database.add'));
|
|
261
265
|
|
|
262
266
|
/**
|
|
263
267
|
* Removes an object from the database.
|
|
264
268
|
* @see {@link Database.remove}
|
|
265
269
|
*/
|
|
266
270
|
export const remove = <T extends Entity.Unknown>(obj: T): Effect.Effect<void, never, Service> =>
|
|
267
|
-
Service.pipe(Effect.map(({ db }) => db.remove(obj)));
|
|
271
|
+
Service.pipe(Effect.map(({ db }) => db.remove(obj))).pipe(Effect.withSpan('Database.remove'));
|
|
268
272
|
|
|
269
273
|
/**
|
|
270
274
|
* Flushes pending changes to disk.
|
|
271
275
|
* @see {@link Database.flush}
|
|
272
276
|
*/
|
|
273
277
|
export const flush = (opts?: FlushOptions) =>
|
|
274
|
-
Service.pipe(Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.flush(opts))))
|
|
278
|
+
Service.pipe(Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.flush(opts)))).pipe(
|
|
279
|
+
Effect.withSpan('Database.flush'),
|
|
280
|
+
);
|
|
275
281
|
|
|
276
282
|
/**
|
|
277
283
|
* Creates a `QueryResult` object that can be subscribed to.
|
|
@@ -292,7 +298,10 @@ export const runQuery: {
|
|
|
292
298
|
<Q extends Query.Any>(query: Q): Effect.Effect<Query.Type<Q>[], never, Service>;
|
|
293
299
|
<F extends Filter.Any>(filter: F): Effect.Effect<Filter.Type<F>[], never, Service>;
|
|
294
300
|
} = (queryOrFilter: Query.Any | Filter.Any) =>
|
|
295
|
-
query(queryOrFilter as any).pipe(
|
|
301
|
+
query(queryOrFilter as any).pipe(
|
|
302
|
+
Effect.flatMap((queryResult) => promiseWithCauseCapture(() => queryResult.run())),
|
|
303
|
+
Effect.withSpan('Database.runQuery'),
|
|
304
|
+
);
|
|
296
305
|
|
|
297
306
|
/**
|
|
298
307
|
* Executes the query once and returns the first result as or None.
|
|
@@ -305,6 +314,19 @@ export const runQueryFirst: {
|
|
|
305
314
|
Effect.flatMap((queryResult) =>
|
|
306
315
|
promiseWithCauseCapture(async () => Option.fromNullable(await queryResult.firstOrUndefined())),
|
|
307
316
|
),
|
|
317
|
+
Effect.withSpan('Database.runQueryFirst'),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Persists schemas in the database so they replicate to other clients.
|
|
322
|
+
* @see {@link SchemaRegistry.SchemaRegistry.register}
|
|
323
|
+
*/
|
|
324
|
+
export const registerSchema = (
|
|
325
|
+
input: SchemaRegistry.RegisterSchemaInput[],
|
|
326
|
+
): Effect.Effect<Type.RuntimeType[], never, Service> =>
|
|
327
|
+
Service.pipe(
|
|
328
|
+
Effect.flatMap(({ db }) => promiseWithCauseCapture(() => db.schemaRegistry.register(input))),
|
|
329
|
+
Effect.withSpan('Database.registerSchema'),
|
|
308
330
|
);
|
|
309
331
|
|
|
310
332
|
/**
|
package/src/Entity.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { DXN, ObjectId } from '@dxos/keys';
|
|
|
9
9
|
|
|
10
10
|
import * as internal from './internal';
|
|
11
11
|
import type * as Relation from './Relation';
|
|
12
|
+
import type * as Type from './Type';
|
|
12
13
|
|
|
13
14
|
// Re-export KindId and SnapshotKindId from internal.
|
|
14
15
|
export const KindId = internal.KindId;
|
|
@@ -131,6 +132,12 @@ export const getDXN = (entity: Unknown | Snapshot): DXN => internal.getDXN(entit
|
|
|
131
132
|
*/
|
|
132
133
|
export const getTypeDXN = internal.getTypeDXN;
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Get the schema of an entity.
|
|
137
|
+
* Returns the branded ECHO schema used to create the entity.
|
|
138
|
+
*/
|
|
139
|
+
export const getSchema: (entity: Unknown | Snapshot) => Type.AnyEntity | undefined = internal.getSchema as any;
|
|
140
|
+
|
|
134
141
|
/**
|
|
135
142
|
* Get the typename of an entity's type.
|
|
136
143
|
*/
|
|
@@ -191,7 +198,7 @@ export const subscribe = (entity: Unknown, callback: () => void): (() => void) =
|
|
|
191
198
|
//
|
|
192
199
|
|
|
193
200
|
/**
|
|
194
|
-
* Used to provide a mutable view of an entity within `Entity.
|
|
201
|
+
* Used to provide a mutable view of an entity within `Entity.update`.
|
|
195
202
|
*/
|
|
196
203
|
export type Mutable<T> = internal.Mutable<T>;
|
|
197
204
|
|
|
@@ -199,7 +206,7 @@ export type Mutable<T> = internal.Mutable<T>;
|
|
|
199
206
|
* Perform mutations on an entity (object or relation) within a change context.
|
|
200
207
|
*
|
|
201
208
|
* Entities are read-only by default. Mutations are batched and notifications fire
|
|
202
|
-
* when the callback completes. Direct mutations outside of `Entity.
|
|
209
|
+
* when the callback completes. Direct mutations outside of `Entity.update` will throw
|
|
203
210
|
* at runtime.
|
|
204
211
|
*
|
|
205
212
|
* @param entity - The echo entity (object or relation) to mutate.
|
|
@@ -207,30 +214,30 @@ export type Mutable<T> = internal.Mutable<T>;
|
|
|
207
214
|
*
|
|
208
215
|
* @example
|
|
209
216
|
* ```typescript
|
|
210
|
-
* // Mutate within Entity.
|
|
211
|
-
* Entity.
|
|
217
|
+
* // Mutate within Entity.update
|
|
218
|
+
* Entity.update(entity, (obj) => {
|
|
212
219
|
* obj.name = 'Updated';
|
|
213
220
|
* obj.count = 42;
|
|
214
221
|
* });
|
|
215
222
|
*
|
|
216
223
|
* // Direct mutation throws
|
|
217
|
-
* entity.name = 'Bob'; // Error: Cannot modify outside Entity.
|
|
224
|
+
* entity.name = 'Bob'; // Error: Cannot modify outside Entity.update()
|
|
218
225
|
* ```
|
|
219
226
|
*
|
|
220
|
-
* Note: For type-specific operations, prefer `Obj.
|
|
227
|
+
* Note: For type-specific operations, prefer `Obj.update` or `Relation.update`.
|
|
221
228
|
*/
|
|
222
|
-
export const
|
|
229
|
+
export const update = <T extends Unknown>(entity: T, callback: internal.ChangeCallback<T>): void => {
|
|
223
230
|
internal.change(entity, callback);
|
|
224
231
|
};
|
|
225
232
|
|
|
226
233
|
/**
|
|
227
234
|
* Add a tag to an entity.
|
|
228
|
-
* Must be called within an `Entity.
|
|
235
|
+
* Must be called within an `Entity.update`, `Obj.update`, or `Relation.update` callback.
|
|
229
236
|
*/
|
|
230
237
|
export const addTag = (entity: Mutable<Unknown>, tag: string): void => internal.addTag(entity, tag);
|
|
231
238
|
|
|
232
239
|
/**
|
|
233
240
|
* Remove a tag from an entity.
|
|
234
|
-
* Must be called within an `Entity.
|
|
241
|
+
* Must be called within an `Entity.update`, `Obj.update`, or `Relation.update` callback.
|
|
235
242
|
*/
|
|
236
243
|
export const removeTag = (entity: Mutable<Unknown>, tag: string): void => internal.removeTag(entity, tag);
|
package/src/Extension.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface Extension<T> extends Record<
|
|
|
43
43
|
* email: 'john@example.com',
|
|
44
44
|
* });
|
|
45
45
|
*
|
|
46
|
-
* Obj.
|
|
46
|
+
* Obj.update(obj, (obj) => {
|
|
47
47
|
* Extension.set(obj.extensions, ColorExtension, 'red');
|
|
48
48
|
* });
|
|
49
49
|
*
|
|
@@ -103,10 +103,10 @@ export const get: {
|
|
|
103
103
|
/**
|
|
104
104
|
* Set the value of an extension in a set of values.
|
|
105
105
|
*
|
|
106
|
-
* Can also be used within Obj.
|
|
106
|
+
* Can also be used within Obj.update callback:
|
|
107
107
|
*
|
|
108
108
|
* ```ts
|
|
109
|
-
* Obj.
|
|
109
|
+
* Obj.update(obj, (obj) => {
|
|
110
110
|
* Extension.set(obj.extensions, ColorExtension, 'red');
|
|
111
111
|
* });
|
|
112
112
|
* ```
|
package/src/Feed.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as Layer from 'effect/Layer';
|
|
|
10
10
|
import type * as Option from 'effect/Option';
|
|
11
11
|
import * as Schema from 'effect/Schema';
|
|
12
12
|
|
|
13
|
-
import { DXN } from '@dxos/keys';
|
|
13
|
+
import { DXN, type ObjectId } from '@dxos/keys';
|
|
14
14
|
|
|
15
15
|
import * as Annotation from './Annotation';
|
|
16
16
|
import type * as Entity from './Entity';
|
|
@@ -109,6 +109,27 @@ export const getQueueDxn = (feed: Feed): DXN | undefined => {
|
|
|
109
109
|
return new DXN(DXN.kind.QUEUE, [feed.namespace ?? 'data', self.spaceId, self.echoId]);
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Creates a Feed object from a queue DXN, inferring the feed's id and namespace from the DXN parts.
|
|
114
|
+
*
|
|
115
|
+
* The resulting Feed, when added to the same space as the queue, will have a queue DXN
|
|
116
|
+
* equal to the input (see `Feed.getQueueDxn`). Useful when migrating `Ref(Queue)` fields to
|
|
117
|
+
* `Ref(Feed.Feed)`.
|
|
118
|
+
*
|
|
119
|
+
* @remarks Unsafe because the caller must ensure the queue DXN's space matches the database
|
|
120
|
+
* the feed is added to; the feed id is set from the queue id, bypassing id generation.
|
|
121
|
+
*/
|
|
122
|
+
export const unsafeFromQueueDXN = (queueDxn: DXN): Feed => {
|
|
123
|
+
const parts = queueDxn.asQueueDXN();
|
|
124
|
+
if (!parts) {
|
|
125
|
+
throw new Error(`Expected a queue DXN, got: ${queueDxn.toString()}`);
|
|
126
|
+
}
|
|
127
|
+
return Obj.make(Feed, {
|
|
128
|
+
id: parts.queueId as ObjectId,
|
|
129
|
+
namespace: parts.subspaceTag === 'trace' ? 'trace' : undefined,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
112
133
|
//
|
|
113
134
|
// Service
|
|
114
135
|
//
|
package/src/Filter.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { type ForeignKey, type QueryAST } from '@dxos/echo-protocol';
|
|
|
13
13
|
import { assertArgument } from '@dxos/invariant';
|
|
14
14
|
import { DXN, ObjectId } from '@dxos/keys';
|
|
15
15
|
|
|
16
|
-
import type * as Entity from './Entity';
|
|
17
16
|
import * as internal from './internal';
|
|
17
|
+
import type * as Obj from './Obj';
|
|
18
18
|
import * as Ref from './Ref';
|
|
19
19
|
|
|
20
20
|
export interface Filter<T> {
|
|
@@ -346,14 +346,18 @@ export type ChildOfOptions = {
|
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
348
|
* Filter objects that are children of the specified parent(s).
|
|
349
|
-
* Accepts ECHO objects,
|
|
349
|
+
* Accepts ECHO objects, Refs, or arrays of either.
|
|
350
|
+
* Refs are resolved to DXNs without loading; objects use {@link Obj.getDXN}.
|
|
350
351
|
* With transitive=true (default), also matches grandchildren and beyond.
|
|
351
352
|
*/
|
|
352
|
-
export const childOf = (
|
|
353
|
+
export const childOf = (
|
|
354
|
+
parents: Obj.Unknown | Ref.Unknown | readonly (Obj.Unknown | Ref.Unknown)[],
|
|
355
|
+
options?: ChildOfOptions,
|
|
356
|
+
): Any => {
|
|
353
357
|
const items = Array.isArray(parents) ? parents : [parents];
|
|
354
358
|
const dxns = items.map((item) => {
|
|
355
|
-
if (item
|
|
356
|
-
return item.toString();
|
|
359
|
+
if (Ref.isRef(item)) {
|
|
360
|
+
return item.dxn.toString();
|
|
357
361
|
}
|
|
358
362
|
return internal.getDXN(item).toString();
|
|
359
363
|
});
|
package/src/Json.test.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { DXN, ObjectId } from '@dxos/keys';
|
|
8
|
+
import { safeStringify } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import * as Database from './Database';
|
|
11
|
+
import * as Json from './Json';
|
|
12
|
+
|
|
13
|
+
/** Mint a random ECHO object id usable as both a stub-db key and a DXN payload. */
|
|
14
|
+
const newId = (): string => ObjectId.random();
|
|
15
|
+
|
|
16
|
+
/** Build a fake encoded ref for a local-space object id. */
|
|
17
|
+
const encodeRef = (id: string): { '/': string } => ({ '/': DXN.fromLocalObjectId(id).toString() });
|
|
18
|
+
|
|
19
|
+
/** Minimal stub: `createRefReplacer` only touches `db.getObjectById`. */
|
|
20
|
+
const makeStubDb = (objects: Record<string, unknown>): Database.Database => {
|
|
21
|
+
return {
|
|
22
|
+
getObjectById: (id: string) => objects[id],
|
|
23
|
+
} as unknown as Database.Database;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run a value through the replacer's public contract — `JSON.stringify(value, replacer)` —
|
|
28
|
+
* then re-parse the result so tests can assert on plain JS shapes. This mirrors the way
|
|
29
|
+
* the JSON highlighter invokes the replacer in production.
|
|
30
|
+
*/
|
|
31
|
+
const stringifyWith = (replacer: Json.JsonReplacer, value: unknown): unknown =>
|
|
32
|
+
JSON.parse(JSON.stringify(value, replacer));
|
|
33
|
+
|
|
34
|
+
describe('createRefReplacer', () => {
|
|
35
|
+
test('passes plain values through unchanged', () => {
|
|
36
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
37
|
+
const subject = { a: 1, b: 'two', c: [3, { d: 4 }] };
|
|
38
|
+
expect(stringifyWith(replacer, subject)).toEqual(subject);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('inlines refs at default depth (1)', () => {
|
|
42
|
+
const id = newId();
|
|
43
|
+
const target = { name: 'inlined' };
|
|
44
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [id]: target }) });
|
|
45
|
+
const subject = { ref: encodeRef(id) };
|
|
46
|
+
expect(stringifyWith(replacer, subject)).toEqual({ ref: target });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('does not follow refs when depth is 0', () => {
|
|
50
|
+
const id = newId();
|
|
51
|
+
const target = { name: 'inlined' };
|
|
52
|
+
const ref = encodeRef(id);
|
|
53
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [id]: target }), depth: 0 });
|
|
54
|
+
expect(stringifyWith(replacer, { ref })).toEqual({ ref });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('inlines refs across multiple levels up to depth', () => {
|
|
58
|
+
const innerId = newId();
|
|
59
|
+
const middleId = newId();
|
|
60
|
+
const inner = { name: 'inner' };
|
|
61
|
+
const middle = { ref: encodeRef(innerId) };
|
|
62
|
+
const outer = { ref: encodeRef(middleId) };
|
|
63
|
+
const db = makeStubDb({ [innerId]: inner, [middleId]: middle });
|
|
64
|
+
|
|
65
|
+
expect(stringifyWith(Json.createRefReplacer({ db, depth: 1 }), outer)).toEqual({
|
|
66
|
+
ref: { ref: encodeRef(innerId) },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(stringifyWith(Json.createRefReplacer({ db, depth: 2 }), outer)).toEqual({
|
|
70
|
+
ref: { ref: inner },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('leaves refs encoded when the target is missing in the db', () => {
|
|
75
|
+
const ref = encodeRef(newId());
|
|
76
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
77
|
+
expect(stringifyWith(replacer, { ref })).toEqual({ ref });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('leaves non-DXN single-key { "/": string } objects untouched', () => {
|
|
81
|
+
// Same `{ '/': string }` shape is used by other IPLD-style refs (e.g. CIDs); those should
|
|
82
|
+
// not crash the replacer and should pass through verbatim.
|
|
83
|
+
const cidLike = { '/': 'bafybeibwzifw7izxykxz' };
|
|
84
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
85
|
+
expect(stringifyWith(replacer, { ref: cidLike })).toEqual({ ref: cidLike });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('leaves malformed dxn strings untouched', () => {
|
|
89
|
+
const malformed = { '/': 'dxn:not-a-real-dxn' };
|
|
90
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
91
|
+
expect(stringifyWith(replacer, { ref: malformed })).toEqual({ ref: malformed });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('leaves non-echo dxns untouched (e.g. type DXN)', () => {
|
|
95
|
+
// Type DXNs share the `dxn:` prefix but `asEchoDXN()` returns undefined.
|
|
96
|
+
const typeRef = { '/': DXN.fromTypename('com.example.Thing').toString() };
|
|
97
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
98
|
+
expect(stringifyWith(replacer, { ref: typeRef })).toEqual({ ref: typeRef });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('inlines refs inside arrays', () => {
|
|
102
|
+
const idA = newId();
|
|
103
|
+
const idB = newId();
|
|
104
|
+
const a = { name: 'a' };
|
|
105
|
+
const b = { name: 'b' };
|
|
106
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [idA]: a, [idB]: b }) });
|
|
107
|
+
expect(stringifyWith(replacer, { items: [encodeRef(idA), encodeRef(idB), { plain: true }] })).toEqual({
|
|
108
|
+
items: [a, b, { plain: true }],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('walks nested objects recursively', () => {
|
|
113
|
+
const innerId = newId();
|
|
114
|
+
const inner = { name: 'inner' };
|
|
115
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }) });
|
|
116
|
+
const subject = { outer: { mid: { ref: encodeRef(innerId) } } };
|
|
117
|
+
expect(stringifyWith(replacer, subject)).toEqual({ outer: { mid: { ref: inner } } });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a single replacer invocation does not recurse on its own', () => {
|
|
121
|
+
// The replacer is per-call; JSON.stringify drives the tree walk. Calling the replacer
|
|
122
|
+
// directly on a cyclic input must therefore return without touching the cycle.
|
|
123
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({}) });
|
|
124
|
+
const node: any = { name: 'self' };
|
|
125
|
+
node.self = node;
|
|
126
|
+
|
|
127
|
+
expect(() => replacer('', node)).not.toThrow();
|
|
128
|
+
expect(replacer('', node)).toBe(node);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('invokes `toJSON` on resolved targets so refs in the target are re-walked', () => {
|
|
132
|
+
// Simulates the ECHO-object branch: `db.getObjectById` returns a live proxy, the replacer
|
|
133
|
+
// calls `.toJSON()` to get the encoded form, then continues walking that form. A ref nested
|
|
134
|
+
// inside the target should be inlined when there's depth budget remaining.
|
|
135
|
+
const outerId = newId();
|
|
136
|
+
const innerId = newId();
|
|
137
|
+
const inner = { name: 'inner' };
|
|
138
|
+
const target = {
|
|
139
|
+
toJSON: () => ({ nestedRef: encodeRef(innerId) }),
|
|
140
|
+
};
|
|
141
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [outerId]: target, [innerId]: inner }), depth: 2 });
|
|
142
|
+
expect(stringifyWith(replacer, { ref: encodeRef(outerId) })).toEqual({
|
|
143
|
+
ref: { nestedRef: inner },
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('depth budget counts ref hops, not tree depth — a ref deep in a plain tree still resolves', () => {
|
|
148
|
+
// A ref nested under arbitrarily many plain objects is one ref hop from the root, so
|
|
149
|
+
// `depth: 1` resolves it. `depth: 0` leaves it encoded.
|
|
150
|
+
const innerId = newId();
|
|
151
|
+
const inner = { name: 'inner' };
|
|
152
|
+
const subject = { a: { b: { c: { d: { ref: encodeRef(innerId) } } } } };
|
|
153
|
+
|
|
154
|
+
const inlining = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }), depth: 1 });
|
|
155
|
+
expect(stringifyWith(inlining, subject)).toEqual({ a: { b: { c: { d: { ref: inner } } } } });
|
|
156
|
+
|
|
157
|
+
const passthrough = Json.createRefReplacer({ db: makeStubDb({ [innerId]: inner }), depth: 0 });
|
|
158
|
+
expect(stringifyWith(passthrough, subject)).toEqual(subject);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('inlines refs when invoked through safeStringify (production path)', () => {
|
|
162
|
+
// `JsonHighlighter` runs the replacer through `@dxos/util/safeStringify`, whose inner
|
|
163
|
+
// wrapper short-circuits the root call without forwarding it to the user's filter. The
|
|
164
|
+
// replacer must therefore work on a per-call basis — not as a one-shot root tree walk.
|
|
165
|
+
// This regression-tests that integration: the `content` ref must inline.
|
|
166
|
+
const targetId = newId();
|
|
167
|
+
const target = { toJSON: () => ({ name: 'README content' }) };
|
|
168
|
+
const document = { id: '01ABC', name: 'README', content: encodeRef(targetId) };
|
|
169
|
+
|
|
170
|
+
const replacer = Json.createRefReplacer({ db: makeStubDb({ [targetId]: target }) });
|
|
171
|
+
const out = JSON.parse(safeStringify(document, replacer, 0)!);
|
|
172
|
+
|
|
173
|
+
expect(out).toEqual({ id: '01ABC', name: 'README', content: { name: 'README content' } });
|
|
174
|
+
});
|
|
175
|
+
});
|
package/src/Json.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { DXN } from '@dxos/keys';
|
|
6
|
+
|
|
7
|
+
import * as Database from './Database';
|
|
8
|
+
import * as Obj from './Obj';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `JSON.stringify` replacer signature.
|
|
12
|
+
*
|
|
13
|
+
* Defined here (rather than re-imported from a UI package) so other ECHO-aware utilities can
|
|
14
|
+
* share a stable signature without creating a dependency edge into the UI tree.
|
|
15
|
+
*/
|
|
16
|
+
export type JsonReplacer = (key: string, value: any) => any;
|
|
17
|
+
|
|
18
|
+
export type CreateRefReplacerOptions = {
|
|
19
|
+
db: Database.Database;
|
|
20
|
+
/** How many ref hops to follow. `0` leaves all refs as-is. Default: `1`. */
|
|
21
|
+
depth?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isEncodedRef = (value: unknown): value is { '/': string } =>
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
value !== null &&
|
|
27
|
+
Object.keys(value as object).length === 1 &&
|
|
28
|
+
typeof (value as { '/': unknown })['/'] === 'string';
|
|
29
|
+
|
|
30
|
+
const toJson = (obj: Obj.Any): unknown => (typeof (obj as any).toJSON === 'function' ? (obj as any).toJSON() : obj);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns a {@link JsonReplacer} that inlines ECHO ref objects (`{ "/": "dxn:echo:..." }`) up to
|
|
34
|
+
* `depth` ref hops. Beyond that depth refs are left in their encoded form.
|
|
35
|
+
*
|
|
36
|
+
* Implemented as a per-call `JSON.stringify` replacer (not a one-shot tree walk at root) so it
|
|
37
|
+
* composes with wrappers like `safeStringify` that intercept the root call. JSON.stringify
|
|
38
|
+
* already drives the recursion; we only need to (a) detect a ref at the current callback,
|
|
39
|
+
* (b) resolve and return the target if hop budget remains, and (c) tag the returned object
|
|
40
|
+
* with its hop count so children know how far in they are.
|
|
41
|
+
*
|
|
42
|
+
* The hop count is tracked per-object via a `WeakMap`: a ref-resolved target's children inherit
|
|
43
|
+
* `parentHops + 1`; a regular intermediate object's children inherit `parentHops`. This makes the
|
|
44
|
+
* budget count *ref hops*, not tree depth — a ref deep in a tree still resolves once when
|
|
45
|
+
* `depth >= 1`.
|
|
46
|
+
*
|
|
47
|
+
* Note: ECHO objects' `toJSON` runs before the replacer is invoked, so by the time we see a
|
|
48
|
+
* value refs are already encoded as `{ "/": "dxn:..." }`.
|
|
49
|
+
*/
|
|
50
|
+
export const createRefReplacer = ({ db, depth = 1 }: CreateRefReplacerOptions): JsonReplacer => {
|
|
51
|
+
// Per-object hop count. Set when we return an object (via ref resolution or pass-through) so
|
|
52
|
+
// the child callbacks (which carry that object as `this`) can read it.
|
|
53
|
+
const hops = new WeakMap<object, number>();
|
|
54
|
+
|
|
55
|
+
return function (this: any, key: string, value: any) {
|
|
56
|
+
// Hop count for this call: hops at the parent, or 0 for the root.
|
|
57
|
+
const parentHops = this && typeof this === 'object' ? (hops.get(this) ?? 0) : 0;
|
|
58
|
+
if (isEncodedRef(value)) {
|
|
59
|
+
if (parentHops >= depth) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The `{ '/': string }` shape is shared with non-DXN IPLD-style refs (e.g. CIDs);
|
|
64
|
+
// an unparseable string would otherwise crash the whole `JSON.stringify`.
|
|
65
|
+
// Treat any parse miss as "leave as-is" rather than propagating.
|
|
66
|
+
const dxnString = value['/'];
|
|
67
|
+
if (!dxnString.startsWith('dxn:')) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let echoId: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
echoId = DXN.parse(dxnString).asEchoDXN()?.echoId;
|
|
74
|
+
} catch {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!echoId) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
const target = db.getObjectById(echoId);
|
|
82
|
+
if (!target) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const encoded = toJson(target);
|
|
87
|
+
if (encoded != null && typeof encoded === 'object') {
|
|
88
|
+
// Children of the resolved target are one hop deeper.
|
|
89
|
+
hops.set(encoded as object, parentHops + 1);
|
|
90
|
+
}
|
|
91
|
+
return encoded;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pass-through object: children inherit the parent's hop count (this branch doesn't burn
|
|
95
|
+
// budget).
|
|
96
|
+
if (value != null && typeof value === 'object') {
|
|
97
|
+
hops.set(value, parentHops);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
};
|