@dxos/echo-protocol 0.8.4-main.ef1bc66f44 → 0.8.4-main.effb148878

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-protocol",
3
- "version": "0.8.4-main.ef1bc66f44",
3
+ "version": "0.8.4-main.effb148878",
4
4
  "description": "Core ECHO APIs.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "DXOS.org",
13
13
  "sideEffects": false,
14
14
  "type": "module",
@@ -20,20 +20,17 @@
20
20
  }
21
21
  },
22
22
  "types": "dist/types/src/index.d.ts",
23
- "typesVersions": {
24
- "*": {}
25
- },
26
23
  "files": [
27
24
  "dist",
28
25
  "src"
29
26
  ],
30
27
  "dependencies": {
31
- "effect": "3.19.16",
32
- "@dxos/crypto": "0.8.4-main.ef1bc66f44",
33
- "@dxos/invariant": "0.8.4-main.ef1bc66f44",
34
- "@dxos/keys": "0.8.4-main.ef1bc66f44",
35
- "@dxos/util": "0.8.4-main.ef1bc66f44",
36
- "@dxos/protocols": "0.8.4-main.ef1bc66f44"
28
+ "effect": "3.20.0",
29
+ "@dxos/invariant": "0.8.4-main.effb148878",
30
+ "@dxos/util": "0.8.4-main.effb148878",
31
+ "@dxos/protocols": "0.8.4-main.effb148878",
32
+ "@dxos/crypto": "0.8.4-main.effb148878",
33
+ "@dxos/keys": "0.8.4-main.effb148878"
37
34
  },
38
35
  "publishConfig": {
39
36
  "access": "public"
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { invariant } from '@dxos/invariant';
6
- import type { DXN, ObjectId } from '@dxos/keys';
6
+ import type { ObjectId, URI } from '@dxos/keys';
7
7
  import { visitValues } from '@dxos/util';
8
8
 
9
9
  import { type RawString } from './automerge';
@@ -41,7 +41,7 @@ export interface DatabaseDirectory {
41
41
  * Object id points to an automerge doc url where the object is embedded.
42
42
  */
43
43
  links?: {
44
- [echoId: string]: string | RawString;
44
+ [echoUri: string]: string | RawString;
45
45
  };
46
46
 
47
47
  /**
@@ -119,9 +119,9 @@ export const ObjectStructure = Object.freeze({
119
119
  /**
120
120
  * @throws On invalid object structure.
121
121
  */
122
- getEntityKind: (object: ObjectStructure): 'object' | 'relation' => {
122
+ getEntityKind: (object: ObjectStructure): 'object' | 'relation' | 'type' => {
123
123
  const kind = object.system?.kind ?? 'object';
124
- invariant(kind === 'object' || kind === 'relation', 'Invalid kind');
124
+ invariant(kind === 'object' || kind === 'relation' || kind === 'type', 'Invalid kind');
125
125
  return kind;
126
126
  },
127
127
 
@@ -166,7 +166,7 @@ export const ObjectStructure = Object.freeze({
166
166
  data,
167
167
  keys,
168
168
  }: {
169
- type: DXN.String;
169
+ type: URI.URI;
170
170
  deleted?: boolean;
171
171
  keys?: ForeignKey[];
172
172
  data?: unknown;
@@ -191,7 +191,7 @@ export const ObjectStructure = Object.freeze({
191
191
  keys,
192
192
  data,
193
193
  }: {
194
- type: DXN.String;
194
+ type: URI.URI;
195
195
  source: EncodedReference;
196
196
  target: EncodedReference;
197
197
  deleted?: boolean;
@@ -212,6 +212,19 @@ export const ObjectStructure = Object.freeze({
212
212
  data: data ?? {},
213
213
  };
214
214
  },
215
+
216
+ makeType: ({ type, keys, data }: { type: URI.URI; keys?: ForeignKey[]; data?: unknown }): ObjectStructure => {
217
+ return {
218
+ system: {
219
+ kind: 'type',
220
+ type: { '/': type },
221
+ },
222
+ meta: {
223
+ keys: keys ?? [],
224
+ },
225
+ data: data ?? {},
226
+ };
227
+ },
215
228
  });
216
229
 
217
230
  /**
@@ -230,6 +243,18 @@ export type ObjectMeta = {
230
243
  * NOTE: Optional for backwards compatibilty.
231
244
  */
232
245
  tags?: string[];
246
+
247
+ /**
248
+ * Fully-qualified registry key for the object (FQN format, e.g. `org.example.type.foo`).
249
+ * Identifies the canonical registry entry the object instance was created from.
250
+ */
251
+ key?: string;
252
+
253
+ /**
254
+ * Semantic version of the registry entry the object was created from.
255
+ * Must be a valid semver string (e.g. `1.2.3`).
256
+ */
257
+ version?: string;
233
258
  };
234
259
 
235
260
  /**
@@ -238,12 +263,22 @@ export type ObjectMeta = {
238
263
  */
239
264
  export type ObjectSystem = {
240
265
  /**
241
- * Entity kind.
266
+ * Entity kind. `'type'` covers persisted ECHO type definitions (instances of
267
+ * the `Type.Type` meta-schema); `'object'` / `'relation'` cover regular ECHO
268
+ * instances.
242
269
  */
243
- kind?: 'object' | 'relation';
270
+ kind?: 'object' | 'relation' | 'type';
244
271
 
245
272
  /**
246
- * Object reference ('protobuf' protocol) type.
273
+ * Object reference ('protobuf' protocol) type — DXN of the schema this
274
+ * entity instantiates.
275
+ *
276
+ * - For `kind === 'object'` / `'relation'` instances, this is the URI of the
277
+ * user-defined schema the entity was created from (e.g. `dxn:type:org.example.Person:1.0.0`).
278
+ * - For `kind === 'type'` entities (persisted Type.Type meta-instances) this
279
+ * is always the URI of the `TypeSchema` meta-schema itself
280
+ * (`dxn:org.dxos.type.schema:0.1.0`). The kind that the meta-instance
281
+ * _describes_ (object/relation/type) lives in `data.jsonSchema.entityKind`.
247
282
  */
248
283
  type?: EncodedReference;
249
284
 
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { FeedProtocol } from '@dxos/protocols';
6
+
7
+ import type { ForeignKey } from './foreign-key';
8
+
9
+ /** Property name for meta when object is serialized to JSON. Matches @dxos/echo/internal ATTR_META. */
10
+ const ATTR_META = '@meta';
11
+
12
+ /**
13
+ * Codec for ECHO objects in feed block payload: JSON object ↔ UTF-8 bytes.
14
+ * Encodes with queue position stripped; decodes with optional position injection.
15
+ */
16
+ export class EchoFeedCodec {
17
+ static readonly #encoder = new TextEncoder();
18
+ static readonly #decoder = new TextDecoder();
19
+
20
+ /**
21
+ * Prepares a value for feed storage (strips queue position from metadata) and encodes to bytes.
22
+ */
23
+ static encode(value: Record<string, unknown>): Uint8Array {
24
+ const prepared = EchoFeedCodec.#stripQueuePosition(value);
25
+ return EchoFeedCodec.#encoder.encode(JSON.stringify(prepared));
26
+ }
27
+
28
+ /**
29
+ * Decodes feed block bytes to a JSON value.
30
+ * If position is provided, injects queue position into the decoded object's metadata.
31
+ */
32
+ static decode(data: Uint8Array, position?: number): Record<string, unknown> {
33
+ const decoded = JSON.parse(EchoFeedCodec.#decoder.decode(data));
34
+ if (position !== undefined && typeof decoded === 'object' && decoded !== null) {
35
+ EchoFeedCodec.#setQueuePosition(decoded, position);
36
+ }
37
+ return decoded;
38
+ }
39
+
40
+ static #stripQueuePosition(value: Record<string, unknown>): Record<string, unknown> {
41
+ if (typeof value !== 'object' || value === null) {
42
+ return value;
43
+ }
44
+ const obj = structuredClone(value);
45
+ const meta = obj[ATTR_META] as { keys?: ForeignKey[] } | undefined;
46
+ if (meta?.keys?.some((key: ForeignKey) => key.source === FeedProtocol.KEY_QUEUE_POSITION)) {
47
+ meta.keys = meta.keys.filter((key: ForeignKey) => key.source !== FeedProtocol.KEY_QUEUE_POSITION);
48
+ }
49
+ return obj;
50
+ }
51
+
52
+ static #setQueuePosition(obj: Record<string, any>, position: number): void {
53
+ obj[ATTR_META] ??= { keys: [] };
54
+ obj[ATTR_META]!.keys ??= [];
55
+ const keys = obj[ATTR_META]!.keys!;
56
+ for (let i = 0; i < keys.length; i++) {
57
+ if (keys[i].source === FeedProtocol.KEY_QUEUE_POSITION) {
58
+ keys.splice(i, 1);
59
+ i--;
60
+ }
61
+ }
62
+ keys.push({
63
+ source: FeedProtocol.KEY_QUEUE_POSITION,
64
+ id: position.toString(),
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type SpaceId } from '@dxos/keys';
6
+ import { EdgeService } from '@dxos/protocols';
7
+ import { compositeKey } from '@dxos/util';
8
+
9
+ /**
10
+ * Returns true if the given peerId belongs to an EDGE replicator (Automerge or Subduction).
11
+ *
12
+ * When `spaceId` is provided, the match is scoped to that space (peerId must start with
13
+ * `<service>:<spaceId>`). When omitted, only the leading service segment is checked
14
+ * (peerId must start with `<service>:`), which is useful when the caller doesn't have a
15
+ * spaceId on hand or wants to match any edge replicator regardless of space.
16
+ */
17
+ export const isEdgePeerId = (peerId: string, spaceId?: SpaceId): boolean => {
18
+ const automergePrefix =
19
+ spaceId !== undefined
20
+ ? compositeKey(EdgeService.AUTOMERGE_REPLICATOR, spaceId)
21
+ : `${EdgeService.AUTOMERGE_REPLICATOR}:`;
22
+ const subductionPrefix =
23
+ spaceId !== undefined
24
+ ? compositeKey(EdgeService.SUBDUCTION_REPLICATOR, spaceId)
25
+ : `${EdgeService.SUBDUCTION_REPLICATOR}:`;
26
+ return peerId.startsWith(automergePrefix) || peerId.startsWith(subductionPrefix);
27
+ };
package/src/index.ts CHANGED
@@ -2,10 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export type * from './collection-sync';
5
6
  export * from './document-structure';
7
+ export * from './edge-peer';
8
+ export * from './echo-feed-codec';
9
+ export * from './foreign-key';
10
+ export * from './query';
6
11
  export * from './reference';
7
12
  export * from './space-doc-version';
8
- export type * from './collection-sync';
9
13
  export * from './space-id';
10
- export * from './foreign-key';
11
- export * from './query';
package/src/query/ast.ts CHANGED
@@ -5,13 +5,14 @@
5
5
  import * as Match from 'effect/Match';
6
6
  import * as Schema from 'effect/Schema';
7
7
 
8
- import { DXN, ObjectId } from '@dxos/keys';
8
+ import { EchoURI, ObjectId, URI } from '@dxos/keys';
9
9
 
10
10
  import { ForeignKey } from '../foreign-key';
11
11
 
12
- const TypenameSpecifier = Schema.Union(DXN.Schema, Schema.Null).annotations({
13
- description: 'DXN or null; null matches any type',
14
- });
12
+ // Type identifier URI — either a DXN (typename) or an EchoURI (stored-schema-as-object).
13
+ // Matches the URI written into an object's `system.type` (see `getSchemaURI`). Null
14
+ // matches any type.
15
+ const TypenameSpecifier = Schema.Union(URI.Schema, Schema.Null);
15
16
 
16
17
  // NOTE: This pattern with 3 definitions per schema is need to make the types opaque, and circular references in AST to not cause compiler errors.
17
18
 
@@ -42,6 +43,17 @@ const FilterObject_ = Schema.Struct({
42
43
  */
43
44
  foreignKeys: Schema.optional(Schema.Array(ForeignKey)),
44
45
 
46
+ /**
47
+ * Match objects whose meta `key` equals this fully-qualified registry key (FQN format).
48
+ */
49
+ metaKey: Schema.optional(Schema.String),
50
+
51
+ /**
52
+ * Semver range matched against the object's meta `version`.
53
+ * Only consulted when {@link metaKey} is set. Objects with no `version` do not satisfy a version-constrained filter.
54
+ */
55
+ metaVersion: Schema.optional(Schema.String),
56
+
45
57
  // NOTE: Make sure to update `FilterStep.isNoop` if you change this.
46
58
  });
47
59
  export interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
@@ -107,6 +119,20 @@ const FilterRange_ = Schema.Struct({
107
119
  export interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
108
120
  export const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
109
121
 
122
+ /**
123
+ * Filter by system timestamp (createdAt / updatedAt).
124
+ * Timestamps are unix milliseconds stored in the object meta index.
125
+ */
126
+ const FilterTimestamp_ = Schema.Struct({
127
+ type: Schema.Literal('timestamp'),
128
+ field: Schema.Literal('createdAt', 'updatedAt'),
129
+ operator: Schema.Literal('gt', 'gte', 'lt', 'lte'),
130
+ value: Schema.Number,
131
+ });
132
+
133
+ export interface FilterTimestamp extends Schema.Schema.Type<typeof FilterTimestamp_> {}
134
+ export const FilterTimestamp: Schema.Schema<FilterTimestamp> = FilterTimestamp_;
135
+
110
136
  /**
111
137
  * Text search.
112
138
  */
@@ -152,6 +178,21 @@ const FilterOr_ = Schema.Struct({
152
178
  export interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
153
179
  export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
154
180
 
181
+ /**
182
+ * Filter objects that are children of the specified parents.
183
+ * With transitive=true (default), matches grandchildren and beyond.
184
+ */
185
+ const FilterChildOf_ = Schema.Struct({
186
+ type: Schema.Literal('child-of'),
187
+ /** Parent DXNs to match children of. */
188
+ parents: Schema.Array(EchoURI.Schema),
189
+ /** Whether to match transitively (grandchildren, etc.). Defaults to true. */
190
+ transitive: Schema.Boolean,
191
+ });
192
+
193
+ export interface FilterChildOf extends Schema.Schema.Type<typeof FilterChildOf_> {}
194
+ export const FilterChildOf: Schema.Schema<FilterChildOf> = FilterChildOf_;
195
+
155
196
  /**
156
197
  * Union of filters.
157
198
  */
@@ -162,11 +203,13 @@ export const Filter = Schema.Union(
162
203
  FilterContains,
163
204
  FilterTag,
164
205
  FilterRange,
206
+ FilterTimestamp,
165
207
  FilterTextSearch,
208
+ FilterChildOf,
166
209
  FilterNot,
167
210
  FilterAnd,
168
211
  FilterOr,
169
- ).annotations({ identifier: 'dxos.org/schema/Filter' });
212
+ ).annotations({ identifier: 'org.dxos.schema.filter' });
170
213
 
171
214
  export type Filter = Schema.Schema.Type<typeof Filter>;
172
215
 
@@ -355,6 +398,21 @@ const QueryLimitClause_ = Schema.Struct({
355
398
  export interface QueryLimitClause extends Schema.Schema.Type<typeof QueryLimitClause_> {}
356
399
  export const QueryLimitClause: Schema.Schema<QueryLimitClause> = QueryLimitClause_;
357
400
 
401
+ export const QueryFromClause_ = Schema.Struct({
402
+ type: Schema.Literal('from'),
403
+ query: Schema.suspend(() => Query),
404
+ from: Schema.Union(
405
+ Schema.TaggedStruct('scope', {
406
+ scope: Schema.suspend(() => Scope),
407
+ }),
408
+ Schema.TaggedStruct('query', {
409
+ query: Schema.suspend(() => Query),
410
+ }),
411
+ ),
412
+ });
413
+ export interface QueryFromClause extends Schema.Schema.Type<typeof QueryFromClause_> {}
414
+ export const QueryFromClause: Schema.Schema<QueryFromClause> = QueryFromClause_;
415
+
358
416
  const Query_ = Schema.Union(
359
417
  QuerySelectClause,
360
418
  QueryFilterClause,
@@ -368,38 +426,50 @@ const Query_ = Schema.Union(
368
426
  QueryOrderClause,
369
427
  QueryOptionsClause,
370
428
  QueryLimitClause,
371
- ).annotations({ identifier: 'dxos.org/schema/Query' });
429
+ QueryFromClause,
430
+ ).annotations({ identifier: 'org.dxos.schema.query' });
372
431
 
373
432
  export type Query = Schema.Schema.Type<typeof Query_>;
374
433
  export const Query: Schema.Schema<Query> = Query_;
375
434
 
376
435
  export const QueryOptions = Schema.Struct({
377
436
  /**
378
- * The nested select statemets will select from the given spaces.
379
- *
380
- * NOTE: Spaces and queues are unioned together if both are specified.
437
+ * Nested select statements will use this option to filter deleted objects.
381
438
  */
382
- spaceIds: Schema.optional(Schema.Array(Schema.String)),
439
+ deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
383
440
 
384
441
  /**
385
- * If true, the nested select statements will select from all queues in the spaces specified by `spaceIds`.
442
+ * Diagnostics-only label for logs / tooling (not used by execution semantics).
386
443
  */
387
- allQueuesFromSpaces: Schema.optional(Schema.Boolean),
444
+ debugLabel: Schema.optional(Schema.String),
445
+ });
388
446
 
447
+ export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
448
+
449
+ /**
450
+ * Specifies the scope of the data to query from.
451
+ */
452
+ export const Scope = Schema.Struct({
389
453
  /**
390
- * The nested select statemets will select from the given queues.
454
+ * The nested select statemets will select from the given spaces.
391
455
  *
392
- * NOTE: Spaces and queues are unioned together if both are specified.
456
+ * NOTE: Spaces and feeds are unioned together if both are specified.
393
457
  */
394
- queues: Schema.optional(Schema.Array(DXN.Schema)),
458
+ spaceIds: Schema.optional(Schema.Array(Schema.String)),
395
459
 
396
460
  /**
397
- * Nested select statements will use this option to filter deleted objects.
461
+ * If true, the nested select statements will select from all feeds in the spaces specified by `spaceIds`.
398
462
  */
399
- deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
400
- });
463
+ allFeedsFromSpaces: Schema.optional(Schema.Boolean),
401
464
 
402
- export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
465
+ /**
466
+ * The nested select statemets will select from the given feeds (by EchoURI or legacy DXN).
467
+ *
468
+ * NOTE: Spaces and feeds are unioned together if both are specified.
469
+ */
470
+ feeds: Schema.optional(Schema.Array(EchoURI.Schema)),
471
+ });
472
+ export interface Scope extends Schema.Schema.Type<typeof Scope> {}
403
473
 
404
474
  export const visit = (query: Query, visitor: (node: Query) => void) => {
405
475
  visitor(query);
@@ -419,11 +489,49 @@ export const visit = (query: Query, visitor: (node: Query) => void) => {
419
489
  }),
420
490
  Match.when({ type: 'order' }, ({ query }) => visit(query, visitor)),
421
491
  Match.when({ type: 'limit' }, ({ query }) => visit(query, visitor)),
492
+ Match.when({ type: 'from' }, (node) => {
493
+ visit(node.query, visitor);
494
+ if (node.from._tag === 'query') {
495
+ visit(node.from.query, visitor);
496
+ }
497
+ }),
422
498
  Match.when({ type: 'select' }, () => {}),
423
499
  Match.exhaustive,
424
500
  );
425
501
  };
426
502
 
503
+ /**
504
+ * Recursively transforms a query tree bottom-up.
505
+ * The mapper receives each node with its children already transformed.
506
+ */
507
+ export const map = (query: Query, mapper: (node: Query) => Query): Query => {
508
+ const mapped: Query = Match.value(query).pipe(
509
+ Match.when({ type: 'filter' }, (node) => ({ ...node, selection: map(node.selection, mapper) })),
510
+ Match.when({ type: 'reference-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
511
+ Match.when({ type: 'incoming-references' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
512
+ Match.when({ type: 'relation' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
513
+ Match.when({ type: 'relation-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
514
+ Match.when({ type: 'hierarchy-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
515
+ Match.when({ type: 'options' }, (node) => ({ ...node, query: map(node.query, mapper) })),
516
+ Match.when({ type: 'order' }, (node) => ({ ...node, query: map(node.query, mapper) })),
517
+ Match.when({ type: 'limit' }, (node) => ({ ...node, query: map(node.query, mapper) })),
518
+ Match.when({ type: 'from' }, (node) => ({
519
+ ...node,
520
+ query: map(node.query, mapper),
521
+ ...(node.from._tag === 'query' ? { from: { _tag: 'query' as const, query: map(node.from.query, mapper) } } : {}),
522
+ })),
523
+ Match.when({ type: 'union' }, (node) => ({ ...node, queries: node.queries.map((q) => map(q, mapper)) })),
524
+ Match.when({ type: 'set-difference' }, (node) => ({
525
+ ...node,
526
+ source: map(node.source, mapper),
527
+ exclude: map(node.exclude, mapper),
528
+ })),
529
+ Match.when({ type: 'select' }, (node) => node),
530
+ Match.exhaustive,
531
+ );
532
+ return mapper(mapped);
533
+ };
534
+
427
535
  export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
428
536
  return Match.value(query).pipe(
429
537
  Match.withReturnType<T[]>(),
@@ -440,6 +548,13 @@ export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
440
548
  ),
441
549
  Match.when({ type: 'order' }, ({ query }) => fold(query, reducer)),
442
550
  Match.when({ type: 'limit' }, ({ query }) => fold(query, reducer)),
551
+ Match.when({ type: 'from' }, (node) => {
552
+ const results = fold(node.query, reducer);
553
+ if (node.from._tag === 'query') {
554
+ return results.concat(fold(node.from.query, reducer));
555
+ }
556
+ return results;
557
+ }),
443
558
  Match.when({ type: 'select' }, () => []),
444
559
  Match.exhaustive,
445
560
  );