@dxos/echo-protocol 0.8.4-main.84f28bd → 0.8.4-main.937b3ca

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,16 +1,20 @@
1
1
  {
2
2
  "name": "@dxos/echo-protocol",
3
- "version": "0.8.4-main.84f28bd",
3
+ "version": "0.8.4-main.937b3ca",
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",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
8
11
  "license": "MIT",
9
12
  "author": "DXOS.org",
10
- "sideEffects": true,
13
+ "sideEffects": false,
11
14
  "type": "module",
12
15
  "exports": {
13
16
  ".": {
17
+ "source": "./src/index.ts",
14
18
  "types": "./dist/types/src/index.d.ts",
15
19
  "browser": "./dist/lib/browser/index.mjs",
16
20
  "node": "./dist/lib/node-esm/index.mjs"
@@ -25,12 +29,12 @@
25
29
  "src"
26
30
  ],
27
31
  "dependencies": {
28
- "effect": "3.16.13",
29
- "@dxos/crypto": "0.8.4-main.84f28bd",
30
- "@dxos/invariant": "0.8.4-main.84f28bd",
31
- "@dxos/keys": "0.8.4-main.84f28bd",
32
- "@dxos/protocols": "0.8.4-main.84f28bd",
33
- "@dxos/util": "0.8.4-main.84f28bd"
32
+ "effect": "3.19.11",
33
+ "@dxos/crypto": "0.8.4-main.937b3ca",
34
+ "@dxos/invariant": "0.8.4-main.937b3ca",
35
+ "@dxos/util": "0.8.4-main.937b3ca",
36
+ "@dxos/keys": "0.8.4-main.937b3ca",
37
+ "@dxos/protocols": "0.8.4-main.937b3ca"
34
38
  },
35
39
  "publishConfig": {
36
40
  "access": "public"
@@ -8,7 +8,7 @@ import { visitValues } from '@dxos/util';
8
8
 
9
9
  import { type RawString } from './automerge';
10
10
  import type { ForeignKey } from './foreign-key';
11
- import { isEncodedReference, type EncodedReference } from './reference';
11
+ import { type EncodedReference, isEncodedReference } from './reference';
12
12
  import { type SpaceDocVersion } from './space-doc-version';
13
13
 
14
14
  export type SpaceState = {
@@ -153,6 +153,10 @@ export const ObjectStructure = Object.freeze({
153
153
  return references;
154
154
  },
155
155
 
156
+ getTags: (object: ObjectStructure): string[] => {
157
+ return object.meta.tags ?? [];
158
+ },
159
+
156
160
  makeObject: ({
157
161
  type,
158
162
  data,
@@ -214,6 +218,14 @@ export type ObjectMeta = {
214
218
  * Foreign keys.
215
219
  */
216
220
  keys: ForeignKey[];
221
+
222
+ /**
223
+ * Tags.
224
+ * An array of DXNs of Tag objects within the space.
225
+ *
226
+ * NOTE: Optional for backwards compatibilty.
227
+ */
228
+ tags?: string[];
217
229
  };
218
230
 
219
231
  /**
@@ -2,7 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, SchemaAST } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
+ import * as SchemaAST from 'effect/SchemaAST';
6
7
 
7
8
  const ForeignKey_ = Schema.Struct({
8
9
  /**
@@ -15,8 +16,8 @@ const ForeignKey_ = Schema.Struct({
15
16
  * Id within the foreign database.
16
17
  */
17
18
  // TODO(wittjosiah): This annotation is currently used to ensure id field shows up in forms.
18
- // TODO(dmaretskyi): `false` is not a valid value for the annotation.
19
- id: Schema.String.annotations({ [SchemaAST.IdentifierAnnotationId]: false }),
19
+ // TODO(dmaretskyi): `false` is not a valid value for the annotation. Use a different annotation.
20
+ id: Schema.String.annotations({ [SchemaAST.IdentifierAnnotationId]: 'false' }),
20
21
  });
21
22
 
22
23
  export type ForeignKey = Schema.Schema.Type<typeof ForeignKey_>;
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  export * from './document-structure';
6
6
  export * from './reference';
7
7
  export * from './space-doc-version';
8
- export * from './collection-sync';
8
+ export type * from './collection-sync';
9
9
  export * from './space-id';
10
10
  export * from './foreign-key';
11
11
  export * from './query';
package/src/query/ast.ts CHANGED
@@ -2,14 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import * as Match from 'effect/Match';
6
+ import * as Schema from 'effect/Schema';
6
7
 
7
8
  import { DXN, ObjectId } from '@dxos/keys';
8
9
 
9
10
  import { ForeignKey } from '../foreign-key';
10
11
 
11
12
  const TypenameSpecifier = Schema.Union(DXN.Schema, Schema.Null).annotations({
12
- description: 'DXN or null. Null means any type will match',
13
+ description: 'DXN or null; null matches any type',
13
14
  });
14
15
 
15
16
  // 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.
@@ -46,6 +47,9 @@ const FilterObject_ = Schema.Struct({
46
47
  export interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
47
48
  export const FilterObject: Schema.Schema<FilterObject> = FilterObject_;
48
49
 
50
+ /**
51
+ * Compare.
52
+ */
49
53
  const FilterCompare_ = Schema.Struct({
50
54
  type: Schema.Literal('compare'),
51
55
  operator: Schema.Literal('eq', 'neq', 'gt', 'gte', 'lt', 'lte'),
@@ -54,6 +58,9 @@ const FilterCompare_ = Schema.Struct({
54
58
  export interface FilterCompare extends Schema.Schema.Type<typeof FilterCompare_> {}
55
59
  export const FilterCompare: Schema.Schema<FilterCompare> = FilterCompare_;
56
60
 
61
+ /**
62
+ * In.
63
+ */
57
64
  const FilterIn_ = Schema.Struct({
58
65
  type: Schema.Literal('in'),
59
66
  values: Schema.Array(Schema.Any),
@@ -61,53 +68,106 @@ const FilterIn_ = Schema.Struct({
61
68
  export interface FilterIn extends Schema.Schema.Type<typeof FilterIn_> {}
62
69
  export const FilterIn: Schema.Schema<FilterIn> = FilterIn_;
63
70
 
71
+ /**
72
+ * Contains.
73
+ */
74
+ const FilterContains_ = Schema.Struct({
75
+ type: Schema.Literal('contains'),
76
+ value: Schema.Any,
77
+ });
78
+
79
+ export interface FilterContains extends Schema.Schema.Type<typeof FilterContains_> {}
80
+
81
+ /**
82
+ * Predicate for an array property to contain the provided value.
83
+ * Nested objects are matched using strict structural matching.
84
+ */
85
+ export const FilterContains: Schema.Schema<FilterContains> = FilterContains_;
86
+
87
+ /**
88
+ * Filters objects that have certain tag.
89
+ */
90
+ const FilterTag_ = Schema.Struct({
91
+ type: Schema.Literal('tag'),
92
+ tag: Schema.String, // TODO(burdon): Make OR-collection?
93
+ });
94
+
95
+ export interface FilterTag extends Schema.Schema.Type<typeof FilterTag_> {}
96
+ export const FilterTag: Schema.Schema<FilterTag> = FilterTag_;
97
+
98
+ /**
99
+ * Range.
100
+ */
64
101
  const FilterRange_ = Schema.Struct({
65
102
  type: Schema.Literal('range'),
66
103
  from: Schema.Any,
67
104
  to: Schema.Any,
68
105
  });
106
+
69
107
  export interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
70
108
  export const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
71
109
 
110
+ /**
111
+ * Text search.
112
+ */
72
113
  const FilterTextSearch_ = Schema.Struct({
73
114
  type: Schema.Literal('text-search'),
74
115
  text: Schema.String,
75
116
  searchKind: Schema.optional(Schema.Literal('full-text', 'vector')),
76
117
  });
118
+
77
119
  export interface FilterTextSearch extends Schema.Schema.Type<typeof FilterTextSearch_> {}
78
120
  export const FilterTextSearch: Schema.Schema<FilterTextSearch> = FilterTextSearch_;
79
121
 
122
+ /**
123
+ * Not.
124
+ */
80
125
  const FilterNot_ = Schema.Struct({
81
126
  type: Schema.Literal('not'),
82
127
  filter: Schema.suspend(() => Filter),
83
128
  });
129
+
84
130
  export interface FilterNot extends Schema.Schema.Type<typeof FilterNot_> {}
85
131
  export const FilterNot: Schema.Schema<FilterNot> = FilterNot_;
86
132
 
133
+ /**
134
+ * And.
135
+ */
87
136
  const FilterAnd_ = Schema.Struct({
88
137
  type: Schema.Literal('and'),
89
138
  filters: Schema.Array(Schema.suspend(() => Filter)),
90
139
  });
140
+
91
141
  export interface FilterAnd extends Schema.Schema.Type<typeof FilterAnd_> {}
92
142
  export const FilterAnd: Schema.Schema<FilterAnd> = FilterAnd_;
93
143
 
144
+ /**
145
+ * Or.
146
+ */
94
147
  const FilterOr_ = Schema.Struct({
95
148
  type: Schema.Literal('or'),
96
149
  filters: Schema.Array(Schema.suspend(() => Filter)),
97
150
  });
151
+
98
152
  export interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
99
153
  export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
100
154
 
155
+ /**
156
+ * Union of filters.
157
+ */
101
158
  export const Filter = Schema.Union(
102
159
  FilterObject,
103
- FilterTextSearch,
104
160
  FilterCompare,
105
161
  FilterIn,
162
+ FilterContains,
163
+ FilterTag,
106
164
  FilterRange,
165
+ FilterTextSearch,
107
166
  FilterNot,
108
167
  FilterAnd,
109
168
  FilterOr,
110
- );
169
+ ).annotations({ identifier: 'dxos.org/schema/Filter' });
170
+
111
171
  export type Filter = Schema.Schema.Type<typeof Filter>;
112
172
 
113
173
  /**
@@ -117,6 +177,7 @@ const QuerySelectClause_ = Schema.Struct({
117
177
  type: Schema.Literal('select'),
118
178
  filter: Schema.suspend(() => Filter),
119
179
  });
180
+
120
181
  export interface QuerySelectClause extends Schema.Schema.Type<typeof QuerySelectClause_> {}
121
182
  export const QuerySelectClause: Schema.Schema<QuerySelectClause> = QuerySelectClause_;
122
183
 
@@ -128,6 +189,7 @@ const QueryFilterClause_ = Schema.Struct({
128
189
  selection: Schema.suspend(() => Query),
129
190
  filter: Schema.suspend(() => Filter),
130
191
  });
192
+
131
193
  export interface QueryFilterClause extends Schema.Schema.Type<typeof QueryFilterClause_> {}
132
194
  export const QueryFilterClause: Schema.Schema<QueryFilterClause> = QueryFilterClause_;
133
195
 
@@ -139,6 +201,7 @@ const QueryReferenceTraversalClause_ = Schema.Struct({
139
201
  anchor: Schema.suspend(() => Query),
140
202
  property: Schema.String, // TODO(dmaretskyi): Change to EscapedPropPath.
141
203
  });
204
+
142
205
  export interface QueryReferenceTraversalClause extends Schema.Schema.Type<typeof QueryReferenceTraversalClause_> {}
143
206
  export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversalClause> =
144
207
  QueryReferenceTraversalClause_;
@@ -149,9 +212,14 @@ export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversa
149
212
  const QueryIncomingReferencesClause_ = Schema.Struct({
150
213
  type: Schema.Literal('incoming-references'),
151
214
  anchor: Schema.suspend(() => Query),
152
- property: Schema.String,
215
+ /**
216
+ * Property path where the reference is located.
217
+ * If null, matches references from any property.
218
+ */
219
+ property: Schema.NullOr(Schema.String),
153
220
  typename: TypenameSpecifier,
154
221
  });
222
+
155
223
  export interface QueryIncomingReferencesClause extends Schema.Schema.Type<typeof QueryIncomingReferencesClause_> {}
156
224
  export const QueryIncomingReferencesClause: Schema.Schema<QueryIncomingReferencesClause> =
157
225
  QueryIncomingReferencesClause_;
@@ -170,6 +238,7 @@ const QueryRelationClause_ = Schema.Struct({
170
238
  direction: Schema.Literal('outgoing', 'incoming', 'both'),
171
239
  filter: Schema.optional(Schema.suspend(() => Filter)),
172
240
  });
241
+
173
242
  export interface QueryRelationClause extends Schema.Schema.Type<typeof QueryRelationClause_> {}
174
243
  export const QueryRelationClause: Schema.Schema<QueryRelationClause> = QueryRelationClause_;
175
244
 
@@ -181,6 +250,7 @@ const QueryRelationTraversalClause_ = Schema.Struct({
181
250
  anchor: Schema.suspend(() => Query),
182
251
  direction: Schema.Literal('source', 'target', 'both'),
183
252
  });
253
+
184
254
  export interface QueryRelationTraversalClause extends Schema.Schema.Type<typeof QueryRelationTraversalClause_> {}
185
255
  export const QueryRelationTraversalClause: Schema.Schema<QueryRelationTraversalClause> = QueryRelationTraversalClause_;
186
256
 
@@ -191,6 +261,7 @@ const QueryUnionClause_ = Schema.Struct({
191
261
  type: Schema.Literal('union'),
192
262
  queries: Schema.Array(Schema.suspend(() => Query)),
193
263
  });
264
+
194
265
  export interface QueryUnionClause extends Schema.Schema.Type<typeof QueryUnionClause_> {}
195
266
  export const QueryUnionClause: Schema.Schema<QueryUnionClause> = QueryUnionClause_;
196
267
 
@@ -202,9 +273,47 @@ const QuerySetDifferenceClause_ = Schema.Struct({
202
273
  source: Schema.suspend(() => Query),
203
274
  exclude: Schema.suspend(() => Query),
204
275
  });
276
+
205
277
  export interface QuerySetDifferenceClause extends Schema.Schema.Type<typeof QuerySetDifferenceClause_> {}
206
278
  export const QuerySetDifferenceClause: Schema.Schema<QuerySetDifferenceClause> = QuerySetDifferenceClause_;
207
279
 
280
+ export const OrderDirection = Schema.Literal('asc', 'desc');
281
+ export type OrderDirection = Schema.Schema.Type<typeof OrderDirection>;
282
+
283
+ const Order_ = Schema.Union(
284
+ Schema.Struct({
285
+ // How database wants to order them (in practice - by id).
286
+ kind: Schema.Literal('natural'),
287
+ }),
288
+ Schema.Struct({
289
+ kind: Schema.Literal('property'),
290
+ property: Schema.String,
291
+ direction: OrderDirection,
292
+ }),
293
+ Schema.Struct({
294
+ // Order by relevance rank (for FTS/vector search results).
295
+ // Default direction is 'desc' (higher rank = better match first).
296
+ kind: Schema.Literal('rank'),
297
+ direction: OrderDirection,
298
+ }),
299
+ );
300
+
301
+ export type Order = Schema.Schema.Type<typeof Order_>;
302
+ export const Order: Schema.Schema<Order> = Order_;
303
+
304
+ /**
305
+ * Order the query results.
306
+ * Left-to-right the orders dominate.
307
+ */
308
+ const QueryOrderClause_ = Schema.Struct({
309
+ type: Schema.Literal('order'),
310
+ query: Schema.suspend(() => Query),
311
+ order: Schema.Array(Order),
312
+ });
313
+
314
+ export interface QueryOrderClause extends Schema.Schema.Type<typeof QueryOrderClause_> {}
315
+ export const QueryOrderClause: Schema.Schema<QueryOrderClause> = QueryOrderClause_;
316
+
208
317
  /**
209
318
  * Add options to a query.
210
319
  */
@@ -213,9 +322,22 @@ const QueryOptionsClause_ = Schema.Struct({
213
322
  query: Schema.suspend(() => Query),
214
323
  options: Schema.suspend(() => QueryOptions),
215
324
  });
325
+
216
326
  export interface QueryOptionsClause extends Schema.Schema.Type<typeof QueryOptionsClause_> {}
217
327
  export const QueryOptionsClause: Schema.Schema<QueryOptionsClause> = QueryOptionsClause_;
218
328
 
329
+ /**
330
+ * Limit the number of results.
331
+ */
332
+ const QueryLimitClause_ = Schema.Struct({
333
+ type: Schema.Literal('limit'),
334
+ query: Schema.suspend(() => Query),
335
+ limit: Schema.Number,
336
+ });
337
+
338
+ export interface QueryLimitClause extends Schema.Schema.Type<typeof QueryLimitClause_> {}
339
+ export const QueryLimitClause: Schema.Schema<QueryLimitClause> = QueryLimitClause_;
340
+
219
341
  const Query_ = Schema.Union(
220
342
  QuerySelectClause,
221
343
  QueryFilterClause,
@@ -225,44 +347,80 @@ const Query_ = Schema.Union(
225
347
  QueryRelationTraversalClause,
226
348
  QueryUnionClause,
227
349
  QuerySetDifferenceClause,
350
+ QueryOrderClause,
228
351
  QueryOptionsClause,
229
- );
352
+ QueryLimitClause,
353
+ ).annotations({ identifier: 'dxos.org/schema/Query' });
230
354
 
231
355
  export type Query = Schema.Schema.Type<typeof Query_>;
232
356
  export const Query: Schema.Schema<Query> = Query_;
233
357
 
234
358
  export const QueryOptions = Schema.Struct({
359
+ /**
360
+ * The nested select statemets will select from the given spaces.
361
+ *
362
+ * NOTE: Spaces and queues are unioned together if both are specified.
363
+ */
235
364
  spaceIds: Schema.optional(Schema.Array(Schema.String)),
365
+
366
+ /**
367
+ * If true, the nested select statements will select from all queues in the spaces specified by `spaceIds`.
368
+ */
369
+ allQueuesFromSpaces: Schema.optional(Schema.Boolean),
370
+
371
+ /**
372
+ * The nested select statemets will select from the given queues.
373
+ *
374
+ * NOTE: Spaces and queues are unioned together if both are specified.
375
+ */
376
+ queues: Schema.optional(Schema.Array(DXN.Schema)),
377
+
378
+ /**
379
+ * Nested select statements will use this option to filter deleted objects.
380
+ */
236
381
  deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
237
382
  });
383
+
238
384
  export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
239
385
 
240
386
  export const visit = (query: Query, visitor: (node: Query) => void) => {
241
- switch (query.type) {
242
- case 'filter':
243
- visit(query.selection, visitor);
244
- break;
245
- case 'reference-traversal':
246
- visit(query.anchor, visitor);
247
- break;
248
- case 'incoming-references':
249
- visit(query.anchor, visitor);
250
- break;
251
- case 'relation':
252
- visit(query.anchor, visitor);
253
- break;
254
- case 'options':
255
- visit(query.query, visitor);
256
- break;
257
- case 'relation-traversal':
258
- visit(query.anchor, visitor);
259
- break;
260
- case 'union':
261
- query.queries.forEach((q) => visit(q, visitor));
262
- break;
263
- case 'set-difference':
264
- visit(query.source, visitor);
265
- visit(query.exclude, visitor);
266
- break;
267
- }
387
+ visitor(query);
388
+
389
+ Match.value(query).pipe(
390
+ Match.when({ type: 'filter' }, ({ selection }) => visit(selection, visitor)),
391
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => visit(anchor, visitor)),
392
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => visit(anchor, visitor)),
393
+ Match.when({ type: 'relation' }, ({ anchor }) => visit(anchor, visitor)),
394
+ Match.when({ type: 'options' }, ({ query }) => visit(query, visitor)),
395
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => visit(anchor, visitor)),
396
+ Match.when({ type: 'union' }, ({ queries }) => queries.forEach((q) => visit(q, visitor))),
397
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) => {
398
+ visit(source, visitor);
399
+ visit(exclude, visitor);
400
+ }),
401
+ Match.when({ type: 'order' }, ({ query }) => visit(query, visitor)),
402
+ Match.when({ type: 'limit' }, ({ query }) => visit(query, visitor)),
403
+ Match.when({ type: 'select' }, () => {}),
404
+ Match.exhaustive,
405
+ );
406
+ };
407
+
408
+ export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
409
+ return Match.value(query).pipe(
410
+ Match.withReturnType<T[]>(),
411
+ Match.when({ type: 'filter' }, ({ selection }) => fold(selection, reducer)),
412
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => fold(anchor, reducer)),
413
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => fold(anchor, reducer)),
414
+ Match.when({ type: 'relation' }, ({ anchor }) => fold(anchor, reducer)),
415
+ Match.when({ type: 'options' }, ({ query }) => fold(query, reducer)),
416
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => fold(anchor, reducer)),
417
+ Match.when({ type: 'union' }, ({ queries }) => queries.flatMap((q) => fold(q, reducer))),
418
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) =>
419
+ fold(source, reducer).concat(fold(exclude, reducer)),
420
+ ),
421
+ Match.when({ type: 'order' }, ({ query }) => fold(query, reducer)),
422
+ Match.when({ type: 'limit' }, ({ query }) => fold(query, reducer)),
423
+ Match.when({ type: 'select' }, () => []),
424
+ Match.exhaustive,
425
+ );
268
426
  };
package/src/reference.ts CHANGED
@@ -170,13 +170,16 @@ export const isEncodedReference = (value: any): value is EncodedReference =>
170
170
  export const EncodedReference = Object.freeze({
171
171
  isEncodedReference,
172
172
  getReferenceString: (value: EncodedReference): string => {
173
- assertArgument(isEncodedReference(value), 'invalid reference');
173
+ assertArgument(isEncodedReference(value), 'value', 'invalid reference');
174
174
  return value['/'];
175
175
  },
176
176
  toDXN: (value: EncodedReference): DXN => {
177
177
  return DXN.parse(EncodedReference.getReferenceString(value));
178
178
  },
179
179
  fromDXN: (dxn: DXN): EncodedReference => {
180
- return encodeReference(Reference.fromDXN(dxn));
180
+ return { '/': dxn.toString() };
181
+ },
182
+ fromLegacyTypename: (typename: string): EncodedReference => {
183
+ return { '/': DXN.fromTypename(typename).toString() };
181
184
  },
182
185
  });
package/src/space-id.ts CHANGED
@@ -18,7 +18,7 @@ export const createIdFromSpaceKey = async (spaceKey: PublicKey): Promise<SpaceId
18
18
  return cachedValue;
19
19
  }
20
20
 
21
- const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array());
21
+ const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array() as Uint8Array<ArrayBuffer>);
22
22
 
23
23
  const bytes = new Uint8Array(digest).slice(0, SpaceId.byteLength);
24
24
  const spaceId = SpaceId.encode(bytes);