Modernize Holepunch stack for HyperDB 6 and current replication APIs

Align P2NS with current Autopass, Hypercore, Hyperdrive, and Protomux behavior:
add plugin DB recovery for incompatible persisted HyperDB data, stop
unconditional spec rebuilds via fingerprinting, update SDK replication paths,
and align proxy invite channels with p2ns.core-* protocols without replicating
Autopass over the p2ns topic.
This commit is contained in:
Raven Scott
2026-05-27 21:02:17 -04:00
parent 7cd98231f7
commit d57433d3e0
19 changed files with 630 additions and 405 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "File Drop",
"version": "1.0.6",
"version": "1.0.7",
"domain": "file.drop",
"enabled": false,
"description": "Temporary 24-hour file storage with shareable links",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Global Profile",
"version": "1.2.1",
"version": "1.2.2",
"domain": "global.profile",
"enabled": true,
"description": "Universal user identity system for all P2NS plugins with P2P profile replication",
+9
View File
@@ -6,6 +6,7 @@
const sdk = require('../../includes/plugins/sdk');
const crypto = require('crypto');
const { isIncompatibleDbError, dbErrorResponse } = require('./db-errors');
// Avatar sizes to generate
const AVATAR_SIZES = [16, 32, 64, 128, 256, 512];
@@ -36,6 +37,10 @@ async function getProfileFromDB(peerId) {
const profile = await sdk.db.get('@profile/profiles', { peerId });
return profile;
} catch (err) {
if (isIncompatibleDbError(err)) {
sdk.log.warn('global.profile', `Incompatible DB data while reading profile: ${err.message}`);
throw err;
}
// Silently return null if database not initialized or timeout - this is expected during startup
if (err.message && (err.message.includes('Database not initialized') || err.message.includes('timeout'))) {
sdk.log.debug('global.profile', `Database not ready for ${peerId ? peerId.slice(0, 16) : 'unknown'}: ${err.message}`);
@@ -170,6 +175,10 @@ async function getAllProfilesFromDB() {
sdk.log.debug('global.profile', `Found ${profiles?.length || 0} profiles in database`);
return profiles || [];
} catch (err) {
if (isIncompatibleDbError(err)) {
sdk.log.warn('global.profile', `Incompatible DB data while listing profiles: ${err.message}`);
throw err;
}
// Silently return empty array if database not initialized or timeout - this is expected during startup
if (err.message && (err.message.includes('Database not initialized') || err.message.includes('timeout') || err.message.includes('not available'))) {
sdk.log.debug('global.profile', `Database not ready when getting all profiles: ${err.message}`);
+46
View File
@@ -0,0 +1,46 @@
/**
* Shared DB error handling for global.profile routes.
*/
const { isIncompatibleDbError } = require('../../includes/plugins/db-recovery');
function isDbNotReadyError(err) {
const msg = err && err.message ? err.message : '';
return (
msg.includes('Database not initialized') ||
msg.includes('timeout') ||
msg.includes('not ready')
);
}
function dbErrorResponse(err) {
if (isIncompatibleDbError(err)) {
return {
status: 503,
body: {
error: 'Profile database is being reset after a schema upgrade',
code: 'db_migration',
hint: 'Retry in a few seconds. If this persists, delete plugin-sites/global.profile/db/ and restart P2NS.'
}
};
}
if (isDbNotReadyError(err)) {
return {
status: 503,
body: {
error: 'Profile database not ready',
code: 'db_not_ready'
}
};
}
return {
status: 500,
body: { error: err.message || 'Database error' }
};
}
module.exports = {
isIncompatibleDbError,
isDbNotReadyError,
dbErrorResponse
};
+3 -4
View File
@@ -5,6 +5,7 @@
*/
const sdk = require('../../../includes/plugins/sdk');
const { dbErrorResponse } = require('../db-errors');
/**
* Handle fields-related routes
@@ -18,10 +19,8 @@ async function handleFieldsRoutes(req, res, path, query, method) {
return sdk.router.json(res, { fields: fields || [] });
} catch (err) {
sdk.log.error('global.profile', `Error getting fields: ${err.message}`);
if (err.message && err.message.includes('Database not initialized')) {
return sdk.router.json(res, { fields: [], error: 'Database not ready' }, 503);
}
return sdk.router.error(res, 'Failed to retrieve fields', 500);
const { status, body } = dbErrorResponse(err);
return sdk.router.json(res, { fields: [], ...body }, status);
}
}
@@ -7,6 +7,7 @@
const sdk = require('../../../includes/plugins/sdk');
const { getLocalPeerId } = require('../utils');
const { getProfileFromDB, saveProfileToDB } = require('../database');
const { dbErrorResponse } = require('../db-errors');
const { broadcastProfileUpdate } = require('../websocket');
/**
@@ -83,7 +84,8 @@ async function handleProfileRoutes(req, res, path, query, method) {
}
sdk.log.error('global.profile', `Error getting profile: ${err.message}`);
if (!res.headersSent) {
return router.error(res, 'Failed to retrieve profile', 500, req);
const { status, body } = dbErrorResponse(err);
return router.json(res, body, status, req);
}
}
}
@@ -6,6 +6,7 @@
const sdk = require('../../../includes/plugins/sdk');
const { getAllProfilesFromDB } = require('../database');
const { dbErrorResponse } = require('../db-errors');
/**
* Handle profiles-related routes
@@ -102,7 +103,8 @@ async function handleProfilesRoutes(req, res, path, query, method) {
});
} catch (err) {
sdk.log.error('global.profile', `Error getting profiles: ${err.message}`);
return sdk.router.error(res, err.message, 500);
const { status, body } = dbErrorResponse(err);
return sdk.router.json(res, body, status, req);
}
}