clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

498 lines
14 KiB

// nip77.ts
import { bytesToHex, hexToBytes } from "@noble/ciphers/utils";
import { sha256 } from "@noble/hashes/sha256";
var PROTOCOL_VERSION = 97;
var ID_SIZE = 32;
var FINGERPRINT_SIZE = 16;
var Mode = {
Skip: 0,
Fingerprint: 1,
IdList: 2
};
var WrappedBuffer = class {
_raw;
length;
constructor(buffer) {
if (typeof buffer === "number") {
this._raw = new Uint8Array(buffer);
this.length = 0;
} else if (buffer instanceof Uint8Array) {
this._raw = new Uint8Array(buffer);
this.length = buffer.length;
} else {
this._raw = new Uint8Array(512);
this.length = 0;
}
}
unwrap() {
return this._raw.subarray(0, this.length);
}
get capacity() {
return this._raw.byteLength;
}
extend(buf) {
if (buf instanceof WrappedBuffer)
buf = buf.unwrap();
if (typeof buf.length !== "number")
throw Error("bad length");
const targetSize = buf.length + this.length;
if (this.capacity < targetSize) {
const oldRaw = this._raw;
const newCapacity = Math.max(this.capacity * 2, targetSize);
this._raw = new Uint8Array(newCapacity);
this._raw.set(oldRaw);
}
this._raw.set(buf, this.length);
this.length += buf.length;
}
shift() {
const first = this._raw[0];
this._raw = this._raw.subarray(1);
this.length--;
return first;
}
shiftN(n = 1) {
const firstSubarray = this._raw.subarray(0, n);
this._raw = this._raw.subarray(n);
this.length -= n;
return firstSubarray;
}
};
function decodeVarInt(buf) {
let res = 0;
while (1) {
if (buf.length === 0)
throw Error("parse ends prematurely");
let byte = buf.shift();
res = res << 7 | byte & 127;
if ((byte & 128) === 0)
break;
}
return res;
}
function encodeVarInt(n) {
if (n === 0)
return new WrappedBuffer(new Uint8Array([0]));
let o = [];
while (n !== 0) {
o.push(n & 127);
n >>>= 7;
}
o.reverse();
for (let i = 0; i < o.length - 1; i++)
o[i] |= 128;
return new WrappedBuffer(new Uint8Array(o));
}
function getByte(buf) {
return getBytes(buf, 1)[0];
}
function getBytes(buf, n) {
if (buf.length < n)
throw Error("parse ends prematurely");
return buf.shiftN(n);
}
var Accumulator = class {
buf;
constructor() {
this.setToZero();
}
setToZero() {
this.buf = new Uint8Array(ID_SIZE);
}
add(otherBuf) {
let currCarry = 0, nextCarry = 0;
let p = new DataView(this.buf.buffer);
let po = new DataView(otherBuf.buffer);
for (let i = 0; i < 8; i++) {
let offset = i * 4;
let orig = p.getUint32(offset, true);
let otherV = po.getUint32(offset, true);
let next = orig;
next += currCarry;
next += otherV;
if (next > 4294967295)
nextCarry = 1;
p.setUint32(offset, next & 4294967295, true);
currCarry = nextCarry;
nextCarry = 0;
}
}
negate() {
let p = new DataView(this.buf.buffer);
for (let i = 0; i < 8; i++) {
let offset = i * 4;
p.setUint32(offset, ~p.getUint32(offset, true));
}
let one = new Uint8Array(ID_SIZE);
one[0] = 1;
this.add(one);
}
getFingerprint(n) {
let input = new WrappedBuffer();
input.extend(this.buf);
input.extend(encodeVarInt(n));
let hash = sha256(input.unwrap());
return hash.subarray(0, FINGERPRINT_SIZE);
}
};
var NegentropyStorageVector = class {
items;
sealed;
constructor() {
this.items = [];
this.sealed = false;
}
insert(timestamp, id) {
if (this.sealed)
throw Error("already sealed");
const idb = hexToBytes(id);
if (idb.byteLength !== ID_SIZE)
throw Error("bad id size for added item");
this.items.push({ timestamp, id: idb });
}
seal() {
if (this.sealed)
throw Error("already sealed");
this.sealed = true;
this.items.sort(itemCompare);
for (let i = 1; i < this.items.length; i++) {
if (itemCompare(this.items[i - 1], this.items[i]) === 0)
throw Error("duplicate item inserted");
}
}
unseal() {
this.sealed = false;
}
size() {
this._checkSealed();
return this.items.length;
}
getItem(i) {
this._checkSealed();
if (i >= this.items.length)
throw Error("out of range");
return this.items[i];
}
iterate(begin, end, cb) {
this._checkSealed();
this._checkBounds(begin, end);
for (let i = begin; i < end; ++i) {
if (!cb(this.items[i], i))
break;
}
}
findLowerBound(begin, end, bound) {
this._checkSealed();
this._checkBounds(begin, end);
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
}
fingerprint(begin, end) {
let out = new Accumulator();
out.setToZero();
this.iterate(begin, end, (item) => {
out.add(item.id);
return true;
});
return out.getFingerprint(end - begin);
}
_checkSealed() {
if (!this.sealed)
throw Error("not sealed");
}
_checkBounds(begin, end) {
if (begin > end || end > this.items.length)
throw Error("bad range");
}
_binarySearch(arr, first, last, cmp) {
let count = last - first;
while (count > 0) {
let it = first;
let step = Math.floor(count / 2);
it += step;
if (cmp(arr[it])) {
first = ++it;
count -= step + 1;
} else {
count = step;
}
}
return first;
}
};
var Negentropy = class {
storage;
frameSizeLimit;
lastTimestampIn;
lastTimestampOut;
constructor(storage, frameSizeLimit = 6e4) {
if (frameSizeLimit < 4096)
throw Error("frameSizeLimit too small");
this.storage = storage;
this.frameSizeLimit = frameSizeLimit;
this.lastTimestampIn = 0;
this.lastTimestampOut = 0;
}
_bound(timestamp, id) {
return { timestamp, id: id || new Uint8Array(0) };
}
initiate() {
let output = new WrappedBuffer();
output.extend(new Uint8Array([PROTOCOL_VERSION]));
this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
return bytesToHex(output.unwrap());
}
reconcile(queryMsg, onhave, onneed) {
const query = new WrappedBuffer(hexToBytes(queryMsg));
this.lastTimestampIn = this.lastTimestampOut = 0;
let fullOutput = new WrappedBuffer();
fullOutput.extend(new Uint8Array([PROTOCOL_VERSION]));
let protocolVersion = getByte(query);
if (protocolVersion < 96 || protocolVersion > 111)
throw Error("invalid negentropy protocol version byte");
if (protocolVersion !== PROTOCOL_VERSION) {
throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96));
}
let storageSize = this.storage.size();
let prevBound = this._bound(0);
let prevIndex = 0;
let skip = false;
while (query.length !== 0) {
let o = new WrappedBuffer();
let doSkip = () => {
if (skip) {
skip = false;
o.extend(this.encodeBound(prevBound));
o.extend(encodeVarInt(Mode.Skip));
}
};
let currBound = this.decodeBound(query);
let mode = decodeVarInt(query);
let lower = prevIndex;
let upper = this.storage.findLowerBound(prevIndex, storageSize, currBound);
if (mode === Mode.Skip) {
skip = true;
} else if (mode === Mode.Fingerprint) {
let theirFingerprint = getBytes(query, FINGERPRINT_SIZE);
let ourFingerprint = this.storage.fingerprint(lower, upper);
if (compareUint8Array(theirFingerprint, ourFingerprint) !== 0) {
doSkip();
this.splitRange(lower, upper, currBound, o);
} else {
skip = true;
}
} else if (mode === Mode.IdList) {
let numIds = decodeVarInt(query);
let theirElems = {};
for (let i = 0; i < numIds; i++) {
let e = getBytes(query, ID_SIZE);
theirElems[bytesToHex(e)] = e;
}
skip = true;
this.storage.iterate(lower, upper, (item) => {
let k = item.id;
const id = bytesToHex(k);
if (!theirElems[id]) {
onhave?.(id);
} else {
delete theirElems[bytesToHex(k)];
}
return true;
});
if (onneed) {
for (let v of Object.values(theirElems)) {
onneed(bytesToHex(v));
}
}
} else {
throw Error("unexpected mode");
}
if (this.exceededFrameSizeLimit(fullOutput.length + o.length)) {
let remainingFingerprint = this.storage.fingerprint(upper, storageSize);
fullOutput.extend(this.encodeBound(this._bound(Number.MAX_VALUE)));
fullOutput.extend(encodeVarInt(Mode.Fingerprint));
fullOutput.extend(remainingFingerprint);
break;
} else {
fullOutput.extend(o);
}
prevIndex = upper;
prevBound = currBound;
}
return fullOutput.length === 1 ? null : bytesToHex(fullOutput.unwrap());
}
splitRange(lower, upper, upperBound, o) {
let numElems = upper - lower;
let buckets = 16;
if (numElems < buckets * 2) {
o.extend(this.encodeBound(upperBound));
o.extend(encodeVarInt(Mode.IdList));
o.extend(encodeVarInt(numElems));
this.storage.iterate(lower, upper, (item) => {
o.extend(item.id);
return true;
});
} else {
let itemsPerBucket = Math.floor(numElems / buckets);
let bucketsWithExtra = numElems % buckets;
let curr = lower;
for (let i = 0; i < buckets; i++) {
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
let ourFingerprint = this.storage.fingerprint(curr, curr + bucketSize);
curr += bucketSize;
let nextBound;
if (curr === upper) {
nextBound = upperBound;
} else {
let prevItem;
let currItem;
this.storage.iterate(curr - 1, curr + 1, (item, index) => {
if (index === curr - 1)
prevItem = item;
else
currItem = item;
return true;
});
nextBound = this.getMinimalBound(prevItem, currItem);
}
o.extend(this.encodeBound(nextBound));
o.extend(encodeVarInt(Mode.Fingerprint));
o.extend(ourFingerprint);
}
}
}
exceededFrameSizeLimit(n) {
return n > this.frameSizeLimit - 200;
}
decodeTimestampIn(encoded) {
let timestamp = decodeVarInt(encoded);
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
if (this.lastTimestampIn === Number.MAX_VALUE || timestamp === Number.MAX_VALUE) {
this.lastTimestampIn = Number.MAX_VALUE;
return Number.MAX_VALUE;
}
timestamp += this.lastTimestampIn;
this.lastTimestampIn = timestamp;
return timestamp;
}
decodeBound(encoded) {
let timestamp = this.decodeTimestampIn(encoded);
let len = decodeVarInt(encoded);
if (len > ID_SIZE)
throw Error("bound key too long");
let id = getBytes(encoded, len);
return { timestamp, id };
}
encodeTimestampOut(timestamp) {
if (timestamp === Number.MAX_VALUE) {
this.lastTimestampOut = Number.MAX_VALUE;
return encodeVarInt(0);
}
let temp = timestamp;
timestamp -= this.lastTimestampOut;
this.lastTimestampOut = temp;
return encodeVarInt(timestamp + 1);
}
encodeBound(key) {
let output = new WrappedBuffer();
output.extend(this.encodeTimestampOut(key.timestamp));
output.extend(encodeVarInt(key.id.length));
output.extend(key.id);
return output;
}
getMinimalBound(prev, curr) {
if (curr.timestamp !== prev.timestamp) {
return this._bound(curr.timestamp);
} else {
let sharedPrefixBytes = 0;
let currKey = curr.id;
let prevKey = prev.id;
for (let i = 0; i < ID_SIZE; i++) {
if (currKey[i] !== prevKey[i])
break;
sharedPrefixBytes++;
}
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
}
}
};
function compareUint8Array(a, b) {
for (let i = 0; i < a.byteLength; i++) {
if (a[i] < b[i])
return -1;
if (a[i] > b[i])
return 1;
}
if (a.byteLength > b.byteLength)
return 1;
if (a.byteLength < b.byteLength)
return -1;
return 0;
}
function itemCompare(a, b) {
if (a.timestamp === b.timestamp) {
return compareUint8Array(a.id, b.id);
}
return a.timestamp - b.timestamp;
}
var NegentropySync = class {
relay;
storage;
neg;
filter;
subscription;
onhave;
onneed;
constructor(relay, storage, filter, params = {}) {
this.relay = relay;
this.storage = storage;
this.neg = new Negentropy(storage);
this.onhave = params.onhave;
this.onneed = params.onneed;
this.filter = filter;
this.subscription = this.relay.prepareSubscription([{}], { label: params.label || "negentropy" });
this.subscription.oncustom = (data) => {
switch (data[0]) {
case "NEG-MSG": {
if (data.length < 3) {
console.warn(`got invalid NEG-MSG from ${this.relay.url}: ${data}`);
}
try {
const response = this.neg.reconcile(data[2], this.onhave, this.onneed);
if (response) {
this.relay.send(`["NEG-MSG", "${this.subscription.id}", "${response}"]`);
} else {
this.close();
params.onclose?.();
}
} catch (error) {
console.error("negentropy reconcile error:", error);
params?.onclose?.(`reconcile error: ${error}`);
}
break;
}
case "NEG-CLOSE": {
const reason = data[2];
console.warn("negentropy error:", reason);
params.onclose?.(reason);
break;
}
case "NEG-ERR": {
params.onclose?.();
}
}
};
}
async start() {
const initMsg = this.neg.initiate();
this.relay.send(`["NEG-OPEN","${this.subscription.id}",${JSON.stringify(this.filter)},"${initMsg}"]`);
}
close() {
this.relay.send(`["NEG-CLOSE","${this.subscription.id}"]`);
this.subscription.close();
}
};
export {
Negentropy,
NegentropyStorageVector,
NegentropySync
};