@substrate-system/mergeparty
Version:
Automerge + Partykit
287 lines (286 loc) • 10.2 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var with_storage_exports = {};
__export(with_storage_exports, {
WithStorage: () => WithStorage
});
module.exports = __toCommonJS(with_storage_exports);
var import_automerge_repo_slim = require("@substrate-system/automerge-repo-slim");
var import_cborg = require("cborg");
var import_relay = require("./relay.js");
var import_polyfill = require("./polyfill.js");
class WithStorage extends import_relay.Relay {
static {
__name(this, "WithStorage");
}
// eslint-disable-line brace-style
isStorageServer = true;
/* This is used by the relay,
to decide if we should be announced as a peer. */
_repo;
constructor(room, repo) {
super(room);
if (!repo) {
this._repo = new import_automerge_repo_slim.Repo({
storage: this,
network: [this],
// server accepts new documents from clients
sharePolicy: /* @__PURE__ */ __name(async () => {
return true;
}, "sharePolicy"),
// Set a stable peer ID for the server
peerId: `server:${this.room.id}`
});
} else {
this._repo = repo;
}
this.setupStoragePersistence();
this._log = this._baseLog.extend("storage");
this.connect(this.serverPeerId, {});
}
async onMessage(raw, conn) {
if (!this.byConn.get(conn)?.joined) {
return super.onMessage(raw, conn);
}
try {
if (raw instanceof ArrayBuffer) {
const decoded = (0, import_cborg.decode)(new Uint8Array(raw));
if (decoded && decoded.type === "sync" && decoded.documentId) {
const documentId = decoded.documentId;
const existingHandle = this._repo.handles[documentId];
if (!existingHandle) {
this._repo.find(documentId);
}
}
}
} catch (_e) {
}
await super.onMessage(raw, conn);
}
/**
* Loads a value from PartyKit storage by key.
* @param {StorageKey} key The storage key
* @returns {Promise<Uint8Array|undefined>}
*/
async load(key) {
const keyStr = this.keyToString(key);
this._log(`Loading from storage: key=${keyStr}`);
const value = await this.room.storage.get(keyStr);
if (!value) {
this._log(`No value found for key: ${keyStr}`);
return;
}
this._log(`Found value for key: ${keyStr}, type=${typeof value}`);
if (value instanceof Uint8Array) return value;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (typeof value === "object" && value !== null && Object.keys(value).every((k) => !isNaN(Number(k)))) {
return new Uint8Array(Object.values(value));
}
throw new Error("Unsupported value type from storage");
}
/**
* Saves a value to PartyKit storage by key.
* @param {StorageKey} key The storage key
* @param {Uint8Array} value The value to store (Uint8Array)
*/
async save(key, value) {
const keyStr = this.keyToString(key);
this._log(`Saving to storage: key=${keyStr}, valueLength=${value.length}`);
await this.room.storage.put(keyStr, value);
this._log(`Successfully saved key: ${keyStr}`);
}
/**
* Removes a value from PartyKit storage by key.
* @param key The storage key
*/
async remove(key) {
await this.room.storage.delete(this.keyToString(key));
}
/**
* Loads a range of values from PartyKit storage by prefix.
* @param prefix The key prefix
* @returns {Promise<{ key:StorageKey, data:Uint8Array|undefined }[]>}
*/
async loadRange(prefix) {
const key = this.keyToString(prefix);
const entries = [];
const map = await this.room.storage.list({ prefix: key });
for (const [k, v] of [...map.entries()].sort(([a], [b]) => {
return a.localeCompare(b);
})) {
let u8;
if (v instanceof Uint8Array) u8 = v;
else if (v instanceof ArrayBuffer) u8 = new Uint8Array(v);
else if (typeof v === "object" && v !== null && Object.keys(v).every((k2) => !isNaN(Number(k2)))) {
u8 = new Uint8Array(Object.values(v));
} else {
u8 = void 0;
}
entries.push({ key: this.stringToKey(k), data: u8 });
}
return entries;
}
/**
* Removes a range of values from PartyKit storage by prefix.
* @param prefix The key prefix
*/
async removeRange(prefix) {
const key = this.keyToString(prefix);
const map = await this.room.storage.list({ prefix: key });
for (const key2 of map.keys()) {
await this.room.storage.delete(key2);
}
}
async onStart() {
this._log("**Stateful sync server started (Automerge peer w/ PartyKit storage)**");
await this.save(
["storage-adapter-id"],
new TextEncoder().encode(this.peerId || "server")
);
this._log("Storage adapter initialized");
}
// HTTP endpoints
async onRequest(req) {
const url = new URL(req.url);
if (url.pathname.includes("/debug/storage")) {
const storageMap = await this.room.storage.list();
const result = {};
for (const [key, value] of storageMap) {
result[key] = value;
}
return Response.json(result, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
}
if (url.pathname.includes("/test/storage")) {
this._log("[WithStorage] Storage test endpoint called");
try {
this._log("[WithStorage] Starting basic storage operations test...");
const testKey = "test-manual-storage";
const testValue = new TextEncoder().encode("test-value");
this._log("[WithStorage] Calling save...");
await this.save([testKey], testValue);
this._log("[WithStorage] Calling load...");
const retrieved = await this.load([testKey]);
if (!retrieved || new TextDecoder().decode(retrieved) !== "test-value") {
throw new Error("Storage test failed");
}
this._log("[WithStorage] Basic storage operations successful");
this._log("[WithStorage] Storage test: getting repo handles...");
let totalHandles = 0;
let readyHandles = 0;
let handleIds = [];
try {
handleIds = Object.keys(this._repo.handles);
totalHandles = handleIds.length;
this._log(`[WithStorage] Found ${totalHandles} handles`);
if (totalHandles > 0) {
const handles = Object.values(this._repo.handles);
this._log("[WithStorage] Checking readiness of handles...");
readyHandles = handles.filter((handle) => {
try {
return handle.isReady();
} catch (e) {
this._log(`[WithStorage] Error checking handle readiness: ${e.message}`);
return false;
}
}).length;
}
} catch (e) {
this._log(
`[WithStorage] Error accessing repo handles: ${e.message}`
);
}
this._log(`[WithStorage] Storage test: found ${totalHandles} total handles, ${readyHandles} ready`);
return Response.json({
success: true,
message: "Storage operations successful - Automerge handles persistence automatically",
repoHandles: handleIds,
readyHandles,
totalHandles,
storageKeys: await this.room.storage.list().then((map) => {
return [...map.keys()];
})
}, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
} catch (error) {
this._log(`[WithStorage] Storage test failed: ${error.message}`);
return Response.json({
success: false,
error: error.message
}, {
status: 500,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
}
}
return super.onRequest(req);
}
async onConnect(conn) {
super.onConnect(conn);
try {
await this._repo.flush();
this._log("Flushed on client connect");
} catch (e) {
this._log(`Failed to flush on connect: ${e}`);
}
}
unicastByPeerId(peerId, data) {
const conn = this.sockets[peerId];
if (conn) conn.send(data);
}
keyToString(key) {
return key.join(".");
}
stringToKey(key) {
return key.split(".");
}
setupStoragePersistence() {
this._log("[WithStorage] Setting up storage persistence - Automerge should handle this automatically");
setInterval(() => {
const handleCount = Object.keys(this._repo.handles).length;
if (handleCount > 0) {
const handles = Object.values(this._repo.handles);
const readyHandles = handles.filter((handle) => handle.isReady());
this._log(`[WithStorage] Repo state: ${handleCount} total handles, ${readyHandles.length} ready`);
this._log(
"[WithStorage] Handle IDs:",
Object.keys(this._repo.handles)
);
}
}, 5e3);
}
}
//# sourceMappingURL=with-storage.cjs.map