@dxos/echo 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29
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/README.md +2 -2
- package/dist/lib/neutral/Annotation.mjs +5 -3
- package/dist/lib/neutral/Database.mjs +8 -4
- package/dist/lib/neutral/Entity.mjs +16 -14
- package/dist/lib/neutral/Err.mjs +1 -1
- package/dist/lib/neutral/Extension.mjs +18 -0
- package/dist/lib/neutral/Extension.mjs.map +7 -0
- package/dist/lib/neutral/Feed.mjs +21 -17
- package/dist/lib/neutral/Filter.mjs +21 -13
- 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 +16 -13
- package/dist/lib/neutral/Order.mjs +1 -1
- package/dist/lib/neutral/Query.mjs +19 -16
- package/dist/lib/neutral/QueryResult.mjs +1 -1
- package/dist/lib/neutral/Ref.mjs +7 -7
- package/dist/lib/neutral/Relation.mjs +15 -14
- package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
- package/dist/lib/neutral/Tag.mjs +14 -13
- package/dist/lib/neutral/Type.mjs +10 -10
- package/dist/lib/neutral/{chunk-FZO7LQO7.mjs → chunk-44HT3MEC.mjs} +2 -2
- package/dist/lib/neutral/{chunk-OENWMTE6.mjs → chunk-5SL5LDLD.mjs} +4 -2
- package/dist/lib/neutral/chunk-5SL5LDLD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-YWXWXIE5.mjs → chunk-6VIJV543.mjs} +2 -2
- package/dist/lib/neutral/{chunk-YWXWXIE5.mjs.map → chunk-6VIJV543.mjs.map} +2 -2
- 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-NEGC54NE.mjs → chunk-FHYIM4RD.mjs} +17 -19
- package/dist/lib/neutral/chunk-FHYIM4RD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs → chunk-FZHVQEHN.mjs} +6 -22
- package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs.map → chunk-FZHVQEHN.mjs.map} +3 -3
- package/dist/lib/neutral/chunk-GWFFC34K.mjs +50 -0
- package/dist/lib/neutral/chunk-GWFFC34K.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-724GQYEN.mjs → chunk-HPNQTEEQ.mjs} +4 -4
- package/dist/lib/neutral/{chunk-UFDK26FO.mjs → chunk-LVGOVFDV.mjs} +12 -8
- package/dist/lib/neutral/chunk-LVGOVFDV.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-43Y5DOS6.mjs → chunk-MPAI2MHO.mjs} +16 -66
- package/dist/lib/neutral/chunk-MPAI2MHO.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-WYOKA6AE.mjs → chunk-N4B7FHQT.mjs} +2 -2
- package/dist/lib/neutral/{chunk-WYOKA6AE.mjs.map → chunk-N4B7FHQT.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-BOZZPUXE.mjs → chunk-O5LRY6CO.mjs} +12 -65
- package/dist/lib/neutral/chunk-O5LRY6CO.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-UTBRYVQC.mjs → chunk-QGMIH2SN.mjs} +2 -2
- package/dist/lib/neutral/{chunk-VA2F47ZC.mjs → chunk-QXIANHKU.mjs} +68 -14
- package/dist/lib/neutral/chunk-QXIANHKU.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NXMFBIT5.mjs → chunk-RF7ZBZ4A.mjs} +59 -4
- package/dist/lib/neutral/chunk-RF7ZBZ4A.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-DYDJBZEG.mjs → chunk-S7IMFVTB.mjs} +51 -21
- package/dist/lib/neutral/chunk-S7IMFVTB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-TQQZLKB7.mjs → chunk-SJKBWMJY.mjs} +193 -469
- package/dist/lib/neutral/chunk-SJKBWMJY.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SUZMWP3Y.mjs +36 -0
- package/dist/lib/neutral/chunk-SUZMWP3Y.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NQRLRYJM.mjs → chunk-SW5CUSBY.mjs} +119 -12
- package/dist/lib/neutral/chunk-SW5CUSBY.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-SEMVAGBM.mjs → chunk-TNBK56IN.mjs} +19 -24
- package/dist/lib/neutral/chunk-TNBK56IN.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-B5OXLWZL.mjs → chunk-TRPZU2HV.mjs} +2 -2
- package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
- package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
- package/dist/lib/neutral/{chunk-KTAWP7GI.mjs → chunk-VGKLHHRT.mjs} +42 -7
- package/dist/lib/neutral/chunk-VGKLHHRT.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-FXEG7EOK.mjs → chunk-VYAGNFSJ.mjs} +3 -3
- package/dist/lib/neutral/{chunk-CIRHIEMW.mjs → chunk-WRVRDZDA.mjs} +18 -46
- package/dist/lib/neutral/chunk-WRVRDZDA.mjs.map +7 -0
- 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 +38 -28
- package/dist/lib/neutral/internal/index.mjs +15 -9
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/testing/index.mjs +185 -129
- package/dist/lib/neutral/testing/index.mjs.map +3 -3
- package/dist/types/src/Annotation.d.ts +1 -1
- package/dist/types/src/Annotation.d.ts.map +1 -1
- package/dist/types/src/Collection.d.ts.map +1 -1
- package/dist/types/src/Database.d.ts +13 -1
- package/dist/types/src/Database.d.ts.map +1 -1
- package/dist/types/src/Dataset.d.ts +2 -1
- package/dist/types/src/Dataset.d.ts.map +1 -1
- package/dist/types/src/Entity.d.ts +17 -11
- 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 +80 -0
- package/dist/types/src/Extension.d.ts.map +1 -0
- package/dist/types/src/Extension.test.d.ts +2 -0
- package/dist/types/src/Extension.test.d.ts.map +1 -0
- package/dist/types/src/Feed.d.ts +37 -20
- package/dist/types/src/Feed.d.ts.map +1 -1
- package/dist/types/src/Filter.d.ts +33 -4
- package/dist/types/src/Filter.d.ts.map +1 -1
- package/dist/types/src/Filter.test.d.ts +2 -0
- package/dist/types/src/Filter.test.d.ts.map +1 -0
- 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 +41 -27
- 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 +13 -2
- 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 +17 -18
- 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 +3 -3
- 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/exemplars.test.d.ts +2 -0
- package/dist/types/src/exemplars.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/annotations.d.ts +12 -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/object.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 +4 -3
- 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 +10 -0
- package/dist/types/src/internal/Query.d.ts.map +1 -0
- package/dist/types/src/internal/Ref/ref-array.d.ts.map +1 -1
- package/dist/types/src/internal/Ref/ref.d.ts +14 -1
- 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 +2 -2
- 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/make-object.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 +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.map +1 -1
- package/dist/types/src/internal/common/types/version.d.ts +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-data.d.ts +8 -8
- 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 +24 -14
- package/src/Annotation.ts +1 -0
- package/src/Collection.ts +5 -0
- package/src/Database.ts +49 -14
- package/src/Entity.ts +18 -12
- package/src/Extension.test.ts +235 -0
- package/src/Extension.ts +122 -0
- package/src/Feed.ts +79 -32
- package/src/Filter.test.ts +90 -0
- package/src/Filter.ts +66 -3
- package/src/Json.test.ts +175 -0
- package/src/Json.ts +102 -0
- package/src/Migration.ts +94 -0
- package/src/Obj.test.ts +105 -13
- package/src/Obj.ts +153 -33
- package/src/Query.test.ts +198 -8
- package/src/Query.ts +46 -1
- package/src/Relation.ts +24 -20
- package/src/Type.ts +1 -1
- package/src/View.ts +5 -0
- package/src/exemplars.test.ts +21 -0
- package/src/index.ts +4 -0
- package/src/internal/Annotation/annotations.test.ts +51 -2
- package/src/internal/Annotation/annotations.ts +33 -14
- package/src/internal/Annotation/sorting.ts +0 -1
- package/src/internal/Entity/api.ts +0 -1
- package/src/internal/Entity/object.ts +0 -1
- package/src/internal/Entity/version.ts +0 -1
- package/src/internal/Format/date.test.ts +0 -1
- package/src/internal/Format/format.test.ts +0 -1
- package/src/internal/JsonSchema/json-schema-type.ts +1 -1
- package/src/internal/JsonSchema/json-schema.test.ts +1 -2
- package/src/internal/JsonSchema/json-schema.ts +1 -2
- package/src/internal/Obj/clone.ts +1 -1
- package/src/internal/Obj/create-object.test.ts +2 -4
- package/src/internal/Obj/create-object.ts +2 -3
- package/src/internal/Obj/deleted.ts +1 -1
- package/src/internal/Obj/json-serializer.test.ts +4 -5
- package/src/internal/Obj/json-serializer.ts +22 -15
- package/src/internal/Obj/set-value.test.ts +24 -24
- package/src/internal/Obj/set-value.ts +1 -1
- package/src/internal/Query.ts +137 -0
- package/src/internal/Ref/ref-array.ts +0 -1
- package/src/internal/Ref/ref.test.ts +0 -1
- package/src/internal/Ref/ref.ts +18 -1
- package/src/internal/Type/compose.test.ts +0 -1
- package/src/internal/Type/echo-schema.ts +3 -4
- package/src/internal/Type/persistent-schema.ts +2 -2
- 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 +94 -94
- package/src/internal/common/proxy/errors.ts +2 -2
- package/src/internal/common/proxy/handler.test.ts +0 -2
- package/src/internal/common/proxy/make-object.ts +0 -1
- package/src/internal/common/proxy/ownership.ts +0 -1
- 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 -3
- package/src/internal/common/proxy/typed-handler.test.ts +0 -1
- package/src/internal/common/proxy/typed-handler.ts +8 -10
- package/src/internal/common/proxy/typed-object.test.ts +2 -3
- package/src/internal/common/types/entity.ts +1 -1
- package/src/internal/common/types/meta.ts +0 -1
- package/src/internal/index.ts +1 -0
- package/src/testing/api.test.ts +0 -1
- package/src/testing/test-data.ts +157 -98
- package/dist/lib/neutral/chunk-43Y5DOS6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BOZZPUXE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-CIRHIEMW.mjs.map +0 -7
- package/dist/lib/neutral/chunk-DYDJBZEG.mjs.map +0 -7
- package/dist/lib/neutral/chunk-KTAWP7GI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NEGC54NE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NQRLRYJM.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NXMFBIT5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-OENWMTE6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-SEMVAGBM.mjs.map +0 -7
- package/dist/lib/neutral/chunk-TQQZLKB7.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UFDK26FO.mjs.map +0 -7
- package/dist/lib/neutral/chunk-VA2F47ZC.mjs.map +0 -7
- /package/dist/lib/neutral/{chunk-FZO7LQO7.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-724GQYEN.mjs.map → chunk-HPNQTEEQ.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-UTBRYVQC.mjs.map → chunk-QGMIH2SN.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-B5OXLWZL.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-OMUPQMLR.mjs.map → chunk-V72DY6LU.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-FXEG7EOK.mjs.map → chunk-VYAGNFSJ.mjs.map} +0 -0
package/src/Feed.ts
CHANGED
|
@@ -10,8 +10,9 @@ 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
|
+
import * as Annotation from './Annotation';
|
|
15
16
|
import type * as Entity from './Entity';
|
|
16
17
|
import type * as Filter from './Filter';
|
|
17
18
|
import * as internal from './internal';
|
|
@@ -33,12 +34,24 @@ export const Feed = Schema.Struct({
|
|
|
33
34
|
name: Schema.String.pipe(Schema.optional),
|
|
34
35
|
/** Identifier for the feed's kind (e.g., plugin id). */
|
|
35
36
|
kind: Schema.String.pipe(internal.FormInputAnnotation.set(false), Schema.optional),
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Feed namespace.
|
|
40
|
+
* Controls how feed data is stored and replicated.
|
|
41
|
+
* - `data`: Data feed (default).
|
|
42
|
+
* - `trace`: Trace feed.
|
|
43
|
+
*/
|
|
44
|
+
namespace: Schema.optional(Schema.Literal('data', 'trace')),
|
|
36
45
|
}).pipe(
|
|
37
46
|
Type.object({
|
|
38
47
|
typename: 'org.dxos.type.feed',
|
|
39
48
|
version: '0.1.0',
|
|
40
49
|
}),
|
|
41
50
|
internal.SystemTypeAnnotation.set(true),
|
|
51
|
+
Annotation.IconAnnotation.set({
|
|
52
|
+
icon: 'ph--rows--regular',
|
|
53
|
+
hue: 'yellow',
|
|
54
|
+
}),
|
|
42
55
|
);
|
|
43
56
|
|
|
44
57
|
/**
|
|
@@ -50,12 +63,6 @@ export interface Feed extends Schema.Schema.Type<typeof Feed> {}
|
|
|
50
63
|
// Types
|
|
51
64
|
//
|
|
52
65
|
|
|
53
|
-
/**
|
|
54
|
-
* Meta key source for storing the backing DXN bound to a feed object.
|
|
55
|
-
*/
|
|
56
|
-
// TODO(dmaretskyi): Enforce that Feed ObjectId = feed storage ID. And remove this key.
|
|
57
|
-
export const DXN_KEY = 'org.dxos.key.feed';
|
|
58
|
-
|
|
59
66
|
/**
|
|
60
67
|
* Opaque cursor for iterating over feed items.
|
|
61
68
|
*/
|
|
@@ -89,14 +96,38 @@ export interface RetentionOptions {
|
|
|
89
96
|
export const make = (props: Obj.MakeProps<typeof Feed> = {}): Feed => Obj.make(Feed, props);
|
|
90
97
|
|
|
91
98
|
/**
|
|
92
|
-
*
|
|
99
|
+
* Derives the queue DXN from the feed object's DXN.
|
|
100
|
+
* Returns `undefined` when the feed is not stored in a space yet.
|
|
93
101
|
*
|
|
94
|
-
*
|
|
102
|
+
* Used internally by the feed service layer.
|
|
95
103
|
*/
|
|
96
|
-
// TODO(wittjosiah): Align backing feed dxn's with object DXN.
|
|
97
104
|
export const getQueueDxn = (feed: Feed): DXN | undefined => {
|
|
98
|
-
const
|
|
99
|
-
|
|
105
|
+
const self = Obj.getDXN(feed).asEchoDXN();
|
|
106
|
+
if (!self || !self.spaceId) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return new DXN(DXN.kind.QUEUE, [feed.namespace ?? 'data', self.spaceId, self.echoId]);
|
|
110
|
+
};
|
|
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
|
+
});
|
|
100
131
|
};
|
|
101
132
|
|
|
102
133
|
//
|
|
@@ -108,8 +139,8 @@ export const getQueueDxn = (feed: Feed): DXN | undefined => {
|
|
|
108
139
|
* Provides the bridge to the underlying storage implementation.
|
|
109
140
|
* Must be provided by the application layer (e.g., echo-db).
|
|
110
141
|
*/
|
|
111
|
-
export class
|
|
112
|
-
|
|
142
|
+
export class FeedService extends Context.Tag('@dxos/echo/Feed/FeedService')<
|
|
143
|
+
FeedService,
|
|
113
144
|
{
|
|
114
145
|
/**
|
|
115
146
|
* Appends items to a feed.
|
|
@@ -132,21 +163,31 @@ export class Service extends Context.Tag('@dxos/echo/Feed/Service')<
|
|
|
132
163
|
}
|
|
133
164
|
>() {}
|
|
134
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @deprecated Use `FeedService` instead.
|
|
168
|
+
*/
|
|
169
|
+
export type Service = FeedService;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @deprecated Use `FeedService` instead.
|
|
173
|
+
*/
|
|
174
|
+
export const Service = FeedService;
|
|
175
|
+
|
|
135
176
|
/**
|
|
136
177
|
* Layer that provides a Feed service that throws when accessed.
|
|
137
178
|
* Useful as a default layer when no feed service is available.
|
|
138
179
|
*/
|
|
139
|
-
export const notAvailable: Layer.Layer<
|
|
180
|
+
export const notAvailable: Layer.Layer<FeedService> = Layer.succeed(FeedService, {
|
|
140
181
|
append: () => {
|
|
141
|
-
throw new Error('Feed.
|
|
182
|
+
throw new Error('Feed.FeedService not available');
|
|
142
183
|
},
|
|
143
184
|
remove: () => {
|
|
144
|
-
throw new Error('Feed.
|
|
185
|
+
throw new Error('Feed.FeedService not available');
|
|
145
186
|
},
|
|
146
187
|
query: () => {
|
|
147
|
-
throw new Error('Feed.
|
|
188
|
+
throw new Error('Feed.FeedService not available');
|
|
148
189
|
},
|
|
149
|
-
} as Context.Tag.Service<
|
|
190
|
+
} as Context.Tag.Service<FeedService>);
|
|
150
191
|
|
|
151
192
|
//
|
|
152
193
|
// Operations
|
|
@@ -160,9 +201,9 @@ export const notAvailable: Layer.Layer<Service> = Layer.succeed(Service, {
|
|
|
160
201
|
* yield* Feed.append(feed, [Obj.make(Notification, { title: 'Hello' })]);
|
|
161
202
|
* ```
|
|
162
203
|
*/
|
|
163
|
-
export const append = (feed: Feed, items: Entity.Unknown[]): Effect.Effect<void, never,
|
|
204
|
+
export const append = (feed: Feed, items: Entity.Unknown[]): Effect.Effect<void, never, FeedService> =>
|
|
164
205
|
Effect.gen(function* () {
|
|
165
|
-
const service = yield*
|
|
206
|
+
const service = yield* FeedService;
|
|
166
207
|
yield* Effect.promise(() => service.append(feed, items));
|
|
167
208
|
});
|
|
168
209
|
|
|
@@ -175,9 +216,9 @@ export const append = (feed: Feed, items: Entity.Unknown[]): Effect.Effect<void,
|
|
|
175
216
|
* ```
|
|
176
217
|
*/
|
|
177
218
|
// TODO(dmaretskyi): Should we allow snapshots here? - what does it mean to remove a snapshot?
|
|
178
|
-
export const remove = (feed: Feed, items: (Entity.Unknown | Obj.Snapshot)[]): Effect.Effect<void, never,
|
|
219
|
+
export const remove = (feed: Feed, items: (Entity.Unknown | Obj.Snapshot)[]): Effect.Effect<void, never, FeedService> =>
|
|
179
220
|
Effect.gen(function* () {
|
|
180
|
-
const service = yield*
|
|
221
|
+
const service = yield* FeedService;
|
|
181
222
|
const ids = items.map((item) => item.id);
|
|
182
223
|
yield* Effect.promise(() => service.remove(feed, ids));
|
|
183
224
|
});
|
|
@@ -196,10 +237,16 @@ export const remove = (feed: Feed, items: (Entity.Unknown | Obj.Snapshot)[]): Ef
|
|
|
196
237
|
// const object = yield* feed.pipe(Feed.query(Filter.type(Person))).first;
|
|
197
238
|
// ... unify for Database and schema queries.
|
|
198
239
|
export const query: {
|
|
199
|
-
<Q extends Query.Any>(
|
|
200
|
-
|
|
240
|
+
<Q extends Query.Any>(
|
|
241
|
+
feed: Feed,
|
|
242
|
+
query: Q,
|
|
243
|
+
): Effect.Effect<QueryResult.QueryResult<Query.Type<Q>>, never, FeedService>;
|
|
244
|
+
<F extends Filter.Any>(
|
|
245
|
+
feed: Feed,
|
|
246
|
+
filter: F,
|
|
247
|
+
): Effect.Effect<QueryResult.QueryResult<Filter.Type<F>>, never, FeedService>;
|
|
201
248
|
} = (feed: Feed, queryOrFilter: Query.Any | Filter.Any) =>
|
|
202
|
-
|
|
249
|
+
FeedService.pipe(Effect.map((service) => service.query(feed, queryOrFilter as any) as QueryResult.QueryResult<any>));
|
|
203
250
|
|
|
204
251
|
/**
|
|
205
252
|
* Executes a feed query once and returns the results.
|
|
@@ -210,8 +257,8 @@ export const query: {
|
|
|
210
257
|
* ```
|
|
211
258
|
*/
|
|
212
259
|
export const runQuery: {
|
|
213
|
-
<Q extends Query.Any>(feed: Feed, query: Q): Effect.Effect<Query.Type<Q>[], never,
|
|
214
|
-
<F extends Filter.Any>(feed: Feed, filter: F): Effect.Effect<Filter.Type<F>[], never,
|
|
260
|
+
<Q extends Query.Any>(feed: Feed, query: Q): Effect.Effect<Query.Type<Q>[], never, FeedService>;
|
|
261
|
+
<F extends Filter.Any>(feed: Feed, filter: F): Effect.Effect<Filter.Type<F>[], never, FeedService>;
|
|
215
262
|
} = (feed: Feed, queryOrFilter: Query.Any | Filter.Any) =>
|
|
216
263
|
query(feed, queryOrFilter as any).pipe(Effect.flatMap((queryResult) => Effect.promise(() => queryResult.run())));
|
|
217
264
|
|
|
@@ -226,21 +273,21 @@ export const runQuery: {
|
|
|
226
273
|
* ```
|
|
227
274
|
*/
|
|
228
275
|
// TODO(wittjosiah): Implement cursor operations. Use Effect streams?
|
|
229
|
-
export const cursor = <T = Obj.Snapshot>(_feed: Feed): Effect.Effect<Cursor<T>, never,
|
|
276
|
+
export const cursor = <T = Obj.Snapshot>(_feed: Feed): Effect.Effect<Cursor<T>, never, FeedService> =>
|
|
230
277
|
Effect.succeed({ _tag: 'Cursor' } as Cursor<T>);
|
|
231
278
|
|
|
232
279
|
/**
|
|
233
280
|
* Returns the next item from a feed cursor.
|
|
234
281
|
* Currently stubbed — cursor operations are not yet implemented.
|
|
235
282
|
*/
|
|
236
|
-
export const next = <T = Obj.Snapshot>(_cursor: Cursor<T>): Effect.Effect<T, never,
|
|
283
|
+
export const next = <T = Obj.Snapshot>(_cursor: Cursor<T>): Effect.Effect<T, never, FeedService> =>
|
|
237
284
|
Effect.die('Feed.next is not yet implemented');
|
|
238
285
|
|
|
239
286
|
/**
|
|
240
287
|
* Returns the next item from a feed cursor as an Option.
|
|
241
288
|
* Currently stubbed — cursor operations are not yet implemented.
|
|
242
289
|
*/
|
|
243
|
-
export const nextOption = <T = Obj.Snapshot>(_cursor: Cursor<T>): Effect.Effect<Option.Option<T>, never,
|
|
290
|
+
export const nextOption = <T = Obj.Snapshot>(_cursor: Cursor<T>): Effect.Effect<Option.Option<T>, never, FeedService> =>
|
|
244
291
|
Effect.die('Feed.nextOption is not yet implemented');
|
|
245
292
|
|
|
246
293
|
/**
|
|
@@ -253,5 +300,5 @@ export const nextOption = <T = Obj.Snapshot>(_cursor: Cursor<T>): Effect.Effect<
|
|
|
253
300
|
* ```
|
|
254
301
|
*/
|
|
255
302
|
// TODO(feed): Implement when queue retention is supported.
|
|
256
|
-
export const setRetention = (_feed: Feed, _options: RetentionOptions): Effect.Effect<void, never,
|
|
303
|
+
export const setRetention = (_feed: Feed, _options: RetentionOptions): Effect.Effect<void, never, FeedService> =>
|
|
257
304
|
Effect.void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import * as Filter from './Filter';
|
|
8
|
+
|
|
9
|
+
describe('Filter timestamp builders', () => {
|
|
10
|
+
test('updated({ after }) produces correct AST', () => {
|
|
11
|
+
const ts = 1700000000000;
|
|
12
|
+
const f = Filter.updated({ after: ts });
|
|
13
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'updatedAt', operator: 'gte', value: ts });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('updated({ before }) produces correct AST', () => {
|
|
17
|
+
const ts = 1700000000000;
|
|
18
|
+
const f = Filter.updated({ before: ts });
|
|
19
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'updatedAt', operator: 'lte', value: ts });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('created({ after }) produces correct AST', () => {
|
|
23
|
+
const ts = 1700000000000;
|
|
24
|
+
const f = Filter.created({ after: ts });
|
|
25
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'createdAt', operator: 'gte', value: ts });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('created({ before }) produces correct AST', () => {
|
|
29
|
+
const ts = 1700000000000;
|
|
30
|
+
const f = Filter.created({ before: ts });
|
|
31
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'createdAt', operator: 'lte', value: ts });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('updated() accepts Date objects', () => {
|
|
35
|
+
const date = new Date('2026-03-20T21:00:00Z');
|
|
36
|
+
const f = Filter.updated({ after: date });
|
|
37
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'updatedAt', operator: 'gte', value: date.getTime() });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('created() accepts Date objects', () => {
|
|
41
|
+
const date = new Date('2026-03-20T09:00:00Z');
|
|
42
|
+
const f = Filter.created({ before: date });
|
|
43
|
+
expect(f.ast).toEqual({ type: 'timestamp', field: 'createdAt', operator: 'lte', value: date.getTime() });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('updated({ after, before }) produces AND of two timestamp nodes', () => {
|
|
47
|
+
const from = 1700000000000;
|
|
48
|
+
const to = 1700086400000;
|
|
49
|
+
const f = Filter.updated({ after: from, before: to });
|
|
50
|
+
expect(f.ast).toEqual({
|
|
51
|
+
type: 'and',
|
|
52
|
+
filters: [
|
|
53
|
+
{ type: 'timestamp', field: 'updatedAt', operator: 'gte', value: from },
|
|
54
|
+
{ type: 'timestamp', field: 'updatedAt', operator: 'lte', value: to },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('updated({ after, before }) accepts Date objects', () => {
|
|
60
|
+
const from = new Date('2026-03-19T00:00:00Z');
|
|
61
|
+
const to = new Date('2026-03-20T00:00:00Z');
|
|
62
|
+
const f = Filter.updated({ after: from, before: to });
|
|
63
|
+
expect(f.ast).toEqual({
|
|
64
|
+
type: 'and',
|
|
65
|
+
filters: [
|
|
66
|
+
{ type: 'timestamp', field: 'updatedAt', operator: 'gte', value: from.getTime() },
|
|
67
|
+
{ type: 'timestamp', field: 'updatedAt', operator: 'lte', value: to.getTime() },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('timestamp filters compose with and()', () => {
|
|
73
|
+
const typeFilter = Filter.everything();
|
|
74
|
+
const timeFilter = Filter.updated({ after: 1700000000000 });
|
|
75
|
+
const combined = Filter.and(typeFilter, timeFilter);
|
|
76
|
+
expect(combined.ast.type).toBe('and');
|
|
77
|
+
expect((combined.ast as any).filters).toHaveLength(2);
|
|
78
|
+
expect((combined.ast as any).filters[1]).toEqual({
|
|
79
|
+
type: 'timestamp',
|
|
80
|
+
field: 'updatedAt',
|
|
81
|
+
operator: 'gte',
|
|
82
|
+
value: 1700000000000,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('timestamp filters pass the is() check', () => {
|
|
87
|
+
const f = Filter.updated({ after: Date.now() });
|
|
88
|
+
expect(Filter.is(f)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/Filter.ts
CHANGED
|
@@ -14,11 +14,12 @@ import { assertArgument } from '@dxos/invariant';
|
|
|
14
14
|
import { DXN, ObjectId } from '@dxos/keys';
|
|
15
15
|
|
|
16
16
|
import * as internal from './internal';
|
|
17
|
+
import type * as Obj from './Obj';
|
|
17
18
|
import * as Ref from './Ref';
|
|
18
19
|
|
|
19
20
|
export interface Filter<T> {
|
|
20
21
|
// TODO(dmaretskyi): See new effect-schema approach to variance.
|
|
21
|
-
'~Filter': { value: Types.
|
|
22
|
+
'~Filter': { value: Types.Covariant<T> };
|
|
22
23
|
|
|
23
24
|
ast: QueryAST.Filter;
|
|
24
25
|
}
|
|
@@ -278,7 +279,7 @@ export const lte = <T>(value: T): Filter<T | undefined> => {
|
|
|
278
279
|
* Predicate for property to be in the provided array.
|
|
279
280
|
* @param values - Values to check against.
|
|
280
281
|
*/
|
|
281
|
-
const in$ = <T>(...values: T[]): Filter<T
|
|
282
|
+
const in$ = <T>(...values: T[]): Filter<T> => {
|
|
282
283
|
return new FilterClass({
|
|
283
284
|
type: 'in',
|
|
284
285
|
values,
|
|
@@ -302,7 +303,7 @@ export const contains = <T>(value: T): Filter<readonly T[] | undefined> => {
|
|
|
302
303
|
* @param from - Start of the range (inclusive).
|
|
303
304
|
* @param to - End of the range (exclusive).
|
|
304
305
|
*/
|
|
305
|
-
export const between = <T>(from: T, to: T): Filter<
|
|
306
|
+
export const between = <T>(from: T, to: T): Filter<T> => {
|
|
306
307
|
return new FilterClass({
|
|
307
308
|
type: 'range',
|
|
308
309
|
from,
|
|
@@ -310,6 +311,63 @@ export const between = <T>(from: T, to: T): Filter<unknown> => {
|
|
|
310
311
|
});
|
|
311
312
|
};
|
|
312
313
|
|
|
314
|
+
type TimeRange = { after?: Date | number; before?: Date | number };
|
|
315
|
+
|
|
316
|
+
const _toUnixMs = (date: Date | number): number => (typeof date === 'number' ? date : date.getTime());
|
|
317
|
+
|
|
318
|
+
const _timeRangeFilter = (field: 'updatedAt' | 'createdAt', range: TimeRange): Any => {
|
|
319
|
+
const filters: Any[] = [];
|
|
320
|
+
if (range.after != null) {
|
|
321
|
+
filters.push(new FilterClass({ type: 'timestamp', field, operator: 'gte', value: _toUnixMs(range.after) }));
|
|
322
|
+
}
|
|
323
|
+
if (range.before != null) {
|
|
324
|
+
filters.push(new FilterClass({ type: 'timestamp', field, operator: 'lte', value: _toUnixMs(range.before) }));
|
|
325
|
+
}
|
|
326
|
+
if (filters.length === 0) {
|
|
327
|
+
return everything();
|
|
328
|
+
}
|
|
329
|
+
return filters.length === 1 ? filters[0] : and(...filters);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Filter objects by updatedAt timestamp.
|
|
334
|
+
*/
|
|
335
|
+
export const updated = (range: TimeRange): Any => _timeRangeFilter('updatedAt', range);
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Filter objects by createdAt timestamp.
|
|
339
|
+
*/
|
|
340
|
+
export const created = (range: TimeRange): Any => _timeRangeFilter('createdAt', range);
|
|
341
|
+
|
|
342
|
+
export type ChildOfOptions = {
|
|
343
|
+
/** Whether to match transitively (grandchildren, etc.). Defaults to true. */
|
|
344
|
+
transitive?: boolean;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Filter objects that are children of the specified parent(s).
|
|
349
|
+
* Accepts ECHO objects, Refs, or arrays of either.
|
|
350
|
+
* Refs are resolved to DXNs without loading; objects use {@link Obj.getDXN}.
|
|
351
|
+
* With transitive=true (default), also matches grandchildren and beyond.
|
|
352
|
+
*/
|
|
353
|
+
export const childOf = (
|
|
354
|
+
parents: Obj.Unknown | Ref.Unknown | readonly (Obj.Unknown | Ref.Unknown)[],
|
|
355
|
+
options?: ChildOfOptions,
|
|
356
|
+
): Any => {
|
|
357
|
+
const items = Array.isArray(parents) ? parents : [parents];
|
|
358
|
+
const dxns = items.map((item) => {
|
|
359
|
+
if (Ref.isRef(item)) {
|
|
360
|
+
return item.dxn.toString();
|
|
361
|
+
}
|
|
362
|
+
return internal.getDXN(item).toString();
|
|
363
|
+
});
|
|
364
|
+
return new FilterClass({
|
|
365
|
+
type: 'child-of',
|
|
366
|
+
parents: dxns,
|
|
367
|
+
transitive: options?.transitive ?? true,
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
313
371
|
/**
|
|
314
372
|
* Negate the filter.
|
|
315
373
|
*/
|
|
@@ -389,3 +447,8 @@ const processPredicate = (predicate: any): QueryAST.Filter => {
|
|
|
389
447
|
Match.orElse((value) => eq(value).ast),
|
|
390
448
|
);
|
|
391
449
|
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Returns a human-readable string representation of a Filter AST.
|
|
453
|
+
*/
|
|
454
|
+
export const pretty = (filter: Any): string => internal.prettyFilter(filter.ast);
|
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
|
+
});
|