@dxos/echo-protocol 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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.
@@ -4,7 +4,7 @@ import { ComplexMap, compositeKey, visitValues } from "@dxos/util";
4
4
  import { subtleCrypto } from "@dxos/crypto/subtle";
5
5
  import { EdgeService, FeedProtocol } from "@dxos/protocols";
6
6
  import * as Schema from "effect/Schema";
7
- import { SchemaAST } from "@dxos/effect";
7
+ import * as SchemaAST from "@dxos/effect/SchemaAST";
8
8
  import * as Match from "effect/Match";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __defProp = Object.defineProperty;
@@ -151,6 +151,15 @@ var EntityStructure = Object.freeze({
151
151
  isDeleted: (object) => {
152
152
  return object.system?.deleted ?? false;
153
153
  },
154
+ getConvergenceKey: (object) => {
155
+ return object.meta?.convergenceKey;
156
+ },
157
+ getMergedInto: (object) => {
158
+ return object.system?.mergedInto;
159
+ },
160
+ getMergedFrom: (object) => {
161
+ return object.system?.mergedFrom ?? [];
162
+ },
154
163
  getRelationSource: (object) => {
155
164
  return object.system?.source;
156
165
  },
@@ -224,25 +233,26 @@ var DATA_NAMESPACE = "data";
224
233
  //#endregion
225
234
  //#region src/edge-peer.ts
226
235
  /**
227
- * Returns true if the given peerId belongs to an EDGE replicator (Automerge or Subduction).
236
+ * Returns true if the given peerId belongs to the EDGE Subduction replicator.
228
237
  *
229
238
  * When `spaceId` is provided, the match is scoped to that space (peerId must start with
230
239
  * `<service>:<spaceId>`). When omitted, only the leading service segment is checked
231
240
  * (peerId must start with `<service>:`), which is useful when the caller doesn't have a
232
- * spaceId on hand or wants to match any edge replicator regardless of space.
241
+ * spaceId on hand or wants to match the edge replicator regardless of space.
233
242
  */
234
243
  var isEdgePeerId = (peerId, spaceId) => {
235
- const automergePrefix = spaceId !== void 0 ? compositeKey(EdgeService.AUTOMERGE_REPLICATOR, spaceId) : `${EdgeService.AUTOMERGE_REPLICATOR}:`;
236
- const subductionPrefix = spaceId !== void 0 ? compositeKey(EdgeService.SUBDUCTION_REPLICATOR, spaceId) : `${EdgeService.SUBDUCTION_REPLICATOR}:`;
237
- return peerId.startsWith(automergePrefix) || peerId.startsWith(subductionPrefix);
244
+ const prefix = spaceId !== void 0 ? compositeKey(EdgeService.SUBDUCTION_REPLICATOR, spaceId) : `${EdgeService.SUBDUCTION_REPLICATOR}:`;
245
+ return peerId.startsWith(prefix);
238
246
  };
239
247
  //#endregion
240
248
  //#region src/echo-feed-codec.ts
241
249
  /** Property name for meta when object is serialized to JSON. Matches @dxos/echo/internal ATTR_META. */
242
250
  var ATTR_META = "@meta";
251
+ /** Meta keys a store stamps on decode and strips on encode: they describe the block, not the object. */
252
+ var FEED_KEY_SOURCES = [FeedProtocol.KEY_QUEUE_POSITION, FeedProtocol.KEY_FEED_BLOCK];
243
253
  /**
244
254
  * Codec for ECHO objects in feed block payload: JSON object ↔ UTF-8 bytes.
245
- * Encodes with queue position stripped; decodes with optional position injection.
255
+ * Encodes with the block's own keys stripped; decodes with them stamped into `@meta`.
246
256
  */
247
257
  var EchoFeedCodec = class EchoFeedCodec {
248
258
  static #encoder = new TextEncoder();
@@ -253,8 +263,7 @@ var EchoFeedCodec = class EchoFeedCodec {
253
263
  * last-write-wins merge at the index (see EntityMetaIndex.update).
254
264
  */
255
265
  static encode(value) {
256
- const prepared = EchoFeedCodec.stripQueuePosition(value);
257
- return EchoFeedCodec.#encoder.encode(JSON.stringify(prepared));
266
+ return EchoFeedCodec.#encoder.encode(JSON.stringify(EchoFeedCodec.stripFeedKeys(value)));
258
267
  }
259
268
  /**
260
269
  * Decodes feed block bytes to a JSON value.
@@ -262,33 +271,77 @@ var EchoFeedCodec = class EchoFeedCodec {
262
271
  */
263
272
  static decode(data, position) {
264
273
  const decoded = JSON.parse(EchoFeedCodec.#decoder.decode(data));
265
- if (position !== void 0 && typeof decoded === "object" && decoded !== null) EchoFeedCodec.#setQueuePosition(decoded, position);
274
+ if (position !== void 0 && typeof decoded === "object" && decoded !== null) EchoFeedCodec.#setKey(decoded, FeedProtocol.KEY_QUEUE_POSITION, position.toString());
266
275
  return decoded;
267
276
  }
268
277
  /**
269
- * Strips the queue-position foreign key from an object's metadata, producing a canonical form
270
- * comparable across a local snapshot and an inbound feed block (positions differ per-append).
278
+ * Decodes a stored block, stamping its identity (and position, once it has one) into `@meta` so a
279
+ * reader can tell blocks apart and order them without comparing their content.
271
280
  */
272
- static stripQueuePosition(value) {
273
- if (typeof value !== "object" || value === null) return value;
274
- const obj = structuredClone(value);
275
- const meta = obj[ATTR_META];
276
- if (meta?.keys?.some((key) => key.source === FeedProtocol.KEY_QUEUE_POSITION)) meta.keys = meta.keys.filter((key) => key.source !== FeedProtocol.KEY_QUEUE_POSITION);
277
- return obj;
281
+ static decodeBlock(block) {
282
+ const decoded = EchoFeedCodec.decode(block.data, block.position ?? void 0);
283
+ if (typeof decoded === "object" && decoded !== null) EchoFeedCodec.#setKey(decoded, FeedProtocol.KEY_FEED_BLOCK, EchoFeedCodec.blockId(block.actorId, block.sequence));
284
+ return decoded;
278
285
  }
279
- static #setQueuePosition(obj, position) {
280
- obj[ATTR_META] ??= { keys: [] };
281
- obj[ATTR_META].keys ??= [];
282
- const keys = obj[ATTR_META].keys;
283
- for (let i = 0; i < keys.length; i++) if (keys[i].source === FeedProtocol.KEY_QUEUE_POSITION) {
284
- keys.splice(i, 1);
285
- i--;
286
- }
287
- keys.push({
288
- source: FeedProtocol.KEY_QUEUE_POSITION,
289
- id: position.toString()
290
- });
286
+ /**
287
+ * The id stamped for a block. The sequence comes first: it is always digits, so the first separator
288
+ * splits the id even when the actor id contains one.
289
+ */
290
+ static blockId(actorId, sequence) {
291
+ return `${sequence}@${actorId}`;
291
292
  }
293
+ /** Reads the block reference {@link decodeBlock} (or {@link decode} with a position) stamped. */
294
+ static blockOf(value) {
295
+ return EchoFeedCodec.blockOfKeys(metaKeysOf(value));
296
+ }
297
+ /** Reads the block reference from an object's foreign keys. */
298
+ static blockOfKeys(keys) {
299
+ const ref = {};
300
+ for (const key of keys) if (key.source === FeedProtocol.KEY_QUEUE_POSITION) {
301
+ const position = Number(key.id);
302
+ if (Number.isFinite(position)) ref.position = position;
303
+ } else if (key.source === FeedProtocol.KEY_FEED_BLOCK) Object.assign(ref, parseBlockId(key.id));
304
+ return ref;
305
+ }
306
+ /**
307
+ * Returns `value` without the keys a store stamps on decode, so re-appending a decoded object does
308
+ * not carry its old block's identity into the new block. Copies only the containers it changes.
309
+ */
310
+ static stripFeedKeys(value) {
311
+ const meta = value[ATTR_META];
312
+ const keys = metaKeysOf(value);
313
+ if (typeof meta !== "object" || meta === null || !keys.some((key) => FEED_KEY_SOURCES.includes(key.source))) return value;
314
+ return {
315
+ ...value,
316
+ [ATTR_META]: {
317
+ ...meta,
318
+ keys: keys.filter((key) => !FEED_KEY_SOURCES.includes(key.source))
319
+ }
320
+ };
321
+ }
322
+ static #setKey(obj, source, id) {
323
+ const meta = obj[ATTR_META];
324
+ obj[ATTR_META] = {
325
+ ...typeof meta === "object" && meta !== null ? meta : {},
326
+ keys: [...metaKeysOf(obj).filter((key) => key.source !== source), {
327
+ source,
328
+ id
329
+ }]
330
+ };
331
+ }
332
+ };
333
+ var metaKeysOf = (value) => {
334
+ const meta = value[ATTR_META];
335
+ const keys = typeof meta === "object" && meta !== null && "keys" in meta ? meta.keys : void 0;
336
+ return Array.isArray(keys) ? keys : [];
337
+ };
338
+ var parseBlockId = (id) => {
339
+ const separator = id.indexOf("@");
340
+ const sequence = Number(id.slice(0, separator));
341
+ return separator > 0 && Number.isInteger(sequence) ? {
342
+ sequence,
343
+ actorId: id.slice(separator + 1)
344
+ } : {};
292
345
  };
293
346
  /**
294
347
  * Foreign-key source for the global position a feed block was assigned.
@@ -315,6 +368,8 @@ var ast_exports = /* @__PURE__ */ __exportAll({
315
368
  FeedScope: () => FeedScope,
316
369
  Filter: () => Filter,
317
370
  FilterAnd: () => FilterAnd,
371
+ FilterAnnotation: () => FilterAnnotation,
372
+ FilterChanges: () => FilterChanges,
318
373
  FilterChildOf: () => FilterChildOf,
319
374
  FilterCompare: () => FilterCompare,
320
375
  FilterContains: () => FilterContains,
@@ -322,6 +377,7 @@ var ast_exports = /* @__PURE__ */ __exportAll({
322
377
  FilterHasParent: () => FilterHasParent,
323
378
  FilterIn: () => FilterIn,
324
379
  FilterInQuery: () => FilterInQuery,
380
+ FilterMnemonic: () => FilterMnemonic,
325
381
  FilterNot: () => FilterNot,
326
382
  FilterObject: () => FilterObject,
327
383
  FilterOr: () => FilterOr,
@@ -354,6 +410,7 @@ var ast_exports = /* @__PURE__ */ __exportAll({
354
410
  Scope: () => Scope,
355
411
  SpaceScope: () => SpaceScope,
356
412
  fold: () => fold,
413
+ isGroupKeyAggregate: () => isGroupKeyAggregate,
357
414
  map: () => map,
358
415
  visit: () => visit
359
416
  });
@@ -414,6 +471,15 @@ var FilterTag = Schema.Struct({
414
471
  type: Schema.Literal("tag"),
415
472
  tag: Schema.String
416
473
  });
474
+ var FilterAnnotation = Schema.Struct({
475
+ type: Schema.Literal("annotation"),
476
+ key: Schema.String,
477
+ value: Schema.optional(Schema.Union([
478
+ Schema.String,
479
+ Schema.Number,
480
+ Schema.Boolean
481
+ ]))
482
+ });
417
483
  var FilterRange = Schema.Struct({
418
484
  type: Schema.Literal("range"),
419
485
  from: Schema.Any,
@@ -466,6 +532,15 @@ var FilterHasParent = Schema.Struct({
466
532
  /** True matches objects with a parent; false matches unparented (root) objects. */
467
533
  value: Schema.Boolean
468
534
  });
535
+ var FilterMnemonic = Schema.Struct({
536
+ type: Schema.Literal("mnemonic"),
537
+ /** Normalized (uppercase) mnemonic. */
538
+ mnemonic: Schema.String
539
+ });
540
+ var FilterChanges = Schema.Struct({
541
+ type: Schema.Literal("changes"),
542
+ targets: Schema.optional(Schema.Array(EID.Schema))
543
+ });
469
544
  /**
470
545
  * Union of filters.
471
546
  */
@@ -476,12 +551,15 @@ var Filter = Schema.Union([
476
551
  FilterInQuery,
477
552
  FilterContains,
478
553
  FilterTag,
554
+ FilterAnnotation,
479
555
  FilterRange,
480
556
  FilterTimestamp,
481
557
  FilterFeedCursor,
482
558
  FilterTextSearch,
483
559
  FilterChildOf,
484
560
  FilterHasParent,
561
+ FilterMnemonic,
562
+ FilterChanges,
485
563
  FilterNot,
486
564
  FilterAnd,
487
565
  FilterOr
@@ -607,6 +685,11 @@ var QuerySkipClause = Schema.Struct({
607
685
  * ordering, independent of any `orderBy` clause elsewhere in the query (which orders the whole
608
686
  * input stream / the resulting groups, not this aggregate's member selection).
609
687
  * - `count` yields the member count. Opt-in — a row carries no count otherwise.
688
+ * - `type` partitions members by their type URI; the field carries the URI string.
689
+ * - `timestamp` partitions members by the UTC hour or day a system timestamp falls in; the field
690
+ * carries the start of that interval in unix ms.
691
+ * - `time` is `timestamp` over a unix-ms member `property` instead of a system timestamp.
692
+ * - `sum` adds a numeric member `property`; non-numeric values count as 0.
610
693
  */
611
694
  var GroupAggregateGroup_ = Schema.Struct({
612
695
  name: Schema.String,
@@ -639,13 +722,41 @@ var GroupAggregateCount_ = Schema.Struct({
639
722
  name: Schema.String,
640
723
  kind: Schema.Literal("count")
641
724
  });
642
- var GroupAggregate = Schema.Union([
725
+ var GroupAggregateType_ = Schema.Struct({
726
+ name: Schema.String,
727
+ kind: Schema.Literal("type")
728
+ });
729
+ var GroupAggregateTimestamp_ = Schema.Struct({
730
+ name: Schema.String,
731
+ kind: Schema.Literal("timestamp"),
732
+ field: Schema.Literals(["createdAt", "updatedAt"]),
733
+ unit: Schema.Literals(["hour", "day"])
734
+ });
735
+ var GroupAggregateTime_ = Schema.Struct({
736
+ name: Schema.String,
737
+ kind: Schema.Literal("time"),
738
+ property: Schema.String,
739
+ unit: Schema.Literals(["hour", "day"])
740
+ });
741
+ var GroupAggregateSum_ = Schema.Struct({
742
+ name: Schema.String,
743
+ kind: Schema.Literal("sum"),
744
+ property: Schema.String
745
+ });
746
+ var GroupAggregate_ = Schema.Union([
643
747
  GroupAggregateGroup_,
644
748
  GroupAggregateMax_,
645
749
  GroupAggregateMin_,
646
750
  GroupAggregateItems_,
647
- GroupAggregateCount_
751
+ GroupAggregateCount_,
752
+ GroupAggregateType_,
753
+ GroupAggregateTimestamp_,
754
+ GroupAggregateTime_,
755
+ GroupAggregateSum_
648
756
  ]);
757
+ /** Aggregate kinds that contribute a component to the group key. */
758
+ var isGroupKeyAggregate = (aggregate) => aggregate.kind === "group" || aggregate.kind === "type" || aggregate.kind === "timestamp" || aggregate.kind === "time";
759
+ var GroupAggregate = GroupAggregate_;
649
760
  var QueryAggregateClause = Schema.Struct({
650
761
  type: Schema.Literal("aggregate"),
651
762
  query: Schema.suspend(() => Query),