@storacha/md-merge 0.2.1 → 0.3.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/dist/codec.d.ts CHANGED
@@ -1,29 +1,18 @@
1
1
  /**
2
2
  * DAG-CBOR serialization for RGATree and RGAChangeSet.
3
3
  *
4
- * Converts RGA class instances (with Maps, nested RGAs) to plain
5
- * CBOR-friendly objects and back.
4
+ * Tree-specific serialization wraps the generic RGA codec from crdt/codec.ts,
5
+ * adding recursive handling of nested RGA children in tree nodes.
6
6
  */
7
7
  import { type RGAEvent, type EventComparator } from './crdt/rga.js';
8
8
  import type { RGATreeRoot, RGATreeNode } from './types.js';
9
9
  import type { RGAChangeSet } from './types.js';
10
- /**
11
- * Encode an RGATree as a DAG-CBOR block.
12
- */
10
+ export { encodeRGA, decodeRGA } from './crdt/codec.js';
13
11
  export declare function encodeTree<E extends RGAEvent>(root: RGATreeRoot<E>): Promise<import("multiformats").BlockView<unknown, 113, 18, 1>>;
14
- /**
15
- * Decode an RGATree from a DAG-CBOR block.
16
- */
17
12
  export declare function decodeTree<E extends RGAEvent>(block: {
18
13
  bytes: Uint8Array;
19
14
  }, parseEvent: (s: string) => E, fingerprintFn: (value: RGATreeNode<E>) => string, compareEvents: EventComparator<E>): Promise<RGATreeRoot<E>>;
20
- /**
21
- * Encode an RGAChangeSet as a DAG-CBOR block.
22
- */
23
15
  export declare function encodeChangeSet<E extends RGAEvent>(cs: RGAChangeSet<E>): Promise<import("multiformats").BlockView<unknown, 113, 18, 1>>;
24
- /**
25
- * Decode an RGAChangeSet from a DAG-CBOR block.
26
- */
27
16
  export declare function decodeChangeSet<E extends RGAEvent>(block: {
28
17
  bytes: Uint8Array;
29
18
  }, parseEvent: (s: string) => E): Promise<RGAChangeSet<E>>;
package/dist/codec.js CHANGED
@@ -1,36 +1,17 @@
1
1
  /**
2
2
  * DAG-CBOR serialization for RGATree and RGAChangeSet.
3
3
  *
4
- * Converts RGA class instances (with Maps, nested RGAs) to plain
5
- * CBOR-friendly objects and back.
4
+ * Tree-specific serialization wraps the generic RGA codec from crdt/codec.ts,
5
+ * adding recursive handling of nested RGA children in tree nodes.
6
6
  */
7
7
  import { encode, decode } from 'multiformats/block';
8
8
  import { sha256 } from 'multiformats/hashes/sha2';
9
9
  import * as cbor from '@ipld/dag-cbor';
10
10
  import { RGA } from './crdt/rga.js';
11
- /** Recursively strip `undefined` values (not IPLD-compatible). */
12
- function stripUndefined(obj) {
13
- if (obj === null || obj === undefined)
14
- return null;
15
- if (Array.isArray(obj))
16
- return obj.map(stripUndefined);
17
- if (typeof obj === 'object') {
18
- const clean = {};
19
- for (const [k, v] of Object.entries(obj)) {
20
- if (v !== undefined)
21
- clean[k] = stripUndefined(v);
22
- }
23
- return clean;
24
- }
25
- return obj;
26
- }
27
- // ---- Helpers ----
28
- function serializeNodeId(id) {
29
- return { uuid: id.uuid, event: id.event.toString() };
30
- }
31
- function deserializeNodeId(raw, parseEvent) {
32
- return { uuid: raw.uuid, event: parseEvent(raw.event) };
33
- }
11
+ import { stripUndefined, serializeNodeId, deserializeNodeId, serializeRGA, deserializeRGA, } from './crdt/codec.js';
12
+ // Re-export plain RGA codec
13
+ export { encodeRGA, decodeRGA } from './crdt/codec.js';
14
+ // ---- Tree-specific helpers ----
34
15
  function isRGAParentSerialized(node) {
35
16
  return (node != null &&
36
17
  typeof node === 'object' &&
@@ -43,59 +24,38 @@ function isRGAParentNode(node) {
43
24
  return 'children' in node && node.children instanceof RGA;
44
25
  }
45
26
  // ---- RGATree serialization ----
46
- function serializeRGA(rga) {
47
- const nodes = [];
48
- for (const node of rga.nodes.values()) {
49
- nodes.push({
50
- id: serializeNodeId(node.id),
51
- value: serializeTreeNodeValue(node.value),
52
- afterId: node.afterId ? serializeNodeId(node.afterId) : null,
53
- tombstone: node.tombstone,
54
- });
55
- }
56
- return { nodes };
57
- }
58
27
  function serializeTreeNodeValue(node) {
59
28
  if (isRGAParentNode(node)) {
60
29
  const { children, ...rest } = node;
61
- return { ...rest, children: serializeRGA(children) };
30
+ return { ...rest, children: serializeRGA(children, (n) => serializeTreeNodeValue(n)) };
62
31
  }
63
32
  return node;
64
33
  }
65
34
  function serializeTree(root) {
66
35
  return {
67
36
  type: 'root',
68
- children: serializeRGA(root.children),
37
+ children: serializeRGA(root.children, (n) => serializeTreeNodeValue(n)),
69
38
  };
70
39
  }
71
- function deserializeRGA(raw, parseEvent, fingerprintFn, compareEvents) {
72
- const rga = new RGA(fingerprintFn, compareEvents);
73
- for (const rawNode of raw.nodes) {
74
- const id = deserializeNodeId(rawNode.id, parseEvent);
75
- const afterId = rawNode.afterId ? deserializeNodeId(rawNode.afterId, parseEvent) : undefined;
76
- const value = deserializeTreeNodeValue(rawNode.value, parseEvent, fingerprintFn, compareEvents);
77
- const key = `${id.uuid}:${id.event.toString()}`;
78
- rga.nodes.set(key, { id, value, afterId, tombstone: rawNode.tombstone });
79
- }
80
- return rga;
81
- }
82
40
  function deserializeTreeNodeValue(raw, parseEvent, fingerprintFn, compareEvents) {
83
41
  if (isRGAParentSerialized(raw)) {
84
42
  const { children, ...rest } = raw;
85
43
  return {
86
44
  ...rest,
87
- children: deserializeRGA(children, parseEvent, fingerprintFn, compareEvents),
45
+ children: deserializeTreeRGA(children, parseEvent, fingerprintFn, compareEvents),
88
46
  };
89
47
  }
90
48
  return raw;
91
49
  }
50
+ function deserializeTreeRGA(raw, parseEvent, fingerprintFn, compareEvents) {
51
+ return deserializeRGA(raw, parseEvent, (v) => deserializeTreeNodeValue(v, parseEvent, fingerprintFn, compareEvents), fingerprintFn, compareEvents);
52
+ }
92
53
  function deserializeTree(raw, parseEvent, fingerprintFn, compareEvents) {
93
54
  return {
94
55
  type: 'root',
95
- children: deserializeRGA(raw.children, parseEvent, fingerprintFn, compareEvents),
56
+ children: deserializeTreeRGA(raw.children, parseEvent, fingerprintFn, compareEvents),
96
57
  };
97
58
  }
98
- // ---- RGAChangeSet serialization ----
99
59
  function serializeChangeSet(cs) {
100
60
  return {
101
61
  event: cs.event.toString(),
@@ -130,30 +90,18 @@ function deserializeChangeSet(raw, parseEvent) {
130
90
  };
131
91
  }
132
92
  // ---- Public API: encode/decode to DAG-CBOR blocks ----
133
- /**
134
- * Encode an RGATree as a DAG-CBOR block.
135
- */
136
93
  export async function encodeTree(root) {
137
94
  const value = stripUndefined(serializeTree(root));
138
95
  return encode({ value, codec: cbor, hasher: sha256 });
139
96
  }
140
- /**
141
- * Decode an RGATree from a DAG-CBOR block.
142
- */
143
97
  export async function decodeTree(block, parseEvent, fingerprintFn, compareEvents) {
144
98
  const decoded = await decode({ bytes: block.bytes, codec: cbor, hasher: sha256 });
145
99
  return deserializeTree(decoded.value, parseEvent, fingerprintFn, compareEvents);
146
100
  }
147
- /**
148
- * Encode an RGAChangeSet as a DAG-CBOR block.
149
- */
150
101
  export async function encodeChangeSet(cs) {
151
102
  const value = stripUndefined(serializeChangeSet(cs));
152
103
  return encode({ value, codec: cbor, hasher: sha256 });
153
104
  }
154
- /**
155
- * Decode an RGAChangeSet from a DAG-CBOR block.
156
- */
157
105
  export async function decodeChangeSet(block, parseEvent) {
158
106
  const decoded = await decode({ bytes: block.bytes, codec: cbor, hasher: sha256 });
159
107
  return deserializeChangeSet(decoded.value, parseEvent);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * DAG-CBOR serialization for plain RGA instances.
3
+ */
4
+ import { RGA, type RGANodeId, type RGAEvent, type EventComparator } from './rga.js';
5
+ /** Recursively strip `undefined` values (not IPLD-compatible). */
6
+ export declare function stripUndefined(obj: unknown): unknown;
7
+ export interface SerializedNodeId {
8
+ uuid: string;
9
+ event: string;
10
+ }
11
+ export interface SerializedRGANode<V = unknown> {
12
+ id: SerializedNodeId;
13
+ value: V;
14
+ afterId: SerializedNodeId | null;
15
+ tombstone: boolean;
16
+ }
17
+ export interface SerializedRGA<V = unknown> {
18
+ nodes: SerializedRGANode<V>[];
19
+ }
20
+ export declare function serializeNodeId<E extends RGAEvent>(id: RGANodeId<E>): SerializedNodeId;
21
+ export declare function deserializeNodeId<E extends RGAEvent>(raw: SerializedNodeId, parseEvent: (s: string) => E): RGANodeId<E>;
22
+ export declare function serializeRGA<T, E extends RGAEvent>(rga: RGA<T, E>, serializeValue?: (v: T) => unknown): SerializedRGA;
23
+ export declare function deserializeRGA<T, E extends RGAEvent>(raw: SerializedRGA, parseEvent: (s: string) => E, deserializeValue: (v: unknown) => T, fingerprintFn: (value: T) => string, compareEvents: EventComparator<E>): RGA<T, E>;
24
+ /**
25
+ * Encode a plain RGA as a DAG-CBOR block.
26
+ */
27
+ export declare function encodeRGA<T, E extends RGAEvent>(rga: RGA<T, E>, serializeValue?: (v: T) => unknown): Promise<import("multiformats").BlockView<unknown, 113, 18, 1>>;
28
+ /**
29
+ * Decode a plain RGA from a DAG-CBOR block.
30
+ */
31
+ export declare function decodeRGA<T, E extends RGAEvent>(block: {
32
+ bytes: Uint8Array;
33
+ }, parseEvent: (s: string) => E, deserializeValue: (v: unknown) => T, fingerprintFn: (value: T) => string, compareEvents: EventComparator<E>): Promise<RGA<T, E>>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * DAG-CBOR serialization for plain RGA instances.
3
+ */
4
+ import { encode, decode } from 'multiformats/block';
5
+ import { sha256 } from 'multiformats/hashes/sha2';
6
+ import * as cbor from '@ipld/dag-cbor';
7
+ import { RGA } from './rga.js';
8
+ /** Recursively strip `undefined` values (not IPLD-compatible). */
9
+ export function stripUndefined(obj) {
10
+ if (obj === null || obj === undefined)
11
+ return null;
12
+ if (Array.isArray(obj))
13
+ return obj.map(stripUndefined);
14
+ if (typeof obj === 'object') {
15
+ const clean = {};
16
+ for (const [k, v] of Object.entries(obj)) {
17
+ if (v !== undefined)
18
+ clean[k] = stripUndefined(v);
19
+ }
20
+ return clean;
21
+ }
22
+ return obj;
23
+ }
24
+ // ---- Helpers ----
25
+ export function serializeNodeId(id) {
26
+ return { uuid: id.uuid, event: id.event.toString() };
27
+ }
28
+ export function deserializeNodeId(raw, parseEvent) {
29
+ return { uuid: raw.uuid, event: parseEvent(raw.event) };
30
+ }
31
+ // ---- Serialize / Deserialize ----
32
+ export function serializeRGA(rga, serializeValue = (v) => v) {
33
+ const nodes = [];
34
+ for (const node of rga.nodes.values()) {
35
+ nodes.push({
36
+ id: serializeNodeId(node.id),
37
+ value: serializeValue(node.value),
38
+ afterId: node.afterId ? serializeNodeId(node.afterId) : null,
39
+ tombstone: node.tombstone,
40
+ });
41
+ }
42
+ return { nodes };
43
+ }
44
+ export function deserializeRGA(raw, parseEvent, deserializeValue, fingerprintFn, compareEvents) {
45
+ const rga = new RGA(fingerprintFn, compareEvents);
46
+ for (const rawNode of raw.nodes) {
47
+ const id = deserializeNodeId(rawNode.id, parseEvent);
48
+ const afterId = rawNode.afterId ? deserializeNodeId(rawNode.afterId, parseEvent) : undefined;
49
+ const value = deserializeValue(rawNode.value);
50
+ const key = `${id.uuid}:${id.event.toString()}`;
51
+ rga.nodes.set(key, { id, value, afterId, tombstone: rawNode.tombstone });
52
+ }
53
+ return rga;
54
+ }
55
+ // ---- Public API: encode/decode to DAG-CBOR blocks ----
56
+ /**
57
+ * Encode a plain RGA as a DAG-CBOR block.
58
+ */
59
+ export async function encodeRGA(rga, serializeValue = (v) => v) {
60
+ const value = stripUndefined(serializeRGA(rga, serializeValue));
61
+ return encode({ value, codec: cbor, hasher: sha256 });
62
+ }
63
+ /**
64
+ * Decode a plain RGA from a DAG-CBOR block.
65
+ */
66
+ export async function decodeRGA(block, parseEvent, deserializeValue, fingerprintFn, compareEvents) {
67
+ const decoded = await decode({ bytes: block.bytes, codec: cbor, hasher: sha256 });
68
+ return deserializeRGA(decoded.value, parseEvent, deserializeValue, fingerprintFn, compareEvents);
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storacha/md-merge",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/codec.ts CHANGED
@@ -1,76 +1,30 @@
1
1
  /**
2
2
  * DAG-CBOR serialization for RGATree and RGAChangeSet.
3
3
  *
4
- * Converts RGA class instances (with Maps, nested RGAs) to plain
5
- * CBOR-friendly objects and back.
4
+ * Tree-specific serialization wraps the generic RGA codec from crdt/codec.ts,
5
+ * adding recursive handling of nested RGA children in tree nodes.
6
6
  */
7
7
 
8
8
  import { encode, decode } from 'multiformats/block'
9
9
  import { sha256 } from 'multiformats/hashes/sha2'
10
10
  import * as cbor from '@ipld/dag-cbor'
11
- import { RGA, type RGANodeId, type RGANode, type RGAEvent, type EventComparator } from './crdt/rga.js'
11
+ import { RGA, type RGAEvent, type EventComparator } from './crdt/rga.js'
12
+ import {
13
+ stripUndefined,
14
+ serializeNodeId,
15
+ deserializeNodeId,
16
+ serializeRGA,
17
+ deserializeRGA,
18
+ type SerializedRGA,
19
+ type SerializedNodeId,
20
+ } from './crdt/codec.js'
12
21
  import type { RGATreeRoot, RGATreeNode, RGAParentNode } from './types.js'
13
22
  import type { RGAChangeSet, RGAChange } from './types.js'
14
23
 
24
+ // Re-export plain RGA codec
25
+ export { encodeRGA, decodeRGA } from './crdt/codec.js'
15
26
 
16
- /** Recursively strip `undefined` values (not IPLD-compatible). */
17
- function stripUndefined(obj: unknown): unknown {
18
- if (obj === null || obj === undefined) return null
19
- if (Array.isArray(obj)) return obj.map(stripUndefined)
20
- if (typeof obj === 'object') {
21
- const clean: Record<string, unknown> = {}
22
- for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
23
- if (v !== undefined) clean[k] = stripUndefined(v)
24
- }
25
- return clean
26
- }
27
- return obj
28
- }
29
-
30
- // ---- Serializable shapes (plain objects, no class instances) ----
31
-
32
- interface SerializedRGANode {
33
- id: { uuid: string; event: string }
34
- value: unknown
35
- afterId: { uuid: string; event: string } | null
36
- tombstone: boolean
37
- }
38
-
39
- interface SerializedRGA {
40
- nodes: SerializedRGANode[]
41
- }
42
-
43
- interface SerializedRGATreeRoot {
44
- type: 'root'
45
- children: SerializedRGA
46
- }
47
-
48
- interface SerializedRGAChangeSet {
49
- event: string
50
- changes: SerializedRGAChange[]
51
- }
52
-
53
- interface SerializedRGAChange {
54
- type: 'insert' | 'delete' | 'modify'
55
- parentPath: Array<{ uuid: string; event: string }>
56
- targetId?: { uuid: string; event: string } | null
57
- afterId?: { uuid: string; event: string } | null
58
- nodes?: unknown[]
59
- before?: unknown[]
60
- }
61
-
62
- // ---- Helpers ----
63
-
64
- function serializeNodeId<E extends RGAEvent>(id: RGANodeId<E>): { uuid: string; event: string } {
65
- return { uuid: id.uuid, event: id.event.toString() }
66
- }
67
-
68
- function deserializeNodeId<E extends RGAEvent>(
69
- raw: { uuid: string; event: string },
70
- parseEvent: (s: string) => E,
71
- ): RGANodeId<E> {
72
- return { uuid: raw.uuid, event: parseEvent(raw.event) }
73
- }
27
+ // ---- Tree-specific helpers ----
74
28
 
75
29
  function isRGAParentSerialized(node: unknown): node is { type: string; children: SerializedRGA } {
76
30
  return (
@@ -89,49 +43,19 @@ function isRGAParentNode<E extends RGAEvent>(node: RGATreeNode<E>): node is RGAP
89
43
 
90
44
  // ---- RGATree serialization ----
91
45
 
92
- function serializeRGA<E extends RGAEvent>(rga: RGA<RGATreeNode<E>, E>): SerializedRGA {
93
- const nodes: SerializedRGANode[] = []
94
- for (const node of rga.nodes.values()) {
95
- nodes.push({
96
- id: serializeNodeId(node.id),
97
- value: serializeTreeNodeValue(node.value),
98
- afterId: node.afterId ? serializeNodeId(node.afterId) : null,
99
- tombstone: node.tombstone,
100
- })
101
- }
102
- return { nodes }
103
- }
104
-
105
46
  function serializeTreeNodeValue<E extends RGAEvent>(node: RGATreeNode<E>): unknown {
106
47
  if (isRGAParentNode(node)) {
107
48
  const { children, ...rest } = node
108
- return { ...rest, children: serializeRGA(children) }
49
+ return { ...rest, children: serializeRGA(children, (n: RGATreeNode<E>) => serializeTreeNodeValue(n)) }
109
50
  }
110
51
  return node
111
52
  }
112
53
 
113
- function serializeTree<E extends RGAEvent>(root: RGATreeRoot<E>): SerializedRGATreeRoot {
54
+ function serializeTree<E extends RGAEvent>(root: RGATreeRoot<E>): unknown {
114
55
  return {
115
56
  type: 'root',
116
- children: serializeRGA(root.children),
117
- }
118
- }
119
-
120
- function deserializeRGA<E extends RGAEvent>(
121
- raw: SerializedRGA,
122
- parseEvent: (s: string) => E,
123
- fingerprintFn: (value: RGATreeNode<E>) => string,
124
- compareEvents: EventComparator<E>,
125
- ): RGA<RGATreeNode<E>, E> {
126
- const rga = new RGA<RGATreeNode<E>, E>(fingerprintFn, compareEvents)
127
- for (const rawNode of raw.nodes) {
128
- const id = deserializeNodeId(rawNode.id, parseEvent)
129
- const afterId = rawNode.afterId ? deserializeNodeId(rawNode.afterId, parseEvent) : undefined
130
- const value = deserializeTreeNodeValue(rawNode.value, parseEvent, fingerprintFn, compareEvents)
131
- const key = `${id.uuid}:${id.event.toString()}`
132
- rga.nodes.set(key, { id, value, afterId, tombstone: rawNode.tombstone })
57
+ children: serializeRGA(root.children, (n: RGATreeNode<E>) => serializeTreeNodeValue(n)),
133
58
  }
134
- return rga
135
59
  }
136
60
 
137
61
  function deserializeTreeNodeValue<E extends RGAEvent>(
@@ -144,26 +68,55 @@ function deserializeTreeNodeValue<E extends RGAEvent>(
144
68
  const { children, ...rest } = raw
145
69
  return {
146
70
  ...rest,
147
- children: deserializeRGA(children, parseEvent, fingerprintFn, compareEvents),
71
+ children: deserializeTreeRGA(children, parseEvent, fingerprintFn, compareEvents),
148
72
  } as RGAParentNode<E>
149
73
  }
150
74
  return raw as RGATreeNode<E>
151
75
  }
152
76
 
77
+ function deserializeTreeRGA<E extends RGAEvent>(
78
+ raw: SerializedRGA,
79
+ parseEvent: (s: string) => E,
80
+ fingerprintFn: (value: RGATreeNode<E>) => string,
81
+ compareEvents: EventComparator<E>,
82
+ ): RGA<RGATreeNode<E>, E> {
83
+ return deserializeRGA<RGATreeNode<E>, E>(
84
+ raw,
85
+ parseEvent,
86
+ (v) => deserializeTreeNodeValue(v, parseEvent, fingerprintFn, compareEvents),
87
+ fingerprintFn,
88
+ compareEvents,
89
+ )
90
+ }
91
+
153
92
  function deserializeTree<E extends RGAEvent>(
154
- raw: SerializedRGATreeRoot,
93
+ raw: { type: string; children: SerializedRGA },
155
94
  parseEvent: (s: string) => E,
156
95
  fingerprintFn: (value: RGATreeNode<E>) => string,
157
96
  compareEvents: EventComparator<E>,
158
97
  ): RGATreeRoot<E> {
159
98
  return {
160
99
  type: 'root',
161
- children: deserializeRGA(raw.children, parseEvent, fingerprintFn, compareEvents),
100
+ children: deserializeTreeRGA(raw.children, parseEvent, fingerprintFn, compareEvents),
162
101
  }
163
102
  }
164
103
 
165
104
  // ---- RGAChangeSet serialization ----
166
105
 
106
+ interface SerializedRGAChangeSet {
107
+ event: string
108
+ changes: SerializedRGAChange[]
109
+ }
110
+
111
+ interface SerializedRGAChange {
112
+ type: 'insert' | 'delete' | 'modify'
113
+ parentPath: SerializedNodeId[]
114
+ targetId?: SerializedNodeId | null
115
+ afterId?: SerializedNodeId | null
116
+ nodes?: unknown[]
117
+ before?: unknown[]
118
+ }
119
+
167
120
  function serializeChangeSet<E extends RGAEvent>(cs: RGAChangeSet<E>): SerializedRGAChangeSet {
168
121
  return {
169
122
  event: cs.event.toString(),
@@ -200,19 +153,11 @@ function deserializeChangeSet<E extends RGAEvent>(
200
153
 
201
154
  // ---- Public API: encode/decode to DAG-CBOR blocks ----
202
155
 
203
- /**
204
- * Encode an RGATree as a DAG-CBOR block.
205
- */
206
- export async function encodeTree<E extends RGAEvent>(
207
- root: RGATreeRoot<E>,
208
- ) {
209
- const value = stripUndefined(serializeTree(root)) as SerializedRGATreeRoot
156
+ export async function encodeTree<E extends RGAEvent>(root: RGATreeRoot<E>) {
157
+ const value = stripUndefined(serializeTree(root))
210
158
  return encode({ value, codec: cbor, hasher: sha256 })
211
159
  }
212
160
 
213
- /**
214
- * Decode an RGATree from a DAG-CBOR block.
215
- */
216
161
  export async function decodeTree<E extends RGAEvent>(
217
162
  block: { bytes: Uint8Array },
218
163
  parseEvent: (s: string) => E,
@@ -220,22 +165,14 @@ export async function decodeTree<E extends RGAEvent>(
220
165
  compareEvents: EventComparator<E>,
221
166
  ): Promise<RGATreeRoot<E>> {
222
167
  const decoded = await decode({ bytes: block.bytes, codec: cbor, hasher: sha256 })
223
- return deserializeTree(decoded.value as SerializedRGATreeRoot, parseEvent, fingerprintFn, compareEvents)
168
+ return deserializeTree(decoded.value as { type: string; children: SerializedRGA }, parseEvent, fingerprintFn, compareEvents)
224
169
  }
225
170
 
226
- /**
227
- * Encode an RGAChangeSet as a DAG-CBOR block.
228
- */
229
- export async function encodeChangeSet<E extends RGAEvent>(
230
- cs: RGAChangeSet<E>,
231
- ) {
232
- const value = stripUndefined(serializeChangeSet(cs)) as SerializedRGAChangeSet
171
+ export async function encodeChangeSet<E extends RGAEvent>(cs: RGAChangeSet<E>) {
172
+ const value = stripUndefined(serializeChangeSet(cs))
233
173
  return encode({ value, codec: cbor, hasher: sha256 })
234
174
  }
235
175
 
236
- /**
237
- * Decode an RGAChangeSet from a DAG-CBOR block.
238
- */
239
176
  export async function decodeChangeSet<E extends RGAEvent>(
240
177
  block: { bytes: Uint8Array },
241
178
  parseEvent: (s: string) => E,
@@ -0,0 +1,116 @@
1
+ /**
2
+ * DAG-CBOR serialization for plain RGA instances.
3
+ */
4
+
5
+ import { encode, decode } from 'multiformats/block'
6
+ import { sha256 } from 'multiformats/hashes/sha2'
7
+ import * as cbor from '@ipld/dag-cbor'
8
+ import { RGA, type RGANodeId, type RGAEvent, type EventComparator } from './rga.js'
9
+
10
+ /** Recursively strip `undefined` values (not IPLD-compatible). */
11
+ export function stripUndefined(obj: unknown): unknown {
12
+ if (obj === null || obj === undefined) return null
13
+ if (Array.isArray(obj)) return obj.map(stripUndefined)
14
+ if (typeof obj === 'object') {
15
+ const clean: Record<string, unknown> = {}
16
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
17
+ if (v !== undefined) clean[k] = stripUndefined(v)
18
+ }
19
+ return clean
20
+ }
21
+ return obj
22
+ }
23
+
24
+ // ---- Serializable shapes ----
25
+
26
+ export interface SerializedNodeId {
27
+ uuid: string
28
+ event: string
29
+ }
30
+
31
+ export interface SerializedRGANode<V = unknown> {
32
+ id: SerializedNodeId
33
+ value: V
34
+ afterId: SerializedNodeId | null
35
+ tombstone: boolean
36
+ }
37
+
38
+ export interface SerializedRGA<V = unknown> {
39
+ nodes: SerializedRGANode<V>[]
40
+ }
41
+
42
+ // ---- Helpers ----
43
+
44
+ export function serializeNodeId<E extends RGAEvent>(id: RGANodeId<E>): SerializedNodeId {
45
+ return { uuid: id.uuid, event: id.event.toString() }
46
+ }
47
+
48
+ export function deserializeNodeId<E extends RGAEvent>(
49
+ raw: SerializedNodeId,
50
+ parseEvent: (s: string) => E,
51
+ ): RGANodeId<E> {
52
+ return { uuid: raw.uuid, event: parseEvent(raw.event) }
53
+ }
54
+
55
+ // ---- Serialize / Deserialize ----
56
+
57
+ export function serializeRGA<T, E extends RGAEvent>(
58
+ rga: RGA<T, E>,
59
+ serializeValue: (v: T) => unknown = (v) => v,
60
+ ): SerializedRGA {
61
+ const nodes: SerializedRGANode[] = []
62
+ for (const node of rga.nodes.values()) {
63
+ nodes.push({
64
+ id: serializeNodeId(node.id),
65
+ value: serializeValue(node.value),
66
+ afterId: node.afterId ? serializeNodeId(node.afterId) : null,
67
+ tombstone: node.tombstone,
68
+ })
69
+ }
70
+ return { nodes }
71
+ }
72
+
73
+ export function deserializeRGA<T, E extends RGAEvent>(
74
+ raw: SerializedRGA,
75
+ parseEvent: (s: string) => E,
76
+ deserializeValue: (v: unknown) => T,
77
+ fingerprintFn: (value: T) => string,
78
+ compareEvents: EventComparator<E>,
79
+ ): RGA<T, E> {
80
+ const rga = new RGA<T, E>(fingerprintFn, compareEvents)
81
+ for (const rawNode of raw.nodes) {
82
+ const id = deserializeNodeId(rawNode.id, parseEvent)
83
+ const afterId = rawNode.afterId ? deserializeNodeId(rawNode.afterId, parseEvent) : undefined
84
+ const value = deserializeValue(rawNode.value)
85
+ const key = `${id.uuid}:${id.event.toString()}`
86
+ rga.nodes.set(key, { id, value, afterId, tombstone: rawNode.tombstone })
87
+ }
88
+ return rga
89
+ }
90
+
91
+ // ---- Public API: encode/decode to DAG-CBOR blocks ----
92
+
93
+ /**
94
+ * Encode a plain RGA as a DAG-CBOR block.
95
+ */
96
+ export async function encodeRGA<T, E extends RGAEvent>(
97
+ rga: RGA<T, E>,
98
+ serializeValue: (v: T) => unknown = (v) => v,
99
+ ) {
100
+ const value = stripUndefined(serializeRGA(rga, serializeValue))
101
+ return encode({ value, codec: cbor, hasher: sha256 })
102
+ }
103
+
104
+ /**
105
+ * Decode a plain RGA from a DAG-CBOR block.
106
+ */
107
+ export async function decodeRGA<T, E extends RGAEvent>(
108
+ block: { bytes: Uint8Array },
109
+ parseEvent: (s: string) => E,
110
+ deserializeValue: (v: unknown) => T,
111
+ fingerprintFn: (value: T) => string,
112
+ compareEvents: EventComparator<E>,
113
+ ): Promise<RGA<T, E>> {
114
+ const decoded = await decode({ bytes: block.bytes, codec: cbor, hasher: sha256 })
115
+ return deserializeRGA(decoded.value as SerializedRGA, parseEvent, deserializeValue, fingerprintFn, compareEvents)
116
+ }