@dxos/echo-protocol 0.8.4-main.fffef41 → 0.8.4-staging.60fe92afc8

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,37 +1,36 @@
1
1
  {
2
2
  "name": "@dxos/echo-protocol",
3
- "version": "0.8.4-main.fffef41",
3
+ "version": "0.8.4-staging.60fe92afc8",
4
4
  "description": "Core ECHO APIs.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
- "repository": "github:dxos/dxos",
8
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
9
12
  "author": "DXOS.org",
10
- "sideEffects": true,
13
+ "sideEffects": false,
11
14
  "type": "module",
12
15
  "exports": {
13
16
  ".": {
14
17
  "source": "./src/index.ts",
15
18
  "types": "./dist/types/src/index.d.ts",
16
- "browser": "./dist/lib/browser/index.mjs",
17
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "default": "./dist/lib/neutral/index.mjs"
18
20
  }
19
21
  },
20
22
  "types": "dist/types/src/index.d.ts",
21
- "typesVersions": {
22
- "*": {}
23
- },
24
23
  "files": [
25
24
  "dist",
26
25
  "src"
27
26
  ],
28
27
  "dependencies": {
29
- "effect": "3.18.3",
30
- "@dxos/crypto": "0.8.4-main.fffef41",
31
- "@dxos/invariant": "0.8.4-main.fffef41",
32
- "@dxos/keys": "0.8.4-main.fffef41",
33
- "@dxos/protocols": "0.8.4-main.fffef41",
34
- "@dxos/util": "0.8.4-main.fffef41"
28
+ "effect": "3.21.3",
29
+ "@dxos/crypto": "0.8.4-staging.60fe92afc8",
30
+ "@dxos/invariant": "0.8.4-staging.60fe92afc8",
31
+ "@dxos/keys": "0.8.4-staging.60fe92afc8",
32
+ "@dxos/protocols": "0.8.4-staging.60fe92afc8",
33
+ "@dxos/util": "0.8.4-staging.60fe92afc8"
35
34
  },
36
35
  "publishConfig": {
37
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,42 +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: EntityStructure): EncodedReference | undefined => {
141
+ return object.system?.parent;
142
+ },
143
+
140
144
  /**
141
145
  * @returns All references in the data section of the object.
142
146
  */
143
- getAllOutgoingReferences: (object: ObjectStructure): { path: ObjectPropPath; reference: EncodedReference }[] => {
144
- const references: { path: ObjectPropPath; reference: EncodedReference }[] = [];
145
- 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) => {
146
150
  if (isEncodedReference(value)) {
147
151
  references.push({ path, reference: value });
148
152
  } else {
@@ -153,7 +157,7 @@ export const ObjectStructure = Object.freeze({
153
157
  return references;
154
158
  },
155
159
 
156
- getTags: (object: ObjectStructure): string[] => {
160
+ getTags: (object: EntityStructure): (EncodedReference | string)[] => {
157
161
  return object.meta.tags ?? [];
158
162
  },
159
163
 
@@ -162,11 +166,11 @@ export const ObjectStructure = Object.freeze({
162
166
  data,
163
167
  keys,
164
168
  }: {
165
- type: DXN.String;
169
+ type: URI.URI;
166
170
  deleted?: boolean;
167
171
  keys?: ForeignKey[];
168
172
  data?: unknown;
169
- }): ObjectStructure => {
173
+ }): EntityStructure => {
170
174
  return {
171
175
  system: {
172
176
  kind: 'object',
@@ -187,13 +191,13 @@ export const ObjectStructure = Object.freeze({
187
191
  keys,
188
192
  data,
189
193
  }: {
190
- type: DXN.String;
194
+ type: URI.URI;
191
195
  source: EncodedReference;
192
196
  target: EncodedReference;
193
197
  deleted?: boolean;
194
198
  keys?: ForeignKey[];
195
199
  data?: unknown;
196
- }): ObjectStructure => {
200
+ }): EntityStructure => {
197
201
  return {
198
202
  system: {
199
203
  kind: 'relation',
@@ -208,12 +212,25 @@ export const ObjectStructure = Object.freeze({
208
212
  data: data ?? {},
209
213
  };
210
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
+ },
211
228
  });
212
229
 
213
230
  /**
214
231
  * Echo object metadata.
215
232
  */
216
- export type ObjectMeta = {
233
+ export type EntityMeta = {
217
234
  /**
218
235
  * Foreign keys.
219
236
  */
@@ -221,25 +238,56 @@ export type ObjectMeta = {
221
238
 
222
239
  /**
223
240
  * Tags.
224
- * An array of DXNs of Tag objects within the space.
241
+ * Encoded references to Tag objects within the space.
225
242
  *
226
- * 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`).
245
+ */
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.
227
251
  */
228
- tags?: string[];
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 };
229
267
  };
230
268
 
231
269
  /**
232
270
  * Automerge object system properties.
233
271
  * (Is automerge specific.)
234
272
  */
235
- export type ObjectSystem = {
273
+ export type EntitySystem = {
236
274
  /**
237
- * 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.
238
278
  */
239
- kind?: 'object' | 'relation';
279
+ kind?: 'object' | 'relation' | 'type';
240
280
 
241
281
  /**
242
- * 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`.
243
291
  */
244
292
  type?: EncodedReference;
245
293
 
@@ -248,15 +296,28 @@ export type ObjectSystem = {
248
296
  */
249
297
  deleted?: boolean;
250
298
 
299
+ /**
300
+ * Object parent.
301
+ * Objects with no parent are at the top level of the object hierarchy in the space.
302
+ */
303
+ parent?: EncodedReference;
304
+
251
305
  /**
252
306
  * Only for relations.
253
307
  */
254
308
  source?: EncodedReference;
255
309
 
256
310
  /**
257
- * Only for relations.w
311
+ * Only for relations.
258
312
  */
259
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;
260
321
  };
261
322
 
262
323
  /**
@@ -266,6 +327,6 @@ export const PROPERTY_ID = 'id';
266
327
 
267
328
  /**
268
329
  * Data namespace.
269
- * The key on {@link ObjectStructure} that contains the user-defined data.
330
+ * The key on {@link EntityStructure} that contains the user-defined data.
270
331
  */
271
332
  export const DATA_NAMESPACE = 'data';
@@ -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';