@fireproof/netlify
Version:
PartyKit gateway for Netlify
307 lines (305 loc) • 10.5 kB
JavaScript
// src/connection-from-store.ts
import { BuildURI, runtimeFn, URI } from "@adviser/cement";
import { bs, ensureLogger } from "@fireproof/core";
var ConnectionFromStore = class extends bs.ConnectionBase {
constructor(sthis, url) {
const logger = 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 = 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, URI.from(iurl));
}
function makeKeyBagUrlExtractable(sthis) {
let base = sthis.env.get("FP_KEYBAG_URL");
if (!base) {
if (runtimeFn().isBrowser) {
base = "indexdb://fp-keybag";
} else {
base = "file://./dist/kb-dir-partykit";
}
}
const kbUrl = 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/netlify/gateway.ts
import { KeyedResolvOnce, Result, URI as URI2, BuildURI as BuildURI2, exception2Result } from "@adviser/cement";
import { bs as bs2, getStore, NotFoundError, ensureSuperLog } from "@fireproof/core";
var NetlifyGateway = class {
constructor(sthis) {
this.sthis = ensureSuperLog(sthis, "NetlifyGateway");
this.logger = this.sthis.logger;
}
async buildUrl(baseUrl, key) {
return Result.Ok(baseUrl.build().setParam("key", key).URI());
}
async destroy(url) {
const { store } = getStore(url, this.sthis, (...args) => args.join("/"));
if (store !== "meta") {
return Result.Ok(void 0);
}
const rName = url.getParamResult("name");
if (rName.isErr()) {
return Result.Err(rName.Err());
}
let name = rName.Ok();
const index = url.getParam("index");
if (index) {
name += `-${index}`;
}
name += ".fp";
const remoteBaseUrl = url.getParam("remoteBaseUrl");
if (!remoteBaseUrl) {
return Result.Err(new Error("Remote base URL not found in the URI"));
}
const fetchUrl = BuildURI2.from(remoteBaseUrl).setParam("meta", name).URI();
const response = await fetch(fetchUrl.asURL(), { method: "DELETE" });
if (!response.ok) {
return this.logger.Error().Str("status", response.statusText).Msg("Failed to destroy meta database").ResultError();
}
return Result.Ok(void 0);
}
async start(uri) {
const protocol = uri.host.startsWith("localhost") ? "http" : "https";
const host = uri.host;
const path = "/fireproof";
const urlString = `${protocol}://${host}${path}`;
const baseUrl = BuildURI2.from(urlString).URI();
const ret = uri.build().defParam("version", "v0.1-netlify").defParam("remoteBaseUrl", baseUrl.toString()).URI();
return Result.Ok(ret);
}
async close() {
return Result.Ok(void 0);
}
async put(url, body) {
const { store } = getStore(url, this.sthis, (...args) => args.join("/"));
const rParams = url.getParamsResult("key", "name");
if (rParams.isErr()) {
return this.logger.Error().Url(url).Err(rParams).Msg("Put Error").ResultError();
}
const { key } = rParams.Ok();
let { name } = rParams.Ok();
const index = url.getParam("index");
if (index) {
name += `-${index}`;
}
name += ".fp";
const remoteBaseUrl = url.getParam("remoteBaseUrl");
if (!remoteBaseUrl) {
return Result.Err(new Error("Remote base URL not found in the URI"));
}
const fetchUrl = BuildURI2.from(remoteBaseUrl);
switch (store) {
case "meta":
fetchUrl.setParam("meta", name);
break;
default:
fetchUrl.setParam("car", key);
break;
}
if (store === "meta") {
const bodyRes = await bs2.addCryptoKeyToGatewayMetaPayload(url, this.sthis, body);
if (bodyRes.isErr()) {
return Result.Err(bodyRes.Err());
}
body = bodyRes.Ok();
}
const done = await fetch(fetchUrl.asURL(), { method: "PUT", body });
if (!done.ok) {
return this.logger.Error().Url(fetchUrl.URI()).Int("status", done.status).Str("statusText", done.statusText).Msg(`failed to upload ${store}`).ResultError();
}
return Result.Ok(void 0);
}
async get(url) {
const { store } = getStore(url, this.sthis, (...args) => args.join("/"));
const rParams = url.getParamsResult("key", "name", "remoteBaseUrl");
if (rParams.isErr()) {
return Result.Err(rParams.Err());
}
const { key, remoteBaseUrl } = rParams.Ok();
let { name } = rParams.Ok();
const index = url.getParam("index");
if (index) {
name += `-${index}`;
}
name += ".fp";
const fetchUrl = BuildURI2.from(remoteBaseUrl);
switch (store) {
case "meta":
fetchUrl.setParam("meta", name);
break;
default:
fetchUrl.setParam("car", key);
break;
}
const rresponse = await exception2Result(() => {
return fetch(fetchUrl.URI().asURL());
});
if (rresponse.isErr()) {
return this.logger.Error().Url(fetchUrl).Err(rresponse).Msg("Failed to fetch").ResultError();
}
const response = rresponse.Ok();
if (!response.ok) {
return Result.Err(new NotFoundError(`${store} not found: ${url}`));
}
const data = new Uint8Array(await response.arrayBuffer());
if (store === "meta") {
const res = await bs2.setCryptoKeyFromGatewayMetaPayload(url, this.sthis, data);
if (res.isErr()) {
return this.logger.Error().Url(url).Err(res).Msg("Failed to set crypto key").ResultError();
}
}
return Result.Ok(data);
}
async delete(url) {
const { store } = getStore(url, this.sthis, (...args) => args.join("/"));
const rParams = url.getParamsResult("key", "name", "remoteBaseUrl");
if (rParams.isErr()) {
return Result.Err(rParams.Err());
}
const { key, remoteBaseUrl } = rParams.Ok();
let { name } = rParams.Ok();
const index = url.getParam("index");
if (index) {
name += `-${index}`;
}
name += ".fp";
const fetchUrl = BuildURI2.from(remoteBaseUrl);
switch (store) {
case "meta":
fetchUrl.setParam("meta", name);
break;
default:
if (!key) {
return Result.Err(new Error("Key not found in the URI"));
}
fetchUrl.setParam("car", key);
break;
}
const response = await fetch(fetchUrl.URI().asURL(), { method: "DELETE" });
if (!response.ok) {
return Result.Err(new Error(`Failed to delete car: ${response.statusText}`));
}
return Result.Ok(void 0);
}
async subscribe(url, callback) {
url = url.build().setParam("key", "main").defParam("interval", "100").defParam("maxInterval", "3000").URI();
let lastData = void 0;
const initInterval = parseInt(url.getParam("interval") || "100", 10);
const maxInterval = parseInt(url.getParam("maxInterval") || "3000", 10);
let interval = initInterval;
const fetchData = async () => {
const result = await this.get(url);
if (result.isOk()) {
const data = result.Ok();
if (!lastData || !data.every((value, index) => lastData && value === lastData[index])) {
lastData = data;
callback(data);
interval = initInterval;
} else {
interval = Math.min(interval * 2, maxInterval);
}
}
timeoutId = setTimeout(fetchData, interval);
};
let timeoutId = setTimeout(fetchData, interval);
return Result.Ok(() => {
clearTimeout(timeoutId);
});
}
};
var NetlifyTestStore = class {
constructor(sthis, gw) {
this.sthis = ensureSuperLog(sthis, "NetlifyTestStore");
this.logger = this.sthis.logger;
this.gateway = gw;
}
async get(iurl, key) {
const url = iurl.build().setParam("key", key).URI();
const buffer = await this.gateway.get(url);
return buffer.Ok();
}
};
var onceRegisterNetlifyStoreProtocol = new KeyedResolvOnce();
function registerNetlifyStoreProtocol(protocol = "netlify:", overrideBaseURL) {
return onceRegisterNetlifyStoreProtocol.get(protocol).once(() => {
URI2.protocolHasHostpart(protocol);
return bs2.registerStoreProtocol({
protocol,
overrideBaseURL,
gateway: async (sthis) => {
return new NetlifyGateway(sthis);
},
test: async (sthis) => {
const gateway = new NetlifyGateway(sthis);
return new NetlifyTestStore(sthis, gateway);
}
});
});
}
// src/netlify/index.ts
import { BuildURI as BuildURI3, KeyedResolvOnce as KeyedResolvOnce2, runtimeFn as runtimeFn2 } from "@adviser/cement";
if (!runtimeFn2().isBrowser) {
const url = BuildURI3.from(process.env.FP_KEYBAG_URL || "file://./dist/kb-dir-netlify");
url.setParam("extractKey", "_deprecated_internal_api");
process.env.FP_KEYBAG_URL = url.toString();
}
registerNetlifyStoreProtocol();
var connectionCache = new KeyedResolvOnce2();
var connect = (db, remoteDbName = "", url = "netlify://localhost:8888?protocol=ws") => {
const { sthis, blockstore, name: dbName } = db;
if (!dbName) {
throw new Error("dbName is required");
}
const urlObj = BuildURI3.from(url);
const existingName = urlObj.getParam("name");
urlObj.defParam("name", remoteDbName || existingName || dbName);
urlObj.defParam("localName", dbName);
urlObj.defParam("storekey", `@${dbName}:data@`);
return connectionCache.get(urlObj.toString()).once(() => {
makeKeyBagUrlExtractable(sthis);
const connection = connectionFactory(sthis, urlObj);
connection.connect_X(blockstore);
return connection;
});
};
export {
connect
};
//# sourceMappingURL=index.js.map