@fireproof/cloud
Version:
Fireproof Cloud gateway for Fireproof
444 lines (440 loc) • 16.8 kB
JavaScript
"use strict";
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/cloud/index.ts
var cloud_exports = {};
__export(cloud_exports, {
connect: () => connect,
rawConnect: () => rawConnect
});
module.exports = __toCommonJS(cloud_exports);
var import_cement3 = require("@adviser/cement");
var import_core3 = require("@fireproof/core");
// src/connection-from-store.ts
var import_cement = require("@adviser/cement");
var import_core = require("@fireproof/core");
var ConnectionFromStore = class extends import_core.bs.ConnectionBase {
constructor(sthis, url) {
const logger = (0, import_core.ensureLogger)(sthis, "ConnectionFromStore", {
url: () => url.toString(),
this: 1,
log: 1
});
super(url, logger);
this.stores = void 0;
this.sthis = sthis;
}
async onConnect() {
this.logger.Debug().Msg("onConnect-start");
const stores = {
base: this.url
// data: this.urlData,
// meta: this.urlMeta,
};
const rName = this.url.getParamResult("name");
if (rName.isErr()) {
throw this.logger.Error().Err(rName).Msg("missing Parameter").AsError();
}
const storeRuntime = import_core.bs.toStoreRuntime({ stores }, this.sthis);
const loader = {
name: rName.Ok(),
ebOpts: {
logger: this.logger,
store: { stores },
storeRuntime
},
sthis: this.sthis
};
this.stores = {
data: await storeRuntime.makeDataStore(loader),
meta: await storeRuntime.makeMetaStore(loader)
};
this.logger.Debug().Msg("onConnect-done");
return;
}
};
function connectionFactory(sthis, iurl) {
return new ConnectionFromStore(sthis, import_cement.URI.from(iurl));
}
function makeKeyBagUrlExtractable(sthis) {
let base = sthis.env.get("FP_KEYBAG_URL");
if (!base) {
if ((0, import_cement.runtimeFn)().isBrowser) {
base = "indexdb://fp-keybag";
} else {
base = "file://./dist/kb-dir-partykit";
}
}
const kbUrl = import_cement.BuildURI.from(base);
kbUrl.defParam("extractKey", "_deprecated_internal_api");
sthis.env.set("FP_KEYBAG_URL", kbUrl.toString());
sthis.logger.Debug().Url(kbUrl, "keyBagUrl").Msg("Make keybag url extractable");
}
// src/cloud/gateway.ts
var import_partysocket = require("partysocket");
var import_cement2 = require("@adviser/cement");
var import_core2 = require("@fireproof/core");
var FireproofCloudGateway = class {
constructor(sthis) {
this.subscriberCallbacks = /* @__PURE__ */ new Set();
this.sthis = sthis;
this.id = sthis.nextId().str;
this.logger = (0, import_core2.ensureLogger)(sthis, "FireproofCloudGateway", {
url: () => this.url?.toString(),
this: this.id
});
this.logger.Debug().Msg("constructor");
}
async buildUrl(baseUrl, key) {
return import_cement2.Result.Ok(baseUrl.build().setParam("key", key).URI());
}
async start(uri) {
this.logger.Debug().Msg("Starting FireproofCloudGateway with URI: " + uri.toString());
await this.sthis.start();
this.url = uri;
const ret = uri.build().defParam("version", "v0.1-fireproof-cloud");
const rName = uri.getParamResult("name");
if (rName.isErr()) {
return this.logger.Error().Err(rName).Msg("name not found").ResultError();
}
let dbName = rName.Ok();
if (this.url.hasParam("index")) {
dbName = dbName + "-idx";
}
ret.defParam("party", "fireproof");
ret.defParam("protocol", "wss");
let possibleUndef = { protocol: ret.getParam("protocol") };
const protocolsStr = uri.getParam("protocols");
if (protocolsStr) {
const ps = protocolsStr.split(",").map((x) => x.trim()).filter((x) => x);
if (ps.length > 0) {
possibleUndef = { ...possibleUndef, protocols: ps };
}
}
const prefixStr = uri.getParam("prefix");
if (prefixStr) {
possibleUndef = { ...possibleUndef, prefix: prefixStr };
}
const query = {};
const partySockOpts = {
id: this.id,
host: this.url.host,
room: dbName,
party: ret.getParam("party"),
...possibleUndef,
query,
path: this.url.pathname.replace(/^\//, "")
};
if ((0, import_cement2.runtimeFn)().isNodeIsh) {
const { WebSocket } = await import("ws");
partySockOpts.WebSocket = WebSocket;
}
this.pso = partySockOpts;
return import_cement2.Result.Ok(ret.URI());
}
async ready() {
this.logger.Debug().Msg("ready");
}
async connectFireproofCloud() {
const pkKeyThis = pkKey(this.pso);
return pkSockets.get(pkKeyThis).once(async () => {
if (!this.pso) {
throw new Error("Party socket options not found");
}
this.party = new import_partysocket.PartySocket(this.pso);
let exposedResolve;
const openFn = () => {
this.logger.Debug().Msg("party open");
this.party?.addEventListener("message", async (event) => {
this.logger.Debug().Msg(`got message: ${event.data}`);
const mbin = this.sthis.txt.encode(event.data);
this.notifySubscribers(mbin);
});
exposedResolve(true);
};
return await new Promise((resolve) => {
exposedResolve = resolve;
this.party?.addEventListener("open", openFn);
});
});
}
async close() {
await this.ready();
this.logger.Debug().Msg("close");
this.party?.close();
return import_cement2.Result.Ok(void 0);
}
async put(uri, body) {
await this.ready();
const { store } = (0, import_core2.getStore)(uri, this.sthis, (...args) => args.join("/"));
if (store === "meta") {
const bodyRes = await import_core2.bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body);
if (bodyRes.isErr()) {
this.logger.Error().Err(bodyRes.Err()).Msg("Error in addCryptoKeyToGatewayMetaPayload");
throw bodyRes.Err();
}
body = bodyRes.Ok();
}
const rkey = uri.getParamResult("key");
if (rkey.isErr()) return import_cement2.Result.Err(rkey.Err());
const key = rkey.Ok();
if (store === "meta") {
const uploadUrl = pkMetaURL(uri, key);
return (0, import_cement2.exception2Result)(async () => {
const response = await fetch(uploadUrl.asURL(), { method: "PUT", body });
if (response.status === 404) {
throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError();
}
});
} else {
const uploadUrl = pkURL(uri, key, "car");
return (0, import_cement2.exception2Result)(async () => {
const response = await fetch(uploadUrl.asURL(), { method: "PUT" });
this.logger.Debug().Url(uploadUrl).Uint64("status", response.status).Str("status-text", response.statusText).Msg("put");
if (response.status === 404) {
throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError();
}
const url = (await response.json()).url;
this.logger.Debug().Url(url).Msg("put");
const uploadResponse = await fetch(url, { method: "PUT", body });
if (uploadResponse.status === 404) {
throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError();
}
});
}
}
notifySubscribers(data) {
for (const callback of this.subscriberCallbacks) {
try {
callback(data);
} catch (error) {
this.logger.Error().Err(error).Msg("Error in subscriber callback execution");
}
}
}
async subscribe(uri, callback) {
await this.ready();
await this.connectFireproofCloud();
const store = uri.getParam("store");
if (store !== "meta") {
return import_cement2.Result.Err(new Error("store must be meta"));
}
this.subscriberCallbacks.add(callback);
return import_cement2.Result.Ok(() => {
this.subscriberCallbacks.delete(callback);
});
}
async get(uri) {
await this.ready();
return (0, import_cement2.exception2Result)(async () => {
const { store } = (0, import_core2.getStore)(uri, this.sthis, (...args) => args.join("/"));
const key = uri.getParam("key");
if (!key) throw new Error("key not found");
let downloadUrl;
this.logger.Debug().Str("store", store).Str("key", key).Msg("get");
switch (store) {
case "meta":
downloadUrl = pkMetaURL(uri, key);
break;
case "data":
downloadUrl = pkCarGetURL(uri, key);
break;
default:
throw new Error(`Unsupported store: ${store}`);
}
const response = await fetch(downloadUrl.toString(), { method: "GET" });
if (response.status === 404) {
throw new Error(`Failure in downloading ${store}!`);
}
const body = new Uint8Array(await response.arrayBuffer());
if (store === "meta") {
const resKeyInfo = await import_core2.bs.setCryptoKeyFromGatewayMetaPayload(uri, this.sthis, body);
if (resKeyInfo.isErr()) {
this.logger.Error().Url(uri).Err(resKeyInfo).Any("body", body).Msg("Error in setCryptoKeyFromGatewayMetaPayload");
throw resKeyInfo.Err();
}
}
return body;
});
}
async delete(_uri) {
await this.ready();
throw new Error("no delete for fireproof cloud");
}
async destroy(uri) {
await this.ready();
return (0, import_cement2.exception2Result)(async () => {
const deleteUrl = pkBaseURL(uri);
const response = await fetch(deleteUrl.asURL(), { method: "DELETE" });
if (response.status === 404) {
throw new Error("Failure in deleting data!");
}
return import_cement2.Result.Ok(void 0);
});
}
};
var pkSockets = new import_cement2.KeyedResolvOnce();
function pkKey(set) {
const ret = JSON.stringify(
Object.entries(set || {}).sort(([a], [b]) => a.localeCompare(b)).filter(([k]) => k !== "id").map(([k, v]) => ({ [k]: v }))
);
return ret;
}
function pkURL(uri, key, type) {
const host = uri.host;
const name = uri.getParam("name");
const idx = uri.getParam("index") || "";
const protocol = uri.getParam("protocol") === "ws" ? "http" : "https";
const path = `/parties/fireproof/${name}${idx}`;
return import_cement2.BuildURI.from(`${protocol}://${host}${path}`).setParam(type, key).URI();
}
function pkBaseURL(uri) {
const host = uri.host;
const name = uri.getParam("name");
const idx = uri.getParam("index") || "";
const protocol = uri.getParam("protocol") === "ws" ? "http" : "https";
const path = `/parties/fireproof/${name}${idx}`;
return import_cement2.BuildURI.from(`${protocol}://${host}${path}`).URI();
}
function pkCarGetURL(uri, key) {
const baseUrl = uri.getParam("getBaseUrl");
if (!baseUrl) {
return pkURL(uri, key, "car");
}
const name = uri.getParam("name");
const idx = uri.getParam("index") || "";
const baseUri = import_cement2.URI.from(baseUrl).asURL();
baseUri.pathname = `/${name}${idx}/${key}`;
return import_cement2.BuildURI.from(baseUri).URI();
}
function pkMetaURL(uri, key) {
return pkURL(uri, key, "meta");
}
var FireproofCloudTestStore = class {
constructor(gw, sthis) {
this.sthis = sthis;
this.logger = (0, import_core2.ensureLogger)(sthis, "FireproofCloudTestStore");
this.gateway = gw;
}
async get(uri, key) {
const url = uri.build().setParam("key", key).URI();
const dbFile = this.sthis.pathOps.join(import_core2.rt.getPath(url, this.sthis), import_core2.rt.getFileName(url, this.sthis));
this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get");
const buffer = await this.gateway.get(url);
this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got");
return buffer.Ok();
}
};
var onceRegisterFireproofCloudStoreProtocol = new import_cement2.KeyedResolvOnce();
function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL) {
return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => {
import_cement2.URI.protocolHasHostpart(protocol);
return import_core2.bs.registerStoreProtocol({
protocol,
overrideBaseURL,
gateway: async (sthis) => {
return new FireproofCloudGateway(sthis);
},
test: async (sthis) => {
const gateway = new FireproofCloudGateway(sthis);
return new FireproofCloudTestStore(gateway, sthis);
}
});
});
}
// src/cloud/index.ts
var SYNC_DB_NAME = "fp_sync";
if (!(0, import_cement3.runtimeFn)().isBrowser) {
const url = import_cement3.BuildURI.from(process.env.FP_KEYBAG_URL || "file://./dist/kb-dir-FireproofCloud");
url.setParam("extractKey", "_deprecated_internal_api");
process.env.FP_KEYBAG_URL = url.toString();
}
registerFireproofCloudStoreProtocol();
var connectionCache = new import_cement3.KeyedResolvOnce();
var rawConnect = (db, remoteDbName = "", url = "fireproof://cloud.fireproof.direct") => {
const { sthis, blockstore, name: dbName } = db;
if (!dbName) {
throw new Error("dbName is required");
}
const urlObj = import_cement3.BuildURI.from(url);
const existingName = urlObj.getParam("name");
urlObj.defParam("name", remoteDbName || existingName || dbName);
urlObj.defParam("localName", dbName);
urlObj.defParam("storekey", `@${dbName}:data@`);
urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/");
const fpUrl = urlObj.toString().replace(/^http:\/\//, "fireproof://").replace(/^https:\/\//, "fireproof://");
return connectionCache.get(fpUrl).once(() => {
makeKeyBagUrlExtractable(sthis);
const connection = connectionFactory(sthis, fpUrl);
connection.connect_X(blockstore);
return connection;
});
};
async function getOrCreateRemoteName(dbName, remoteName) {
const syncDb = (0, import_core3.fireproof)(SYNC_DB_NAME);
const result = await syncDb.query("localName", { key: dbName, includeDocs: true });
if (result.rows.length === 0) {
const doc2 = {
remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str,
localName: dbName,
firstConnect: !remoteName
};
const { id } = await syncDb.put(doc2);
return { ...doc2, _id: id };
}
const doc = result.rows[0].doc;
return doc;
}
function connect(db, remoteName, dashboardURI = "https://dashboard.fireproof.storage/", remoteURI = "fireproof://cloud.fireproof.direct") {
const dbName = db.name;
if (!dbName) {
throw new Error("Database name is required for cloud connection");
}
return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => {
if (!doc) {
throw new Error("Failed to get or create remote name");
}
doc.endpoint = import_cement3.URI.from(remoteURI).toString();
const connection = rawConnect(db, doc.remoteName, import_cement3.URI.from(doc.endpoint).toString());
const connectURI = import_cement3.URI.from(dashboardURI).build().pathname("/fp/databases/connect");
connectURI.defParam("localName", dbName);
connectURI.defParam("remoteName", doc.remoteName);
if (doc.endpoint) {
connectURI.defParam("endpoint", doc.endpoint);
}
console.log("Fireproof Cloud: " + connectURI.toString());
if (doc.firstConnect && (0, import_cement3.runtimeFn)().isBrowser && window.location.href.indexOf(import_cement3.URI.from(dashboardURI).toString()) === -1) {
const syncDb = (0, import_core3.fireproof)(SYNC_DB_NAME);
doc.firstConnect = false;
await syncDb.put(doc);
}
connection.dashboardUrl = import_cement3.URI.from(connectURI);
return connection;
});
}
//# sourceMappingURL=index.cjs.map