mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-04 05:43:10 +00:00
feat: initial SDS implementaiton
This commit is contained in:
parent
fe17cbc57b
commit
4b96bedcdd
@ -1,10 +1,10 @@
|
|||||||
import { WalletService } from '../wallets/index';
|
import { WalletService } from '../identity/wallets/index';
|
||||||
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
||||||
import { AppKit } from '@reown/appkit';
|
import { AppKit } from '@reown/appkit';
|
||||||
import { OrdinalAPI } from '../ordinal';
|
import { OrdinalAPI } from '../identity/ordinal';
|
||||||
import { CryptoService, DelegationDuration } from './CryptoService';
|
import { CryptoService, DelegationDuration } from './CryptoService';
|
||||||
import { EVerificationStatus, User } from '@/types/forum';
|
import { EVerificationStatus, User } from '@/types/forum';
|
||||||
import { WalletInfo } from '../wallets/ReOwnWalletService';
|
import { WalletInfo } from '../identity/wallets/ReOwnWalletService';
|
||||||
|
|
||||||
export interface AuthResult {
|
export interface AuthResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -31,7 +31,7 @@ export interface AuthServiceInterface {
|
|||||||
clearStoredUser(): void;
|
clearStoredUser(): void;
|
||||||
|
|
||||||
// Wallet info
|
// Wallet info
|
||||||
getWalletInfo(): Promise<WalletInfo>;
|
getWalletInfo(): Promise<WalletInfo | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthService implements AuthServiceInterface {
|
export class AuthService implements AuthServiceInterface {
|
||||||
@ -152,8 +152,6 @@ export class AuthService implements AuthServiceInterface {
|
|||||||
async disconnectWallet(): Promise<void> {
|
async disconnectWallet(): Promise<void> {
|
||||||
// Clear any existing delegations when disconnecting
|
// Clear any existing delegations when disconnecting
|
||||||
this.cryptoService.clearDelegation();
|
this.cryptoService.clearDelegation();
|
||||||
this.walletService.clearDelegation('bitcoin');
|
|
||||||
this.walletService.clearDelegation('ethereum');
|
|
||||||
|
|
||||||
// Clear stored user data
|
// Clear stored user data
|
||||||
this.clearStoredUser();
|
this.clearStoredUser();
|
||||||
@ -298,7 +296,7 @@ export class AuthService implements AuthServiceInterface {
|
|||||||
/**
|
/**
|
||||||
* Get current wallet info
|
* Get current wallet info
|
||||||
*/
|
*/
|
||||||
async getWalletInfo() {
|
async getWalletInfo(): Promise<WalletInfo | null> {
|
||||||
// Use the wallet service to get detailed wallet info including ENS
|
// Use the wallet service to get detailed wallet info including ENS
|
||||||
return await this.walletService.getWalletInfo();
|
return await this.walletService.getWalletInfo();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,50 +4,38 @@ import { CellMessage, PostMessage, CommentMessage, VoteMessage } from './types'
|
|||||||
import { CONTENT_TOPICS, NETWORK_CONFIG } from './constants';
|
import { CONTENT_TOPICS, NETWORK_CONFIG } from './constants';
|
||||||
import { OpchanMessage } from '@/types/forum';
|
import { OpchanMessage } from '@/types/forum';
|
||||||
|
|
||||||
|
// Create the sharded pubsub topic
|
||||||
|
const PUBSUB_TOPIC = `/waku/2/rs/${NETWORK_CONFIG.clusterId}/0`;
|
||||||
|
|
||||||
export const encoders = {
|
export const encoders = {
|
||||||
[MessageType.CELL]: createEncoder({
|
[MessageType.CELL]: createEncoder({
|
||||||
contentTopic: CONTENT_TOPICS['cell'],
|
contentTopic: CONTENT_TOPICS[MessageType.CELL],
|
||||||
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
|
routingInfo: { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }
|
||||||
}),
|
}),
|
||||||
[MessageType.POST]: createEncoder({
|
[MessageType.POST]: createEncoder({
|
||||||
contentTopic: CONTENT_TOPICS['post'],
|
contentTopic: CONTENT_TOPICS[MessageType.POST],
|
||||||
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
|
routingInfo: { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }
|
||||||
}),
|
}),
|
||||||
[MessageType.COMMENT]: createEncoder({
|
[MessageType.COMMENT]: createEncoder({
|
||||||
contentTopic: CONTENT_TOPICS['comment'],
|
contentTopic: CONTENT_TOPICS[MessageType.COMMENT],
|
||||||
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
|
routingInfo: { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }
|
||||||
}),
|
}),
|
||||||
[MessageType.VOTE]: createEncoder({
|
[MessageType.VOTE]: createEncoder({
|
||||||
contentTopic: CONTENT_TOPICS['vote'],
|
contentTopic: CONTENT_TOPICS[MessageType.VOTE],
|
||||||
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
|
routingInfo: { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }
|
||||||
}),
|
}),
|
||||||
[MessageType.MODERATE]: createEncoder({
|
[MessageType.MODERATE]: createEncoder({
|
||||||
contentTopic: CONTENT_TOPICS['moderate'],
|
contentTopic: CONTENT_TOPICS[MessageType.MODERATE],
|
||||||
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
|
routingInfo: { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decoders = {
|
export const decoders = {
|
||||||
[MessageType.CELL]: createDecoder(CONTENT_TOPICS['cell'], {
|
[MessageType.CELL]: createDecoder(CONTENT_TOPICS[MessageType.CELL], { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }),
|
||||||
clusterId: NETWORK_CONFIG.clusterId,
|
[MessageType.POST]: createDecoder(CONTENT_TOPICS[MessageType.POST], { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }),
|
||||||
shard: 0
|
[MessageType.COMMENT]: createDecoder(CONTENT_TOPICS[MessageType.COMMENT], { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }),
|
||||||
}),
|
[MessageType.VOTE]: createDecoder(CONTENT_TOPICS[MessageType.VOTE], { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC }),
|
||||||
[MessageType.POST]: createDecoder(CONTENT_TOPICS['post'], {
|
[MessageType.MODERATE]: createDecoder(CONTENT_TOPICS[MessageType.MODERATE], { clusterId: NETWORK_CONFIG.clusterId, shardId: 0, pubsubTopic: PUBSUB_TOPIC })
|
||||||
clusterId: NETWORK_CONFIG.clusterId,
|
|
||||||
shard: 0
|
|
||||||
}),
|
|
||||||
[MessageType.COMMENT]: createDecoder(CONTENT_TOPICS['comment'], {
|
|
||||||
clusterId: NETWORK_CONFIG.clusterId,
|
|
||||||
shard: 0
|
|
||||||
}),
|
|
||||||
[MessageType.VOTE]: createDecoder(CONTENT_TOPICS['vote'], {
|
|
||||||
clusterId: NETWORK_CONFIG.clusterId,
|
|
||||||
shard: 0
|
|
||||||
}),
|
|
||||||
[MessageType.MODERATE]: createDecoder(CONTENT_TOPICS['moderate'], {
|
|
||||||
clusterId: NETWORK_CONFIG.clusterId,
|
|
||||||
shard: 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
//TODO: perhaps store all messages in an indexed DB? (helpful when Waku is down)
|
//TODO: perhaps store all messages in an indexed DB? (helpful when Waku is down)
|
||||||
// with a `isPublished` flag to indicate if the message has been sent to the network
|
// with a `isPublished` flag to indicate if the message has been sent to the network
|
||||||
|
|
||||||
import { createLightNode, LightNode } from "@waku/sdk";
|
import { createLightNode, LightNode, WakuEvent, HealthStatus } from "@waku/sdk";
|
||||||
import StoreManager from "./store";
|
import StoreManager from "./store";
|
||||||
import { CommentCache, MessageType, VoteCache, ModerateMessage } from "./types";
|
import { CommentCache, MessageType, VoteCache, ModerateMessage } from "./types";
|
||||||
import { PostCache } from "./types";
|
import { PostCache } from "./types";
|
||||||
import { CellCache } from "./types";
|
import { CellCache } from "./types";
|
||||||
import { OpchanMessage } from "@/types/forum";
|
import { OpchanMessage } from "@/types/forum";
|
||||||
import { EphemeralProtocolsManager } from "./lightpush_filter";
|
|
||||||
import { NETWORK_CONFIG } from "./constants";
|
import { NETWORK_CONFIG } from "./constants";
|
||||||
|
import { ReliableMessageManager } from "./reliable_channel";
|
||||||
|
|
||||||
export type HealthChangeCallback = (isReady: boolean) => void;
|
export type HealthChangeCallback = (isReady: boolean, health: HealthStatus) => void;
|
||||||
|
|
||||||
class MessageManager {
|
class MessageManager {
|
||||||
private node: LightNode;
|
private node: LightNode;
|
||||||
//TODO: implement SDS?
|
private reliableMessageManager: ReliableMessageManager | null = null;
|
||||||
private ephemeralProtocolsManager: EphemeralProtocolsManager;
|
|
||||||
private storeManager: StoreManager;
|
private storeManager: StoreManager;
|
||||||
private _isReady: boolean = false;
|
private _isReady: boolean = false;
|
||||||
|
private _currentHealth: HealthStatus = HealthStatus.Unhealthy;
|
||||||
private healthListeners: Set<HealthChangeCallback> = new Set();
|
private healthListeners: Set<HealthChangeCallback> = new Set();
|
||||||
private peerCheckInterval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
|
|
||||||
public readonly messageCache: {
|
public readonly messageCache: {
|
||||||
@ -48,55 +47,84 @@ class MessageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
if (this.peerCheckInterval) {
|
if (this.reliableMessageManager) {
|
||||||
clearInterval(this.peerCheckInterval);
|
this.reliableMessageManager.cleanup();
|
||||||
this.peerCheckInterval = null;
|
this.reliableMessageManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.node.stop();
|
await this.node.stop();
|
||||||
this.setIsReady(false);
|
this.setIsReady(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(node: LightNode) {
|
private constructor(node: LightNode) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.ephemeralProtocolsManager = new EphemeralProtocolsManager(node);
|
|
||||||
this.storeManager = new StoreManager(node);
|
this.storeManager = new StoreManager(node);
|
||||||
|
|
||||||
// Start peer monitoring
|
this.setupHealthMonitoring();
|
||||||
this.startPeerMonitoring();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitoring connected peers to determine node health
|
* Set up health monitoring using Waku's built-in health events
|
||||||
* Runs every 1 second to check if we have at least one peer
|
|
||||||
*/
|
*/
|
||||||
private startPeerMonitoring() {
|
private setupHealthMonitoring() {
|
||||||
// Initial peer check
|
this.node.events.addEventListener(WakuEvent.Health, (event) => {
|
||||||
this.checkPeers();
|
const health = event.detail;
|
||||||
|
this._currentHealth = health;
|
||||||
// Regular peer checking
|
|
||||||
this.peerCheckInterval = setInterval(() => {
|
console.log(`Waku health status: ${health}`);
|
||||||
this.checkPeers();
|
|
||||||
}, 1000);
|
if (health === HealthStatus.SufficientlyHealthy) {
|
||||||
|
console.log("Waku is sufficiently healthy - initializing reliable messaging");
|
||||||
|
this.setIsReady(true);
|
||||||
|
this.initializeReliableManager();
|
||||||
|
} else if (health === HealthStatus.MinimallyHealthy) {
|
||||||
|
console.log("Waku is minimally healthy - may have issues sending/receiving messages");
|
||||||
|
this.setIsReady(true);
|
||||||
|
this.initializeReliableManager();
|
||||||
|
} else {
|
||||||
|
console.log("Waku is unhealthy - disconnected from network");
|
||||||
|
this.setIsReady(false);
|
||||||
|
this.cleanupReliableManager();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async initializeReliableManager() {
|
||||||
* Check if we have connected peers and update ready state
|
// Only initialize if not already initialized
|
||||||
*/
|
if (this.reliableMessageManager) {
|
||||||
private async checkPeers() {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const peers = await this.node.getConnectedPeers();
|
this.reliableMessageManager = new ReliableMessageManager(this.node);
|
||||||
this.setIsReady(peers.length >= 1);
|
|
||||||
} catch (err) {
|
// Set up listener for incoming reliable messages
|
||||||
console.error("Error checking peers:", err);
|
this.reliableMessageManager.addIncomingMessageListener({
|
||||||
this.setIsReady(false);
|
onMessage: (message) => {
|
||||||
|
console.log("Received reliable message:", message);
|
||||||
|
this.updateCache(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Reliable message manager initialized successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize reliable message manager:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupReliableManager() {
|
||||||
|
if (this.reliableMessageManager) {
|
||||||
|
console.log("Cleaning up reliable message manager due to health status");
|
||||||
|
this.reliableMessageManager.cleanup();
|
||||||
|
this.reliableMessageManager = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setIsReady(isReady: boolean) {
|
private setIsReady(isReady: boolean) {
|
||||||
if (this._isReady !== isReady) {
|
if (this._isReady !== isReady) {
|
||||||
this._isReady = isReady;
|
this._isReady = isReady;
|
||||||
// Notify all health listeners
|
// Notify all health listeners with both ready state and health status
|
||||||
this.healthListeners.forEach(listener => listener(isReady));
|
this.healthListeners.forEach(listener => listener(isReady, this._currentHealth));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +135,13 @@ class MessageManager {
|
|||||||
return this._isReady;
|
return this._isReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current Waku health status
|
||||||
|
*/
|
||||||
|
public get currentHealth(): HealthStatus {
|
||||||
|
return this._currentHealth;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to health status changes
|
* Subscribe to health status changes
|
||||||
* @param callback Function to call when health status changes
|
* @param callback Function to call when health status changes
|
||||||
@ -116,7 +151,7 @@ class MessageManager {
|
|||||||
this.healthListeners.add(callback);
|
this.healthListeners.add(callback);
|
||||||
|
|
||||||
// Immediately call with current status
|
// Immediately call with current status
|
||||||
callback(this._isReady);
|
callback(this._isReady, this._currentHealth);
|
||||||
|
|
||||||
// Return unsubscribe function
|
// Return unsubscribe function
|
||||||
return () => {
|
return () => {
|
||||||
@ -125,31 +160,28 @@ class MessageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the node to connect to at least one peer
|
* Waits for the node to achieve at least minimally healthy status
|
||||||
* @param timeoutMs Maximum time to wait in milliseconds
|
* @param timeoutMs Maximum time to wait in milliseconds
|
||||||
* @returns Promise that resolves when connected or rejects on timeout
|
* @returns Promise that resolves when healthy or rejects on timeout
|
||||||
*/
|
*/
|
||||||
public async waitForRemotePeer(timeoutMs: number = 15000): Promise<boolean> {
|
public async waitForRemotePeer(timeoutMs: number = 15000): Promise<boolean> {
|
||||||
if (this._isReady) return true;
|
if (this._isReady) return true;
|
||||||
|
|
||||||
return new Promise<boolean>((resolve, reject) => {
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reject(new Error(`Timed out waiting for remote peer after ${timeoutMs}ms`));
|
reject(new Error(`Timed out waiting for healthy network connection after ${timeoutMs}ms`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
const checkHandler = (isReady: boolean) => {
|
const checkHandler = (isReady: boolean, health: HealthStatus) => {
|
||||||
if (isReady) {
|
if (isReady && (health === HealthStatus.MinimallyHealthy || health === HealthStatus.SufficientlyHealthy)) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
this.healthListeners.delete(checkHandler);
|
this.healthListeners.delete(checkHandler);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add temporary listener for peer connection
|
// Add temporary listener for health status
|
||||||
this.healthListeners.add(checkHandler);
|
this.healthListeners.add(checkHandler);
|
||||||
|
|
||||||
// Also do an immediate check in case we already have peers
|
|
||||||
this.checkPeers();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,19 +197,23 @@ class MessageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(message: OpchanMessage) {
|
public async sendMessage(message: OpchanMessage) {
|
||||||
await this.ephemeralProtocolsManager.sendMessage(message);
|
if (!this.reliableMessageManager) {
|
||||||
//TODO: should we update the cache here? or just from store/filter?
|
throw new Error("Reliable message manager not initialized");
|
||||||
this.updateCache(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async subscribeToMessages(types: MessageType[] = [MessageType.CELL, MessageType.POST, MessageType.COMMENT, MessageType.VOTE, MessageType.MODERATE]) {
|
|
||||||
const { result, subscription } = await this.ephemeralProtocolsManager.subscribeToMessages(types);
|
|
||||||
|
|
||||||
for (const message of result) {
|
|
||||||
this.updateCache(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use reliable channel with status tracking
|
||||||
|
const messageId = await this.reliableMessageManager.sendMessage(message, {
|
||||||
|
onSent: (id) => console.log(`Message ${id} sent ✓`),
|
||||||
|
onAcknowledged: (id) => console.log(`Message ${id} acknowledged ✓✓`),
|
||||||
|
onError: (id, error) => console.error(`Message ${id} failed:`, error)
|
||||||
|
});
|
||||||
|
|
||||||
return { messages: result, subscription };
|
console.log(`Sent reliable message with ID: ${messageId}`);
|
||||||
|
|
||||||
|
// Update local cache immediately for optimistic UI
|
||||||
|
this.updateCache(message);
|
||||||
|
|
||||||
|
return messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCache(message: OpchanMessage) {
|
private updateCache(message: OpchanMessage) {
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import { LightNode } from "@waku/sdk";
|
|
||||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage, ModerateMessage } from "./types";
|
|
||||||
import { OpchanMessage } from "@/types/forum";
|
|
||||||
import { encodeMessage, encoders, decoders, decodeMessage } from "./codec";
|
|
||||||
|
|
||||||
export class EphemeralProtocolsManager {
|
|
||||||
private node: LightNode;
|
|
||||||
|
|
||||||
constructor(node: LightNode) {
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendMessage(message: OpchanMessage) {
|
|
||||||
const encodedMessage = encodeMessage(message);
|
|
||||||
const result = await this.node.lightPush.send(encoders[message.type], {
|
|
||||||
payload: encodedMessage
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async subscribeToMessages(types: MessageType[]) {
|
|
||||||
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage)[] = [];
|
|
||||||
|
|
||||||
const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => {
|
|
||||||
const {payload} = message;
|
|
||||||
|
|
||||||
const decodedMessage = decodeMessage(payload);
|
|
||||||
if (types.includes(decodedMessage.type)) {
|
|
||||||
result.push(decodedMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (subscription.error) {
|
|
||||||
throw new Error(subscription.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscription.results.successes.length === 0) {
|
|
||||||
throw new Error("No successes");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {result, subscription};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import messageManager from '@/lib/waku';
|
import messageManager from '@/lib/waku';
|
||||||
|
import { HealthStatus } from '@waku/sdk';
|
||||||
|
|
||||||
export type ToastFunction = (props: {
|
export type ToastFunction = (props: {
|
||||||
title: string;
|
title: string;
|
||||||
@ -44,8 +45,7 @@ export const initializeNetwork = async (
|
|||||||
toast({ title: 'Connection timeout', description: 'Could not connect to any peers. Some features may be unavailable.', variant: 'destructive' });
|
toast({ title: 'Connection timeout', description: 'Could not connect to any peers. Some features may be unavailable.', variant: 'destructive' });
|
||||||
console.warn('Timeout connecting to peer:', err);
|
console.warn('Timeout connecting to peer:', err);
|
||||||
}
|
}
|
||||||
await messageManager.queryStore();
|
// await messageManager.queryStore();
|
||||||
await messageManager.subscribeToMessages();
|
|
||||||
updateStateFromCache();
|
updateStateFromCache();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading forum data:', err);
|
console.error('Error loading forum data:', err);
|
||||||
@ -81,10 +81,13 @@ export const monitorNetworkHealth = (
|
|||||||
toast: ToastFunction,
|
toast: ToastFunction,
|
||||||
): { unsubscribe: () => void } => {
|
): { unsubscribe: () => void } => {
|
||||||
setIsNetworkConnected(messageManager.isReady);
|
setIsNetworkConnected(messageManager.isReady);
|
||||||
const unsubscribe = messageManager.onHealthChange((isReady) => {
|
const unsubscribe = messageManager.onHealthChange((isReady, health) => {
|
||||||
setIsNetworkConnected(isReady);
|
setIsNetworkConnected(isReady);
|
||||||
if (isReady) {
|
|
||||||
toast({ title: 'Network connected', description: 'Connected to the Waku network' });
|
if (health === HealthStatus.SufficientlyHealthy) {
|
||||||
|
toast({ title: 'Network connected', description: 'Connected to the Waku network with excellent connectivity' });
|
||||||
|
} else if (health === HealthStatus.MinimallyHealthy) {
|
||||||
|
toast({ title: 'Network connected', description: 'Connected to Waku network. Some features may be limited.', variant: 'default' });
|
||||||
} else {
|
} else {
|
||||||
toast({ title: 'Network disconnected', description: 'Lost connection to the Waku network', variant: 'destructive' });
|
toast({ title: 'Network disconnected', description: 'Lost connection to the Waku network', variant: 'destructive' });
|
||||||
}
|
}
|
||||||
|
|||||||
133
src/lib/waku/reliable_channel.ts
Normal file
133
src/lib/waku/reliable_channel.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { IDecodedMessage, LightNode, ReliableChannel, ReliableChannelEvent } from "@waku/sdk";
|
||||||
|
import { MessageType } from "./types";
|
||||||
|
import { decodeMessage, decoders, encodeMessage, encoders } from "./codec";
|
||||||
|
import { generateStringId } from "@/lib/utils";
|
||||||
|
import { OpchanMessage } from "@/types/forum";
|
||||||
|
|
||||||
|
export interface MessageStatusCallback {
|
||||||
|
onSent?: (messageId: string) => void;
|
||||||
|
onAcknowledged?: (messageId: string) => void;
|
||||||
|
onError?: (messageId: string, error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IncomingMessageCallback {
|
||||||
|
onMessage: (message: OpchanMessage) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReliableMessageManager {
|
||||||
|
private channels: Map<MessageType, ReliableChannel<IDecodedMessage>> = new Map();
|
||||||
|
private messageCallbacks: Map<string, MessageStatusCallback> = new Map();
|
||||||
|
private incomingMessageCallbacks: IncomingMessageCallback[] = [];
|
||||||
|
|
||||||
|
constructor(node: LightNode) {
|
||||||
|
this.initializeChannels(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeChannels(node: LightNode) {
|
||||||
|
for (const type of Object.values(MessageType)) {
|
||||||
|
const encoder = encoders[type];
|
||||||
|
const decoder = decoders[type];
|
||||||
|
const senderId = generateStringId();
|
||||||
|
const channelId = `opchan-${type}`; // Unique channel ID for each message type
|
||||||
|
|
||||||
|
try {
|
||||||
|
const channel = await ReliableChannel.create(node, channelId, senderId, encoder, decoder);
|
||||||
|
this.channels.set(type, channel);
|
||||||
|
this.setupChannelListeners(channel, type);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create reliable channel for ${type}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupChannelListeners(channel: ReliableChannel<IDecodedMessage>, type: MessageType) {
|
||||||
|
channel.addEventListener(ReliableChannelEvent.InMessageReceived, (event) => {
|
||||||
|
try {
|
||||||
|
const wakuMessage = event.detail;
|
||||||
|
if (wakuMessage.payload) {
|
||||||
|
const opchanMessage = decodeMessage(wakuMessage.payload);
|
||||||
|
|
||||||
|
|
||||||
|
this.incomingMessageCallbacks.forEach(callback => {
|
||||||
|
callback.onMessage(opchanMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to process incoming message for ${type}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for outgoing message status updates
|
||||||
|
channel.addEventListener(ReliableChannelEvent.OutMessageSent, (event) => {
|
||||||
|
const messageId = event.detail;
|
||||||
|
const callback = this.messageCallbacks.get(messageId);
|
||||||
|
if (callback?.onSent) {
|
||||||
|
callback.onSent(messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.addEventListener(ReliableChannelEvent.OutMessageAcknowledged, (event) => {
|
||||||
|
const messageId = event.detail;
|
||||||
|
const callback = this.messageCallbacks.get(messageId);
|
||||||
|
if (callback?.onAcknowledged) {
|
||||||
|
callback.onAcknowledged(messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.addEventListener(ReliableChannelEvent.OutMessageIrrecoverableError, (event) => {
|
||||||
|
const messageId = event.detail.messageId;
|
||||||
|
const error = event.detail.error;
|
||||||
|
const callback = this.messageCallbacks.get(messageId);
|
||||||
|
if (callback?.onError) {
|
||||||
|
callback.onError(messageId, error?.toString() || 'Unknown error');
|
||||||
|
}
|
||||||
|
// Clean up callback after error
|
||||||
|
this.messageCallbacks.delete(messageId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendMessage(message: OpchanMessage, statusCallback?: MessageStatusCallback): Promise<string> {
|
||||||
|
const channel = this.channels.get(message.type);
|
||||||
|
if (!channel) {
|
||||||
|
throw new Error(`No reliable channel for message type: ${message.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedMessage = encodeMessage(message);
|
||||||
|
const messageId = ReliableChannel.getMessageId(encodedMessage);
|
||||||
|
|
||||||
|
// Store callback for this message
|
||||||
|
if (statusCallback) {
|
||||||
|
this.messageCallbacks.set(messageId, statusCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await channel.send(encodedMessage);
|
||||||
|
return messageId;
|
||||||
|
} catch (error) {
|
||||||
|
// Clean up callback on immediate send failure
|
||||||
|
this.messageCallbacks.delete(messageId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addIncomingMessageListener(callback: IncomingMessageCallback) {
|
||||||
|
this.incomingMessageCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeIncomingMessageListener(callback: IncomingMessageCallback) {
|
||||||
|
const index = this.incomingMessageCallbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.incomingMessageCallbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChannelStatus(type: MessageType): boolean {
|
||||||
|
return this.channels.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
this.messageCallbacks.clear();
|
||||||
|
this.incomingMessageCallbacks.length = 0;
|
||||||
|
this.channels.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user