peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
333 lines (287 loc) • 9.91 kB
JavaScript
import { EventEmitter } from './EventEmitter.js';
import DebugLogger from './DebugLogger.js';
/**
* Lexical Storage Interface - GUN-like chaining API for DistributedStorageManager
*
* Usage:
* const user = storage.get('users').get('alice');
* await user.put({name: 'Alice', age: 30});
* const name = await user.get('name').val();
*
* user.get('friends').set({bob: true, charlie: true});
* user.get('friends').map().on((friend, key) => console.log(key, friend));
*/
export class LexicalStorageInterface extends EventEmitter {
constructor(distributedStorage, path = []) {
super();
this.debug = DebugLogger.create('LexicalStorageInterface');
this.storage = distributedStorage;
this.path = path.slice(); // Create a copy of the path
this.subscriptions = new Map();
this._isMap = false;
}
/**
* Navigate to a key (creates a new interface instance)
* @param {string} key - The key to navigate to
* @returns {LexicalStorageInterface} New interface for the key
*/
get(key) {
const newInterface = new LexicalStorageInterface(this.storage, [...this.path, key]);
return newInterface;
}
/**
* Store data at the current path
* @param {any} value - Value to store
* @param {Object} options - Storage options
* @returns {Promise<LexicalStorageInterface>} This interface for chaining
*/
async put(value, options = {}) {
const fullKey = this.path.join(':');
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// For objects, store each property as a separate key
const promises = Object.entries(value).map(([prop, val]) => {
const propKey = `${fullKey}:${prop}`;
return this.storage.store(propKey, val, options);
});
await Promise.all(promises);
// Also store the object structure for reference
await this.storage.store(fullKey, { _keys: Object.keys(value), _type: 'object' }, options);
} else {
// For primitives, store directly
await this.storage.store(fullKey, value, options);
}
// Emit change event
this.emit('put', { key: fullKey, value });
return this;
}
/**
* Retrieve the current value
* @param {Object} options - Retrieval options
* @returns {Promise<any>} The stored value
*/
async val(options = {}) {
const fullKey = this.path.join(':');
const value = await this.storage.retrieve(fullKey, options);
// If it's an object structure, reconstruct it
if (value && typeof value === 'object' && value._keys && value._type === 'object') {
const reconstructed = {};
const promises = value._keys.map(async (key) => {
const keyValue = await this.storage.retrieve(`${fullKey}:${key}`, options);
reconstructed[key] = keyValue;
});
await Promise.all(promises);
return reconstructed;
}
return value;
}
/**
* Set multiple key-value pairs (like GUN's set)
* @param {Object} obj - Object with key-value pairs to set
* @param {Object} options - Storage options
* @returns {Promise<LexicalStorageInterface>} This interface for chaining
*/
async set(obj, options = {}) {
const fullKey = this.path.join(':');
// Store each entry with a unique ID
const promises = Object.entries(obj).map(([key, value]) => {
const setKey = `${fullKey}:${key}`;
return this.storage.store(setKey, value, options);
});
await Promise.all(promises);
// Update the set index
const existingSet = await this.storage.retrieve(`${fullKey}:_set`) || {};
const updatedSet = { ...existingSet, ...obj };
await this.storage.store(`${fullKey}:_set`, updatedSet, options);
// Emit set event
this.emit('set', { key: fullKey, values: obj });
return this;
}
/**
* Map over a set (like GUN's map)
* @returns {LexicalStorageInterface} New interface for mapping
*/
map() {
const mapInterface = new LexicalStorageInterface(this.storage, this.path);
mapInterface._isMap = true;
return mapInterface;
}
/**
* Subscribe to changes (like GUN's on)
* @param {Function} callback - Callback for changes
* @returns {Function} Unsubscribe function
*/
on(callback) {
const fullKey = this.path.join(':');
if (this._isMap) {
// For maps, listen to set changes
const setKey = `${fullKey}:_set`;
const handleChange = async () => {
const setData = await this.storage.retrieve(setKey);
if (setData) {
Object.entries(setData).forEach(([key, value]) => {
callback(value, key);
});
}
};
// Subscribe to the set key using storage's subscription mechanism
if (this.storage.subscribe) {
this.storage.subscribe(setKey).then(() => {
this.storage.on('dataUpdated', (event) => {
if (event.key === setKey) {
handleChange();
}
});
});
}
// Initial call
handleChange();
return () => {
if (this.storage.unsubscribe) {
this.storage.unsubscribe(setKey);
}
};
} else {
// For regular keys, listen to direct changes
if (this.storage.subscribe) {
this.storage.subscribe(fullKey).then((currentValue) => {
callback(currentValue, fullKey);
});
this.storage.on('dataUpdated', (event) => {
if (event.key === fullKey) {
this.storage.retrieve(fullKey).then(value => {
callback(value, fullKey);
});
}
});
}
return () => {
if (this.storage.unsubscribe) {
this.storage.unsubscribe(fullKey);
}
};
}
}
/**
* Once - listen for a single change
* @param {Function} callback - Callback for the change
* @returns {Promise} Promise that resolves with the value
*/
async once(callback) {
return new Promise((resolve) => {
const unsubscribe = this.on((value, key) => {
if (callback) callback(value, key);
unsubscribe();
resolve(value);
});
});
}
/**
* Delete the current path
* @returns {Promise<boolean>} Success status
*/
async delete() {
const fullKey = this.path.join(':');
// If it's an object, delete all its properties first
const value = await this.storage.retrieve(fullKey);
if (value && typeof value === 'object' && value._keys && value._type === 'object') {
const deletePromises = value._keys.map(key => {
const propKey = `${fullKey}:${key}`;
return this.storage.delete(propKey);
});
await Promise.all(deletePromises);
}
const result = await this.storage.delete(fullKey);
// Emit delete event
this.emit('delete', { key: fullKey });
return result;
}
/**
* Update with merge semantics
* @param {any} value - Value to merge
* @param {Object} options - Update options
* @returns {Promise<LexicalStorageInterface>} This interface for chaining
*/
async update(value, options = {}) {
const fullKey = this.path.join(':');
const currentValue = await this.storage.retrieve(fullKey);
let mergedValue;
if (typeof value === 'object' && value !== null && !Array.isArray(value) &&
typeof currentValue === 'object' && currentValue !== null && !Array.isArray(currentValue)) {
// Handle object reconstruction first
if (currentValue._keys && currentValue._type === 'object') {
const reconstructed = {};
const promises = currentValue._keys.map(async (key) => {
const keyValue = await this.storage.retrieve(`${fullKey}:${key}`, options);
reconstructed[key] = keyValue;
});
await Promise.all(promises);
mergedValue = { ...reconstructed, ...value };
} else {
mergedValue = { ...currentValue, ...value };
}
} else {
mergedValue = value;
}
// Use put instead of update to ensure proper object decomposition
await this.put(mergedValue, options);
// Emit update event
this.emit('update', { key: fullKey, value: mergedValue });
return this;
}
/**
* Create a proxied interface that allows property access
* @returns {Proxy} Proxied interface
*/
proxy() {
return new Proxy(this, {
get(target, prop) {
// If it's a method, return it bound
if (typeof target[prop] === 'function') {
return target[prop].bind(target);
}
// If it's a property access, create a new get() call
if (typeof prop === 'string' && !prop.startsWith('_') && prop !== 'constructor') {
return target.get(prop).proxy();
}
return target[prop];
}
});
}
/**
* Get the current path as a string
* @returns {string} The path
*/
getPath() {
return this.path.join(':');
}
/**
* Check if a value exists at the current path
* @returns {Promise<boolean>} Whether the value exists
*/
async exists() {
const fullKey = this.path.join(':');
const value = await this.storage.retrieve(fullKey);
return value !== null && value !== undefined;
}
/**
* Get all keys under the current path (for object-like structures)
* @returns {Promise<string[]>} Array of keys
*/
async keys() {
const fullKey = this.path.join(':');
const value = await this.storage.retrieve(fullKey);
if (value && typeof value === 'object' && value._keys && value._type === 'object') {
return value._keys;
}
return [];
}
}
/**
* Create a lexical interface for the distributed storage
* @param {DistributedStorageManager} distributedStorage
* @returns {Proxy} Proxied lexical interface
*/
export function createLexicalInterface(distributedStorage) {
const lexicalInterface = new LexicalStorageInterface(distributedStorage);
return lexicalInterface.proxy();
}