UNPKG

@substrate-system/mergeparty

Version:
287 lines (286 loc) 10.2 kB
"use strict"; 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