Skip to content

Commit ca6b564

Browse files
authored
Merge pull request #544 from streamich/hamt-crdt
HAMT CRDT implementation
2 parents 6571cd8 + f6107c8 commit ca6b564

File tree

31 files changed

+892
-50
lines changed

31 files changed

+892
-50
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
"arg": "^5.0.2",
8484
"hyperdyperid": "^1.2.0",
8585
"multibase": "^4.0.6",
86-
"thingies": "^1.17.0"
86+
"thingies": "^1.18.0"
8787
},
8888
"devDependencies": {
8989
"@automerge/automerge": "2.1.7",

src/json-pack/cbor/CborDecoderBase.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ export class CborDecoderBase<R extends IReader & IReaderResettable = IReader & I
1313
{
1414
public constructor(
1515
public reader: R = new Reader() as any,
16-
protected readonly keyDecoder: CachedUtf8Decoder = sharedCachedUtf8Decoder,
16+
public readonly keyDecoder: CachedUtf8Decoder = sharedCachedUtf8Decoder,
1717
) {}
1818

1919
public read(uint8: Uint8Array): PackValue {
2020
this.reader.reset(uint8);
2121
return this.val() as PackValue;
2222
}
2323

24-
/** @deprecated */
2524
public decode(uint8: Uint8Array): unknown {
2625
this.reader.reset(uint8);
2726
return this.val();

src/json-pack/cbor/__tests__/CborEncoderDag.spec.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Writer} from '../../../util/buffers/Writer';
22
import {CborEncoderDag} from '../CborEncoderDag';
33
import {CborDecoder} from '../CborDecoder';
44
import {JsonPackExtension} from '../../JsonPackExtension';
5+
import {CborDecoderDag} from '../CborDecoderDag';
56

67
const writer = new Writer(1);
78
const encoder = new CborEncoderDag(writer);
@@ -50,37 +51,50 @@ describe('only extension = 42 is permitted', () => {
5051
expect(val).toStrictEqual({a: 'a', b: 'b'});
5152
});
5253

53-
test('can encode CID using inlined custom class', () => {
54-
class CID {
55-
constructor(public readonly value: string) {}
54+
class CID {
55+
constructor(public readonly value: string) {}
56+
}
57+
class NotCID {
58+
constructor(public readonly value: string) {}
59+
}
60+
61+
class IpfsCborEncoder extends CborEncoderDag {
62+
public writeUnknown(val: unknown): void {
63+
if (val instanceof CID) this.writeTag(42, val.value);
64+
else throw new Error('Unknown value type');
5665
}
57-
const encoder = new (class extends CborEncoderDag {
58-
public writeUnknown(val: unknown): void {
59-
if (val instanceof CID) encoder.writeTag(42, val.value);
60-
else throw new Error('Unknown value type');
61-
}
62-
})();
66+
}
67+
68+
class IpfsCborDecoder extends CborDecoderDag {
69+
public readTagRaw(tag: number): CID | unknown {
70+
if (tag === 42) return new CID(this.val() as any);
71+
throw new Error('UNKNOWN_TAG');
72+
}
73+
}
74+
75+
test('can encode CID using inlined custom class', () => {
76+
const encoder = new IpfsCborEncoder();
6377
const encoded = encoder.encode({a: 'a', b: new JsonPackExtension(42, 'b')});
6478
const val = decoder.read(encoded);
6579
expect(val).toStrictEqual({a: 'a', b: new JsonPackExtension(42, 'b')});
6680
const encoded2 = encoder.encode({a: 'a', b: new CID('b')});
67-
const val2 = decoder.read(encoded2);
81+
const val2 = decoder.decode(encoded2);
6882
expect(val).toStrictEqual({a: 'a', b: new JsonPackExtension(42, 'b')});
83+
expect(val2).toStrictEqual({a: 'a', b: new JsonPackExtension(42, 'b')});
84+
});
85+
86+
test('can encode CID inside a nested array', () => {
87+
const encoder = new IpfsCborEncoder();
88+
const decoder = new IpfsCborDecoder();
89+
const cid = new CID('my-cid');
90+
const data = [1, [2, [3, cid, 4], 5], 6];
91+
const encoded = encoder.encode(data);
92+
const decoded = decoder.decode(encoded);
93+
expect(decoded).toStrictEqual(data);
6994
});
7095

7196
test('can throw on unknown custom class', () => {
72-
class CID {
73-
constructor(public readonly value: string) {}
74-
}
75-
class NotCID {
76-
constructor(public readonly value: string) {}
77-
}
78-
const encoder = new (class extends CborEncoderDag {
79-
public writeUnknown(val: unknown): void {
80-
if (val instanceof CID) encoder.writeTag(42, val.value);
81-
else throw new Error('Unknown value type');
82-
}
83-
})();
97+
const encoder = new IpfsCborEncoder();
8498
const encoded1 = encoder.encode({a: 'a', b: new CID('b')});
8599
expect(() => encoder.encode({a: 'a', b: new NotCID('b')})).toThrowError(new Error('Unknown value type'));
86100
});

src/json-pack/json/JsonDecoder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ export class JsonDecoder implements BinaryJsonDecoder {
203203
return this.readAny();
204204
}
205205

206+
public decode(uint8: Uint8Array): unknown {
207+
this.reader.reset(uint8);
208+
return this.readAny();
209+
}
210+
206211
public readAny(): PackValue {
207212
this.skipWhitespace();
208213
const reader = this.reader;

src/json-pack/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type PackArray = PackValue[] | readonly PackValue[];
1515
type PackObject = {[key: string]: PackValue} | Readonly<{[key: string]: PackValue}>;
1616

1717
export interface BinaryJsonEncoder {
18+
encode(value: unknown): Uint8Array;
1819
writer: IWriter & IWriterGrowable;
1920
writeAny(value: unknown): void;
2021
writeNull(): void;
@@ -52,6 +53,7 @@ export interface TlvBinaryJsonEncoder {
5253
}
5354

5455
export interface BinaryJsonDecoder {
56+
decode(uint8: Uint8Array): unknown;
5557
reader: IReader & IReaderResettable;
5658
read(uint8: Uint8Array): PackValue;
5759
}

src/json-pack/ubjson/UbjsonDecoder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export class UbjsonDecoder implements BinaryJsonDecoder {
1111
return this.readAny();
1212
}
1313

14+
public decode(uint8: Uint8Array): unknown {
15+
this.reader.reset(uint8);
16+
return this.readAny();
17+
}
18+
1419
public readAny(): PackValue {
1520
const reader = this.reader;
1621
const octet = reader.u8();

src/util/buffers/b.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const b = (...args: number[]) => new Uint8Array(args);

src/util/buffers/cmpUint8Array2.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const cmpUint8Array2 = (a: Uint8Array, b: Uint8Array): number => {
2+
const len1 = a.length;
3+
const len2 = b.length;
4+
const len = Math.min(len1, len2);
5+
for (let i = 0; i < len; i++) {
6+
const diffChar = a[i] - b[i];
7+
if (diffChar !== 0) return diffChar;
8+
}
9+
return len1 - len2;
10+
};

src/util/buffers/toBuf.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {encode} from './utf8/encode';
2+
3+
export const toBuf = (str: string): Uint8Array => {
4+
const maxLength = str.length * 4;
5+
const arr = new Uint8Array(maxLength);
6+
const strBufferLength = encode(arr, str, 0, maxLength);
7+
return arr.slice(0, strBufferLength);
8+
};

src/web3/adl/hamt-crdt/Hamt.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {Defer} from 'thingies/es2020/Defer';
2+
import {concurrency} from 'thingies/es2020/concurrencyDecorator';
3+
import {HamtFrame} from './HamtFrame';
4+
import * as hlc from '../../hlc';
5+
import {Cid} from '../../multiformats';
6+
import {sha256} from '../../crypto';
7+
import {toBuf} from '../../../util/buffers/toBuf';
8+
import type {CidCasStruct} from '../../store/cas/CidCasStruct';
9+
import type * as types from './types';
10+
11+
export interface HamtDependencies {
12+
cas: CidCasStruct;
13+
hlcs: hlc.HlcFactory;
14+
}
15+
16+
export class Hamt implements types.HamtApi {
17+
protected _root: HamtFrame;
18+
protected _dirty: boolean = false;
19+
protected _loading: Promise<void> | null = null;
20+
21+
public cid: Cid | null = null;
22+
public prev: Cid | null = null;
23+
public seq: number = 0;
24+
public ops: types.HamtOp[] = [];
25+
26+
constructor(protected readonly deps: HamtDependencies) {
27+
this._root = new HamtFrame(deps.cas, null);
28+
}
29+
30+
public hasChanges(): boolean {
31+
return this._dirty;
32+
}
33+
34+
// ------------------------------------------------------------------ HamtApi
35+
36+
public async load(cid: Cid): Promise<void> {
37+
this.cid = cid;
38+
const future = new Defer<void>();
39+
this._loading = future.promise;
40+
try {
41+
const [prev, seq, ops, data] = (await this.deps.cas.get(cid)) as types.HamtRootFrameDto;
42+
this.prev = prev;
43+
this.seq = seq;
44+
this._root.loadDto(data, null);
45+
future.resolve();
46+
} catch (err) {
47+
future.reject(err);
48+
} finally {
49+
this._loading = null;
50+
}
51+
return future.promise;
52+
}
53+
54+
@concurrency(1)
55+
public async put(key: Uint8Array | string, val: unknown): Promise<boolean> {
56+
if (this._loading) await this._loading;
57+
const hashedKey = await this._key(key);
58+
const id = this.deps.hlcs.inc();
59+
const idDto = hlc.toDto(id);
60+
const op: types.HamtOp = [hashedKey, val, idDto];
61+
const success = await this._root.put(op);
62+
if (success) this.ops.push(op);
63+
return success;
64+
}
65+
66+
public async get(key: Uint8Array | string): Promise<unknown | undefined> {
67+
if (this._loading) await this._loading;
68+
const hashedKey = await this._key(key);
69+
return await this._root.get(hashedKey);
70+
}
71+
72+
/** Convert any key to buffer and prefix with 4-byte hash. */
73+
protected async _key(key: Uint8Array | string): Promise<Uint8Array> {
74+
const keyBuf = typeof key === 'string' ? toBuf(key) : key;
75+
const hash = await sha256(keyBuf);
76+
const buf = new Uint8Array(4 + keyBuf.length);
77+
buf.set(hash.subarray(0, 4), 0);
78+
buf.set(keyBuf, 4);
79+
return buf;
80+
}
81+
82+
public async has(key: Uint8Array | string): Promise<boolean> {
83+
if (this._loading) await this._loading;
84+
return (await this.get(key)) !== undefined;
85+
}
86+
87+
public async del(key: Uint8Array | string): Promise<boolean> {
88+
if (this._loading) await this._loading;
89+
return await this.put(key, undefined);
90+
}
91+
92+
public async save(): Promise<[head: Cid, affected: Cid[]]> {
93+
const [, affected] = await this._root.saveChildren();
94+
const prev = this.cid;
95+
const seq = this.seq + 1;
96+
const frameDto = this._root.toDto();
97+
const dto: types.HamtRootFrameDto = [prev, seq, this.ops, frameDto];
98+
const cid = await this.deps.cas.put(dto);
99+
this.cid = cid;
100+
this.prev = prev;
101+
this.seq = seq;
102+
this.ops = [];
103+
affected.push(cid);
104+
return [cid, affected];
105+
}
106+
}

0 commit comments

Comments
 (0)