@dxos/echo-protocol 0.8.4-staging.ac66bdf99f → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-protocol",
3
- "version": "0.8.4-staging.ac66bdf99f",
3
+ "version": "0.9.0",
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.20.0",
32
- "@dxos/crypto": "0.8.4-staging.ac66bdf99f",
33
- "@dxos/invariant": "0.8.4-staging.ac66bdf99f",
34
- "@dxos/keys": "0.8.4-staging.ac66bdf99f",
35
- "@dxos/protocols": "0.8.4-staging.ac66bdf99f",
36
- "@dxos/util": "0.8.4-staging.ac66bdf99f"
28
+ "effect": "3.21.3",
29
+ "@dxos/crypto": "0.9.0",
30
+ "@dxos/protocols": "0.9.0",
31
+ "@dxos/util": "0.9.0",
32
+ "@dxos/keys": "0.9.0",
33
+ "@dxos/invariant": "0.9.0"
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 { EntityId, URI } from '@dxos/keys';
7
7
  import { visitValues } from '@dxos/util';
8
8
 
9
9
  import { type RawString } from './automerge';
@@ -19,8 +19,8 @@ export type SpaceState = {
19
19
  /**
20
20
  * Array indexes get converted to strings.
21
21
  */
22
- export type ObjectProp = string;
23
- export type ObjectPropPath = ObjectProp[];
22
+ export type EntityProp = string;
23
+ export type EntityPropPath = EntityProp[];
24
24
 
25
25
  /**
26
26
  * Link to all documents that hold objects in the space.
@@ -35,13 +35,13 @@ export interface DatabaseDirectory {
35
35
  * Objects inlined in the current document.
36
36
  */
37
37
  objects?: {
38
- [id: string]: ObjectStructure;
38
+ [id: string]: EntityStructure;
39
39
  };
40
40
  /**
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
  /**
@@ -67,11 +67,11 @@ export const DatabaseDirectory = Object.freeze({
67
67
  return rawKey;
68
68
  },
69
69
 
70
- getInlineObject: (doc: DatabaseDirectory, id: ObjectId): ObjectStructure | undefined => {
70
+ getInlineObject: (doc: DatabaseDirectory, id: EntityId): EntityStructure | undefined => {
71
71
  return doc.objects?.[id];
72
72
  },
73
73
 
74
- getLink: (doc: DatabaseDirectory, id: ObjectId): string | undefined => {
74
+ getLink: (doc: DatabaseDirectory, id: EntityId): string | undefined => {
75
75
  return doc.links?.[id]?.toString();
76
76
  },
77
77
 
@@ -81,7 +81,7 @@ export const DatabaseDirectory = Object.freeze({
81
81
  links,
82
82
  }: {
83
83
  spaceKey: string;
84
- objects?: Record<string, ObjectStructure>;
84
+ objects?: Record<string, EntityStructure>;
85
85
  links?: Record<string, RawString>;
86
86
  }): DatabaseDirectory => ({
87
87
  access: {
@@ -95,11 +95,11 @@ export const DatabaseDirectory = Object.freeze({
95
95
  /**
96
96
  * Representation of an ECHO object in an AM document.
97
97
  */
98
- export type ObjectStructure = {
98
+ export type EntityStructure = {
99
99
  // TODO(dmaretskyi): Missing in some cases.
100
- system?: ObjectSystem;
100
+ system?: EntitySystem;
101
101
 
102
- meta: ObjectMeta;
102
+ meta: EntityMeta;
103
103
  /**
104
104
  * User-defined data.
105
105
  * Adheres to schema in `system.type`
@@ -107,46 +107,46 @@ export type ObjectStructure = {
107
107
  data: Record<string, any>;
108
108
  };
109
109
 
110
- // Helper methods to interact with the {@link ObjectStructure}.
111
- export const ObjectStructure = Object.freeze({
110
+ // Helper methods to interact with the {@link EntityStructure}.
111
+ export const EntityStructure = Object.freeze({
112
112
  /**
113
113
  * @throws On invalid object structure.
114
114
  */
115
- getTypeReference: (object: ObjectStructure): EncodedReference | undefined => {
115
+ getTypeReference: (object: EntityStructure): EncodedReference | undefined => {
116
116
  return object.system?.type;
117
117
  },
118
118
 
119
119
  /**
120
120
  * @throws On invalid object structure.
121
121
  */
122
- getEntityKind: (object: ObjectStructure): 'object' | 'relation' => {
122
+ getEntityKind: (object: EntityStructure): '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
 
128
- isDeleted: (object: ObjectStructure): boolean => {
128
+ isDeleted: (object: EntityStructure): boolean => {
129
129
  return object.system?.deleted ?? false;
130
130
  },
131
131
 
132
- getRelationSource: (object: ObjectStructure): EncodedReference | undefined => {
132
+ getRelationSource: (object: EntityStructure): EncodedReference | undefined => {
133
133
  return object.system?.source;
134
134
  },
135
135
 
136
- getRelationTarget: (object: ObjectStructure): EncodedReference | undefined => {
136
+ getRelationTarget: (object: EntityStructure): EncodedReference | undefined => {
137
137
  return object.system?.target;
138
138
  },
139
139
 
140
- getParent: (object: ObjectStructure): EncodedReference | undefined => {
140
+ getParent: (object: EntityStructure): EncodedReference | undefined => {
141
141
  return object.system?.parent;
142
142
  },
143
143
 
144
144
  /**
145
145
  * @returns All references in the data section of the object.
146
146
  */
147
- getAllOutgoingReferences: (object: ObjectStructure): { path: ObjectPropPath; reference: EncodedReference }[] => {
148
- const references: { path: ObjectPropPath; reference: EncodedReference }[] = [];
149
- const visit = (path: ObjectPropPath, value: unknown) => {
147
+ getAllOutgoingReferences: (object: EntityStructure): { path: EntityPropPath; reference: EncodedReference }[] => {
148
+ const references: { path: EntityPropPath; reference: EncodedReference }[] = [];
149
+ const visit = (path: EntityPropPath, value: unknown) => {
150
150
  if (isEncodedReference(value)) {
151
151
  references.push({ path, reference: value });
152
152
  } else {
@@ -157,7 +157,7 @@ export const ObjectStructure = Object.freeze({
157
157
  return references;
158
158
  },
159
159
 
160
- getTags: (object: ObjectStructure): string[] => {
160
+ getTags: (object: EntityStructure): (EncodedReference | string)[] => {
161
161
  return object.meta.tags ?? [];
162
162
  },
163
163
 
@@ -166,11 +166,11 @@ 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;
173
- }): ObjectStructure => {
173
+ }): EntityStructure => {
174
174
  return {
175
175
  system: {
176
176
  kind: 'object',
@@ -191,13 +191,13 @@ 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;
198
198
  keys?: ForeignKey[];
199
199
  data?: unknown;
200
- }): ObjectStructure => {
200
+ }): EntityStructure => {
201
201
  return {
202
202
  system: {
203
203
  kind: 'relation',
@@ -212,12 +212,25 @@ 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 }): EntityStructure => {
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
  /**
218
231
  * Echo object metadata.
219
232
  */
220
- export type ObjectMeta = {
233
+ export type EntityMeta = {
221
234
  /**
222
235
  * Foreign keys.
223
236
  */
@@ -225,25 +238,56 @@ export type ObjectMeta = {
225
238
 
226
239
  /**
227
240
  * Tags.
228
- * An array of DXNs of Tag objects within the space.
241
+ * Encoded references to Tag objects within the space.
229
242
  *
230
- * NOTE: Optional for backwards compatibilty.
243
+ * NOTE: Optional for backwards compatibility; legacy data may store bare DXN strings, which are
244
+ * upgraded to encoded references on read (see `object-core.ts`).
231
245
  */
232
- tags?: string[];
246
+ tags?: (EncodedReference | string)[];
247
+
248
+ /**
249
+ * Fully-qualified registry key for the object (FQN format, e.g. `org.example.type.foo`).
250
+ * Identifies the canonical registry entry the object instance was created from.
251
+ */
252
+ key?: string;
253
+
254
+ /**
255
+ * Semantic version of the registry entry the object was created from.
256
+ * Must be a valid semver string (e.g. `1.2.3`).
257
+ */
258
+ version?: string;
259
+
260
+ /**
261
+ * Dictionary of annotations to this entity.
262
+ *
263
+ * NOTE: Optional for backwards compatibility. Values are arbitrary decoded automerge primitives;
264
+ * typed as `any` so `EntityStructure` stays assignable to `DecodedAutomergePrimaryValue`.
265
+ */
266
+ annotations?: { readonly [key: string]: any };
233
267
  };
234
268
 
235
269
  /**
236
270
  * Automerge object system properties.
237
271
  * (Is automerge specific.)
238
272
  */
239
- export type ObjectSystem = {
273
+ export type EntitySystem = {
240
274
  /**
241
- * Entity kind.
275
+ * Entity kind. `'type'` covers persisted ECHO type definitions (instances of
276
+ * the `Type.Type` meta-schema); `'object'` / `'relation'` cover regular ECHO
277
+ * instances.
242
278
  */
243
- kind?: 'object' | 'relation';
279
+ kind?: 'object' | 'relation' | 'type';
244
280
 
245
281
  /**
246
- * Object reference ('protobuf' protocol) type.
282
+ * Object reference ('protobuf' protocol) type — DXN of the schema this
283
+ * entity instantiates.
284
+ *
285
+ * - For `kind === 'object'` / `'relation'` instances, this is the URI of the
286
+ * user-defined schema the entity was created from (e.g. `dxn:org.example.Person:1.0.0`).
287
+ * - For `kind === 'type'` entities (persisted Type.Type meta-instances) this
288
+ * is always the URI of the `TypeSchema` meta-schema itself
289
+ * (`dxn:org.dxos.type.schema:0.1.0`). The kind that the meta-instance
290
+ * _describes_ (object/relation/type) lives in `data.jsonSchema.entityKind`.
247
291
  */
248
292
  type?: EncodedReference;
249
293
 
@@ -267,6 +311,13 @@ export type ObjectSystem = {
267
311
  * Only for relations.
268
312
  */
269
313
  target?: EncodedReference;
314
+
315
+ /**
316
+ * Unix ms timestamp recorded at object creation time.
317
+ * Set once when the ObjectStructure is first written; never modified after that.
318
+ * Survives compaction / migrations (unlike automerge change timestamps).
319
+ */
320
+ createdAt?: number;
270
321
  };
271
322
 
272
323
  /**
@@ -276,6 +327,6 @@ export const PROPERTY_ID = 'id';
276
327
 
277
328
  /**
278
329
  * Data namespace.
279
- * The key on {@link ObjectStructure} that contains the user-defined data.
330
+ * The key on {@link EntityStructure} that contains the user-defined data.
280
331
  */
281
332
  export const DATA_NAMESPACE = 'data';
@@ -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
@@ -4,6 +4,7 @@
4
4
 
5
5
  export type * from './collection-sync';
6
6
  export * from './document-structure';
7
+ export * from './edge-peer';
7
8
  export * from './echo-feed-codec';
8
9
  export * from './foreign-key';
9
10
  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 { EID, EntityId, 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 EID (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
 
@@ -26,7 +27,7 @@ const FilterObject_ = Schema.Struct({
26
27
 
27
28
  typename: TypenameSpecifier,
28
29
 
29
- id: Schema.optional(Schema.Array(ObjectId)),
30
+ id: Schema.optional(Schema.Array(EntityId)),
30
31
 
31
32
  /**
32
33
  * Filter by property.
@@ -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_> {}
@@ -173,7 +185,7 @@ export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
173
185
  const FilterChildOf_ = Schema.Struct({
174
186
  type: Schema.Literal('child-of'),
175
187
  /** Parent DXNs to match children of. */
176
- parents: Schema.Array(DXN.Schema),
188
+ parents: Schema.Array(EID.Schema),
177
189
  /** Whether to match transitively (grandchildren, etc.). Defaults to true. */
178
190
  transitive: Schema.Boolean,
179
191
  });
@@ -344,6 +356,12 @@ const Order_ = Schema.Union(
344
356
  kind: Schema.Literal('rank'),
345
357
  direction: OrderDirection,
346
358
  }),
359
+ Schema.Struct({
360
+ // Order by system timestamp (createdAt / updatedAt) from the object meta index.
361
+ kind: Schema.Literal('timestamp'),
362
+ field: Schema.Literal('createdAt', 'updatedAt'),
363
+ direction: OrderDirection,
364
+ }),
347
365
  );
348
366
 
349
367
  export type Order = Schema.Schema.Type<typeof Order_>;
@@ -391,7 +409,7 @@ export const QueryFromClause_ = Schema.Struct({
391
409
  query: Schema.suspend(() => Query),
392
410
  from: Schema.Union(
393
411
  Schema.TaggedStruct('scope', {
394
- scope: Schema.suspend(() => Scope),
412
+ scopes: Schema.Array(Schema.suspend(() => Scope)),
395
413
  }),
396
414
  Schema.TaggedStruct('query', {
397
415
  query: Schema.suspend(() => Query),
@@ -425,34 +443,55 @@ export const QueryOptions = Schema.Struct({
425
443
  * Nested select statements will use this option to filter deleted objects.
426
444
  */
427
445
  deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
446
+
447
+ /**
448
+ * Diagnostics-only label for logs / tooling (not used by execution semantics).
449
+ */
450
+ debugLabel: Schema.optional(Schema.String),
428
451
  });
429
452
 
430
453
  export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
431
454
 
432
455
  /**
433
- * Specifies the scope of the data to query from.
456
+ * Selects from a space (automerge documents).
457
+ * When `spaceId` is omitted, targets the owning space — i.e. the space of whichever
458
+ * database executes the query. This lets callers reference "this space" without
459
+ * having to look up its id.
460
+ * When `includeAllFeeds` is true, also selects from all feeds belonging to that space.
434
461
  */
435
- export const Scope = Schema.Struct({
436
- /**
437
- * The nested select statemets will select from the given spaces.
438
- *
439
- * NOTE: Spaces and queues are unioned together if both are specified.
440
- */
441
- spaceIds: Schema.optional(Schema.Array(Schema.String)),
462
+ export const SpaceScope = Schema.TaggedStruct('space', {
463
+ spaceId: Schema.optional(Schema.String),
464
+ includeAllFeeds: Schema.optional(Schema.Boolean),
465
+ });
466
+ export interface SpaceScope extends Schema.Schema.Type<typeof SpaceScope> {}
442
467
 
443
- /**
444
- * If true, the nested select statements will select from all queues in the spaces specified by `spaceIds`.
445
- */
446
- allQueuesFromSpaces: Schema.optional(Schema.Boolean),
468
+ /**
469
+ * Selects from a specific feed (by its underlying queue DXN).
470
+ */
471
+ export const FeedScope = Schema.TaggedStruct('feed', {
472
+ feedUri: Schema.String,
473
+ });
474
+ export interface FeedScope extends Schema.Schema.Type<typeof FeedScope> {}
447
475
 
448
- /**
449
- * The nested select statemets will select from the given queues.
450
- *
451
- * NOTE: Spaces and queues are unioned together if both are specified.
452
- */
453
- queues: Schema.optional(Schema.Array(DXN.Schema)),
476
+ /**
477
+ * Selects from a code-shipped object registry.
478
+ *
479
+ * - `'local'` — the in-process registry attached to the hypergraph.
480
+ * - `'remote'` — a future remote registry service (not yet implemented).
481
+ *
482
+ * To include both, add two separate `RegistryScope` entries to the `scopes` array.
483
+ */
484
+ export const RegistryScope = Schema.TaggedStruct('registry', {
485
+ location: Schema.Literal('local', 'remote'),
454
486
  });
455
- export interface Scope extends Schema.Schema.Type<typeof Scope> {}
487
+ export interface RegistryScope extends Schema.Schema.Type<typeof RegistryScope> {}
488
+
489
+ /**
490
+ * Specifies the scope of the data to query from.
491
+ * A `from` clause may carry multiple scopes; results are unioned across them.
492
+ */
493
+ export const Scope = Schema.Union(SpaceScope, FeedScope, RegistryScope);
494
+ export type Scope = Schema.Schema.Type<typeof Scope>;
456
495
 
457
496
  export const visit = (query: Query, visitor: (node: Query) => void) => {
458
497
  visitor(query);