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

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.f466a3d56e",
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/crypto": "0.8.4-main.f466a3d56e",
30
+ "@dxos/invariant": "0.8.4-main.f466a3d56e",
31
+ "@dxos/protocols": "0.8.4-main.f466a3d56e",
32
+ "@dxos/keys": "0.8.4-main.f466a3d56e",
33
+ "@dxos/util": "0.8.4-main.f466a3d56e"
37
34
  },
38
35
  "publishConfig": {
39
36
  "access": "public"
@@ -230,6 +230,18 @@ export type ObjectMeta = {
230
230
  * NOTE: Optional for backwards compatibilty.
231
231
  */
232
232
  tags?: string[];
233
+
234
+ /**
235
+ * Fully-qualified registry key for the object (FQN format, e.g. `org.example.type.foo`).
236
+ * Identifies the canonical registry entry the object instance was created from.
237
+ */
238
+ key?: string;
239
+
240
+ /**
241
+ * Semantic version of the registry entry the object was created from.
242
+ * Must be a valid semver string (e.g. `1.2.3`).
243
+ */
244
+ version?: string;
233
245
  };
234
246
 
235
247
  /**
@@ -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
@@ -42,6 +42,17 @@ const FilterObject_ = Schema.Struct({
42
42
  */
43
43
  foreignKeys: Schema.optional(Schema.Array(ForeignKey)),
44
44
 
45
+ /**
46
+ * Match objects whose meta `key` equals this fully-qualified registry key (FQN format).
47
+ */
48
+ metaKey: Schema.optional(Schema.String),
49
+
50
+ /**
51
+ * Semver range matched against the object's meta `version`.
52
+ * Only consulted when {@link metaKey} is set. Objects with no `version` do not satisfy a version-constrained filter.
53
+ */
54
+ metaVersion: Schema.optional(Schema.String),
55
+
45
56
  // NOTE: Make sure to update `FilterStep.isNoop` if you change this.
46
57
  });
47
58
  export interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
@@ -107,6 +118,20 @@ const FilterRange_ = Schema.Struct({
107
118
  export interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
108
119
  export const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
109
120
 
121
+ /**
122
+ * Filter by system timestamp (createdAt / updatedAt).
123
+ * Timestamps are unix milliseconds stored in the object meta index.
124
+ */
125
+ const FilterTimestamp_ = Schema.Struct({
126
+ type: Schema.Literal('timestamp'),
127
+ field: Schema.Literal('createdAt', 'updatedAt'),
128
+ operator: Schema.Literal('gt', 'gte', 'lt', 'lte'),
129
+ value: Schema.Number,
130
+ });
131
+
132
+ export interface FilterTimestamp extends Schema.Schema.Type<typeof FilterTimestamp_> {}
133
+ export const FilterTimestamp: Schema.Schema<FilterTimestamp> = FilterTimestamp_;
134
+
110
135
  /**
111
136
  * Text search.
112
137
  */
@@ -152,6 +177,21 @@ const FilterOr_ = Schema.Struct({
152
177
  export interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
153
178
  export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
154
179
 
180
+ /**
181
+ * Filter objects that are children of the specified parents.
182
+ * With transitive=true (default), matches grandchildren and beyond.
183
+ */
184
+ const FilterChildOf_ = Schema.Struct({
185
+ type: Schema.Literal('child-of'),
186
+ /** Parent DXNs to match children of. */
187
+ parents: Schema.Array(DXN.Schema),
188
+ /** Whether to match transitively (grandchildren, etc.). Defaults to true. */
189
+ transitive: Schema.Boolean,
190
+ });
191
+
192
+ export interface FilterChildOf extends Schema.Schema.Type<typeof FilterChildOf_> {}
193
+ export const FilterChildOf: Schema.Schema<FilterChildOf> = FilterChildOf_;
194
+
155
195
  /**
156
196
  * Union of filters.
157
197
  */
@@ -162,11 +202,13 @@ export const Filter = Schema.Union(
162
202
  FilterContains,
163
203
  FilterTag,
164
204
  FilterRange,
205
+ FilterTimestamp,
165
206
  FilterTextSearch,
207
+ FilterChildOf,
166
208
  FilterNot,
167
209
  FilterAnd,
168
210
  FilterOr,
169
- ).annotations({ identifier: 'dxos.org/schema/Filter' });
211
+ ).annotations({ identifier: 'org.dxos.schema.filter' });
170
212
 
171
213
  export type Filter = Schema.Schema.Type<typeof Filter>;
172
214
 
@@ -355,6 +397,21 @@ const QueryLimitClause_ = Schema.Struct({
355
397
  export interface QueryLimitClause extends Schema.Schema.Type<typeof QueryLimitClause_> {}
356
398
  export const QueryLimitClause: Schema.Schema<QueryLimitClause> = QueryLimitClause_;
357
399
 
400
+ export const QueryFromClause_ = Schema.Struct({
401
+ type: Schema.Literal('from'),
402
+ query: Schema.suspend(() => Query),
403
+ from: Schema.Union(
404
+ Schema.TaggedStruct('scope', {
405
+ scope: Schema.suspend(() => Scope),
406
+ }),
407
+ Schema.TaggedStruct('query', {
408
+ query: Schema.suspend(() => Query),
409
+ }),
410
+ ),
411
+ });
412
+ export interface QueryFromClause extends Schema.Schema.Type<typeof QueryFromClause_> {}
413
+ export const QueryFromClause: Schema.Schema<QueryFromClause> = QueryFromClause_;
414
+
358
415
  const Query_ = Schema.Union(
359
416
  QuerySelectClause,
360
417
  QueryFilterClause,
@@ -368,38 +425,50 @@ const Query_ = Schema.Union(
368
425
  QueryOrderClause,
369
426
  QueryOptionsClause,
370
427
  QueryLimitClause,
371
- ).annotations({ identifier: 'dxos.org/schema/Query' });
428
+ QueryFromClause,
429
+ ).annotations({ identifier: 'org.dxos.schema.query' });
372
430
 
373
431
  export type Query = Schema.Schema.Type<typeof Query_>;
374
432
  export const Query: Schema.Schema<Query> = Query_;
375
433
 
376
434
  export const QueryOptions = Schema.Struct({
377
435
  /**
378
- * The nested select statemets will select from the given spaces.
379
- *
380
- * NOTE: Spaces and queues are unioned together if both are specified.
436
+ * Nested select statements will use this option to filter deleted objects.
381
437
  */
382
- spaceIds: Schema.optional(Schema.Array(Schema.String)),
438
+ deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
383
439
 
384
440
  /**
385
- * If true, the nested select statements will select from all queues in the spaces specified by `spaceIds`.
441
+ * Diagnostics-only label for logs / tooling (not used by execution semantics).
386
442
  */
387
- allQueuesFromSpaces: Schema.optional(Schema.Boolean),
443
+ debugLabel: Schema.optional(Schema.String),
444
+ });
445
+
446
+ export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
388
447
 
448
+ /**
449
+ * Specifies the scope of the data to query from.
450
+ */
451
+ export const Scope = Schema.Struct({
389
452
  /**
390
- * The nested select statemets will select from the given queues.
453
+ * The nested select statemets will select from the given spaces.
391
454
  *
392
- * NOTE: Spaces and queues are unioned together if both are specified.
455
+ * NOTE: Spaces and feeds are unioned together if both are specified.
393
456
  */
394
- queues: Schema.optional(Schema.Array(DXN.Schema)),
457
+ spaceIds: Schema.optional(Schema.Array(Schema.String)),
395
458
 
396
459
  /**
397
- * Nested select statements will use this option to filter deleted objects.
460
+ * If true, the nested select statements will select from all feeds in the spaces specified by `spaceIds`.
398
461
  */
399
- deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
400
- });
462
+ allFeedsFromSpaces: Schema.optional(Schema.Boolean),
401
463
 
402
- export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
464
+ /**
465
+ * The nested select statemets will select from the given feeds (by underlying queue DXN).
466
+ *
467
+ * NOTE: Spaces and feeds are unioned together if both are specified.
468
+ */
469
+ feeds: Schema.optional(Schema.Array(DXN.Schema)),
470
+ });
471
+ export interface Scope extends Schema.Schema.Type<typeof Scope> {}
403
472
 
404
473
  export const visit = (query: Query, visitor: (node: Query) => void) => {
405
474
  visitor(query);
@@ -419,11 +488,49 @@ export const visit = (query: Query, visitor: (node: Query) => void) => {
419
488
  }),
420
489
  Match.when({ type: 'order' }, ({ query }) => visit(query, visitor)),
421
490
  Match.when({ type: 'limit' }, ({ query }) => visit(query, visitor)),
491
+ Match.when({ type: 'from' }, (node) => {
492
+ visit(node.query, visitor);
493
+ if (node.from._tag === 'query') {
494
+ visit(node.from.query, visitor);
495
+ }
496
+ }),
422
497
  Match.when({ type: 'select' }, () => {}),
423
498
  Match.exhaustive,
424
499
  );
425
500
  };
426
501
 
502
+ /**
503
+ * Recursively transforms a query tree bottom-up.
504
+ * The mapper receives each node with its children already transformed.
505
+ */
506
+ export const map = (query: Query, mapper: (node: Query) => Query): Query => {
507
+ const mapped: Query = Match.value(query).pipe(
508
+ Match.when({ type: 'filter' }, (node) => ({ ...node, selection: map(node.selection, mapper) })),
509
+ Match.when({ type: 'reference-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
510
+ Match.when({ type: 'incoming-references' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
511
+ Match.when({ type: 'relation' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
512
+ Match.when({ type: 'relation-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
513
+ Match.when({ type: 'hierarchy-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
514
+ Match.when({ type: 'options' }, (node) => ({ ...node, query: map(node.query, mapper) })),
515
+ Match.when({ type: 'order' }, (node) => ({ ...node, query: map(node.query, mapper) })),
516
+ Match.when({ type: 'limit' }, (node) => ({ ...node, query: map(node.query, mapper) })),
517
+ Match.when({ type: 'from' }, (node) => ({
518
+ ...node,
519
+ query: map(node.query, mapper),
520
+ ...(node.from._tag === 'query' ? { from: { _tag: 'query' as const, query: map(node.from.query, mapper) } } : {}),
521
+ })),
522
+ Match.when({ type: 'union' }, (node) => ({ ...node, queries: node.queries.map((q) => map(q, mapper)) })),
523
+ Match.when({ type: 'set-difference' }, (node) => ({
524
+ ...node,
525
+ source: map(node.source, mapper),
526
+ exclude: map(node.exclude, mapper),
527
+ })),
528
+ Match.when({ type: 'select' }, (node) => node),
529
+ Match.exhaustive,
530
+ );
531
+ return mapper(mapped);
532
+ };
533
+
427
534
  export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
428
535
  return Match.value(query).pipe(
429
536
  Match.withReturnType<T[]>(),
@@ -440,6 +547,13 @@ export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
440
547
  ),
441
548
  Match.when({ type: 'order' }, ({ query }) => fold(query, reducer)),
442
549
  Match.when({ type: 'limit' }, ({ query }) => fold(query, reducer)),
550
+ Match.when({ type: 'from' }, (node) => {
551
+ const results = fold(node.query, reducer);
552
+ if (node.from._tag === 'query') {
553
+ return results.concat(fold(node.from.query, reducer));
554
+ }
555
+ return results;
556
+ }),
443
557
  Match.when({ type: 'select' }, () => []),
444
558
  Match.exhaustive,
445
559
  );