@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 +3 -14
- package/dist/codec.js +13 -65
- package/dist/crdt/codec.d.ts +33 -0
- package/dist/crdt/codec.js +69 -0
- package/package.json +1 -1
- package/src/codec.ts +55 -118
- package/src/crdt/codec.ts +116 -0
package/dist/codec.d.ts
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DAG-CBOR serialization for RGATree and RGAChangeSet.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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:
|
|
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
package/src/codec.ts
CHANGED
|
@@ -1,76 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DAG-CBOR serialization for RGATree and RGAChangeSet.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
-
|
|
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>):
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
168
|
+
return deserializeTree(decoded.value as { type: string; children: SerializedRGA }, parseEvent, fingerprintFn, compareEvents)
|
|
224
169
|
}
|
|
225
170
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|