y-fdp-storage
Version:
fdp-storage database provider for Yjs
519 lines (510 loc) • 16.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
FdpStoragePersistence: () => FdpStoragePersistence,
Feeds: () => feed_exports,
SequentialFeed: () => SequentialFeed,
TOPIC_BYTES_LENGTH: () => TOPIC_BYTES_LENGTH,
TOPIC_HEX_LENGTH: () => TOPIC_HEX_LENGTH,
UNCOMPRESSED_RECOVERY_ID: () => UNCOMPRESSED_RECOVERY_ID,
assertSigner: () => assertSigner,
defaultSign: () => defaultSign,
isObject: () => isObject,
isStrictlyObject: () => isStrictlyObject,
makeBytes: () => makeBytes,
makePrivateKeySigner: () => makePrivateKeySigner,
makeSigner: () => makeSigner,
readUint64BigEndian: () => readUint64BigEndian,
serializeBytes: () => serializeBytes,
writeUint64BigEndian: () => writeUint64BigEndian
});
module.exports = __toCommonJS(src_exports);
// src/adapter.ts
var Y = __toESM(require("yjs"));
// src/storage.ts
var import_utils = require("ethers/lib/utils");
var FeedStorage = class {
constructor(bee, feed, signer, topic, postageBatchId) {
this.bee = bee;
this.feed = feed;
this.signer = signer;
this.topic = topic;
this.postageBatchId = postageBatchId;
}
/**
* Reads the last state from the feed.
* @returns contract state
*/
async storageRead() {
const feedR = this.feed.makeFeedR(this.topic, this.signer.address);
const last = await feedR.getLastUpdate();
const state = await this.bee.downloadData(last.reference);
return {
...JSON.parse(state.text()),
...last
};
}
/**
* Writes the state to the feed.
* @param state The state to be written.
* @returns void
*/
async storageWrite(state) {
const feedRW = this.feed.makeFeedRW(this.topic, this.signer);
const block = {
state: (0, import_utils.hexlify)(state),
timestamp: Date.now()
};
const reference = await this.bee.uploadData(this.postageBatchId, JSON.stringify(block));
return feedRW.setLastUpdate(this.postageBatchId, reference.reference);
}
/**
* Writes the state to the feed.
* @param schemaFn The schema function to be used to encode the state.
* @param state The state to be written.
* @returns void
*/
async storageWriteWithSchema(schemaFn, state) {
const feedRW = this.feed.makeFeedRW(this.topic, this.signer);
let block;
if (state) {
block = schemaFn((0, import_utils.hexlify)(state));
} else {
block = schemaFn("");
}
const reference = await this.bee.uploadData(this.postageBatchId, JSON.stringify(block));
return feedRW.setLastUpdate(this.postageBatchId, reference.reference);
}
};
// src/adapter.ts
var import_ethers = require("ethers");
// src/feeds/feed.ts
var feed_exports = {};
__export(feed_exports, {
FEED_TYPES: () => FEED_TYPES,
assembleSocPayload: () => assembleSocPayload,
extractDataFromSocPayload: () => extractDataFromSocPayload,
fetchIndexToInt: () => fetchIndexToInt,
makeTopic: () => makeTopic,
mapSocToFeed: () => mapSocToFeed
});
var import_bee_js2 = require("@ethersphere/bee-js");
// src/feeds/utils.ts
var import_bee_js = require("@ethersphere/bee-js");
var import_elliptic = __toESM(require("elliptic"));
var EC = import_elliptic.default.ec;
var { assertBytes, hexToBytes, makeHexString, isBytes } = import_bee_js.Utils;
var TOPIC_BYTES_LENGTH = 32;
var TOPIC_HEX_LENGTH = 64;
var UNCOMPRESSED_RECOVERY_ID = 27;
function makeBytes(length) {
return new Uint8Array(length);
}
function writeUint64BigEndian(value, bytes = makeBytes(8)) {
const dataView = new DataView(bytes.buffer);
const valueLower32 = value & 4294967295;
dataView.setUint32(0, 0);
dataView.setUint32(4, valueLower32);
return bytes;
}
function publicKeyToAddress(pubKey) {
const pubBytes = pubKey.encode("array", false);
return import_bee_js.Utils.keccak256Hash(pubBytes.slice(1)).slice(12);
}
function hashWithEthereumPrefix(data) {
const ethereumSignedMessagePrefix = `Ethereum Signed Message:
${data.length}`;
const prefixBytes = new TextEncoder().encode(ethereumSignedMessagePrefix);
return import_bee_js.Utils.keccak256Hash(prefixBytes, data);
}
function defaultSign(data, privateKey) {
const curve = new EC("secp256k1");
const keyPair = curve.keyFromPrivate(privateKey);
const hashedDigest = hashWithEthereumPrefix(data);
const sigRaw = curve.sign(hashedDigest, keyPair, { canonical: true, pers: void 0 });
if (sigRaw.recoveryParam === null) {
throw new Error("signDigest recovery param was null");
}
const signature = new Uint8Array([
...sigRaw.r.toArray("be", 32),
...sigRaw.s.toArray("be", 32),
sigRaw.recoveryParam + UNCOMPRESSED_RECOVERY_ID
]);
return signature;
}
function makeSigner(signer) {
if (typeof signer === "string") {
const hexKey = makeHexString(signer, 64);
const keyBytes = hexToBytes(hexKey);
return makePrivateKeySigner(keyBytes);
} else if (signer instanceof Uint8Array) {
assertBytes(signer, 32);
return makePrivateKeySigner(signer);
}
assertSigner(signer);
return signer;
}
function assertSigner(signer) {
if (!isStrictlyObject(signer)) {
throw new TypeError("Signer must be an object!");
}
const typedSigner = signer;
if (!isBytes(typedSigner.address, 20)) {
throw new TypeError("Signer's address must be Uint8Array with 20 bytes!");
}
if (typeof typedSigner.sign !== "function") {
throw new TypeError("Signer sign property needs to be function!");
}
}
function makePrivateKeySigner(privateKey) {
const curve = new EC("secp256k1");
const keyPair = curve.keyFromPrivate(privateKey);
const address = publicKeyToAddress(keyPair.getPublic());
return {
sign: (digest) => defaultSign(digest, privateKey),
address
};
}
function readUint64BigEndian(bytes) {
const dataView = new DataView(bytes.buffer);
return dataView.getUint32(4);
}
function serializeBytes(...arrays) {
const length = arrays.reduce((prev, curr) => prev + curr.length, 0);
const buffer = new Uint8Array(length);
let offset = 0;
arrays.forEach((arr) => {
buffer.set(arr, offset);
offset += arr.length;
});
return buffer;
}
function isStrictlyObject(value) {
return isObject(value) && !Array.isArray(value);
}
function isObject(value) {
return value !== null && typeof value === "object";
}
// src/feeds/feed.ts
var { assertBytes: assertBytes2, bytesToHex, hexToBytes: hexToBytes2, makeHexString: makeHexString2 } = import_bee_js2.Utils;
var FEED_TYPES = ["sequential", "fault-tolerant-stream"];
function extractDataFromSocPayload(payload) {
const timestamp = readUint64BigEndian(payload.slice(0, 8));
const p = payload.slice(8);
if (p.length === 32 || p.length === 64) {
return {
timestamp,
reference: p
};
}
throw new Error("NotImplemented: payload is longer than expected");
}
function mapSocToFeed(socChunk, index) {
const { reference, timestamp } = extractDataFromSocPayload(socChunk.payload());
return {
...socChunk,
index,
timestamp,
reference: bytesToHex(reference)
};
}
function assembleSocPayload(reference, options) {
const at = options?.at ?? Date.now() / 1e3;
const timestamp = writeUint64BigEndian(at);
return serializeBytes(timestamp, reference);
}
function fetchIndexToInt(fetchIndex) {
const indexBytes = hexToBytes2(fetchIndex);
let index = 0;
for (let i = indexBytes.length - 1; i >= 0; i--) {
const byte = indexBytes[i];
if (byte === 0)
break;
index += byte;
}
return index;
}
function makeTopic(topic) {
if (typeof topic === "string") {
return makeHexString2(topic, TOPIC_HEX_LENGTH);
} else if (topic instanceof Uint8Array) {
assertBytes2(topic, TOPIC_BYTES_LENGTH);
return bytesToHex(topic, TOPIC_HEX_LENGTH);
}
throw new TypeError("invalid topic");
}
// src/feeds/sequential-feed.ts
var import_bee_js3 = require("@ethersphere/bee-js");
var { makeHexEthAddress, hexToBytes: hexToBytes3 } = import_bee_js3.Utils;
var SequentialFeed = class {
constructor(bee) {
this.bee = bee;
this.type = "sequential";
}
/**
* Creates a sequential feed reader
* @param topic a swarm topic
* @param owner owner
* @returns a sequential feed reader
*/
makeFeedR(topic, owner) {
const socReader = this.bee.makeSOCReader(owner);
const topicHex = makeTopic(topic);
const topicBytes = hexToBytes3(topicHex);
const ownerHex = makeHexEthAddress(owner);
const feedReader = this.bee.makeFeedReader("sequence", topicHex, owner);
const getLastUpdate = async () => {
const lastUpdate = await feedReader.download();
return lastUpdate;
};
const getLastIndex = async () => {
let index;
try {
const lastUpdate = await feedReader.download();
const { feedIndex } = lastUpdate;
index = fetchIndexToInt(feedIndex);
} catch (e) {
index = -1;
}
return index;
};
const findLastUpdate = async () => {
const index = await getLastIndex();
const id = this.getIdentifier(topic, index);
const socChunk = await socReader.download(id);
return mapSocToFeed(socChunk, index);
};
const getUpdate = async (index) => {
const socChunk = await socReader.download(this.getIdentifier(topic, index));
return mapSocToFeed(socChunk, index);
};
const getUpdates = async (indices) => {
const promises = [];
for (const index of indices) {
promises.push(socReader.download(this.getIdentifier(topic, index)));
}
const socs = await Promise.all(promises);
const feeds = socs.map((soc, orderIndex) => {
return mapSocToFeed(soc, indices[orderIndex]);
});
return feeds;
};
return {
type: "sequential",
owner: ownerHex,
topic: topicHex,
findLastUpdate,
getUpdate,
getUpdates,
getLastIndex,
getLastUpdate
};
}
/**
* Creates a sequential feed reader / writer
* @param topic a swarm topic
* @param signer signer
* @returns a sequential feed reader / writer
*/
makeFeedRW(topic, signer) {
const canonicalSigner = makeSigner(signer);
const topicHex = makeTopic(topic);
const topicBytes = hexToBytes3(topicHex);
const feedR = this.makeFeedR(topic, canonicalSigner.address);
const socWriter = this.bee.makeSOCWriter(canonicalSigner);
const setUpdate = async (index, postageBatchId, reference) => {
const identifier = this.getIdentifier(topicBytes, index);
return socWriter.upload(
postageBatchId,
identifier,
assembleSocPayload(hexToBytes3(reference))
//TODO metadata
);
};
const setLastUpdate = async (postageBatchId, reference) => {
let index;
try {
const lastIndex = await feedR.getLastIndex();
index = lastIndex + 1;
} catch (e) {
index = 0;
}
return setUpdate(index, postageBatchId, reference);
};
return {
...feedR,
setUpdate,
setLastUpdate
};
}
/**
* Get Single Owner Chunk identifier
* @param topic a swarm topic, bytes 32 length
* @param index the chunk index
* @returns a bytes 32
*/
getIdentifier(topic, index) {
const indexBytes = writeUint64BigEndian(index);
return import_bee_js3.Utils.keccak256Hash(topic, indexBytes);
}
};
// src/adapter.ts
var import_utils4 = require("ethers/lib/utils");
var import_rxjs = require("rxjs");
var FdpStoragePersistence = class {
/**
* @param bee Bee instance
* @param signer Signer instance
* @param topic Topic
* @param postageBatchId Postage batch id
*/
constructor(bee, signer, topic, postageBatchId) {
this.bee = bee;
this.signer = signer;
this.topic = topic;
this.postageBatchId = postageBatchId;
this.onUpdates = new import_rxjs.Subject();
const hash = import_ethers.ethers.utils.id(topic).slice(2);
const feed = new SequentialFeed(bee);
this.stateStorage = new FeedStorage(bee, feed, signer, hash, postageBatchId);
}
/**
* Writes the state to the feed.
* @param schemaFn The schema function to be used to encode the state.
* @param state The state to be written.
* @returns void
*/
async storeWithSchema(schemaFn, state) {
return this.stateStorage.storageWriteWithSchema(schemaFn, state);
}
/**
* Converts a hex string to a Uint8Array.
* @param hexstring hex string
* @returns Uint8Array
* */
fromHexToUint8Array(hexstring) {
return (0, import_utils4.arrayify)(hexstring);
}
/**
* Converts a Uint8Array to a hex string.
* @param arr Uint8Array
* @returns hex string
* */
fromUint8ArrayToHex(arr) {
return (0, import_utils4.hexlify)(arr);
}
/**
* Writes the update to the feed.
* @param update The update to be written. Handles exceptions internally.
* @returns void
*/
async storeUpdate(update) {
try {
const current = await this.stateStorage.storageRead();
const temp = (0, import_utils4.arrayify)(current.state);
const merged = Y.mergeUpdates([temp, update]);
await this.stateStorage.storageWrite(merged);
} catch (e) {
await this.stateStorage.storageWrite(update);
}
}
/**
* Writes the update to the feed.
* @param update The update to be written. Errors will be thrown if the update is not valid.
* @returns void
*/
async store(update) {
await this.stateStorage.storageWrite(update);
}
/**
* Reads the last state from the feed.
* @returns Uint8Array
*/
async read() {
const updates = await this.stateStorage.storageRead();
return updates.state;
}
/**
* Reads the last state from the feed.
* @returns contract state
*/
async getYDoc() {
const updates = await this.stateStorage.storageRead();
const doc = new Y.Doc();
Y.applyUpdate(doc, (0, import_utils4.arrayify)(updates.state));
return doc;
}
/**
* Subscribes to the feed and emits updates.
* @returns void
* @emits {update: Uint8Array, timestamp: number, reference: string}
**/
subscribe(doc, interval = 15e3) {
const temp = setInterval(async () => {
const updates = await this.stateStorage.storageRead();
Y.applyUpdate(doc, (0, import_utils4.arrayify)(updates.state));
this.onUpdates.next({ update: (0, import_utils4.arrayify)(updates.state), ...updates });
}, interval);
return () => clearInterval(temp);
}
/**
* Auto updates the feed.
* @returns void
* @emits {update: Uint8Array, timestamp: number, reference: string}
**/
autoUpdate(doc, interval = 5e3) {
const temp = setInterval(async () => {
const update = Y.encodeStateAsUpdate(doc);
await this.stateStorage.storageWrite(update);
}, interval);
return () => clearInterval(temp);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FdpStoragePersistence,
Feeds,
SequentialFeed,
TOPIC_BYTES_LENGTH,
TOPIC_HEX_LENGTH,
UNCOMPRESSED_RECOVERY_ID,
assertSigner,
defaultSign,
isObject,
isStrictlyObject,
makeBytes,
makePrivateKeySigner,
makeSigner,
readUint64BigEndian,
serializeBytes,
writeUint64BigEndian
});
//# sourceMappingURL=index.js.map