holosphere 1.1.20 → 2.0.0-alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +7 -0
- package/LICENSE +162 -38
- package/README.md +483 -367
- package/bin/holosphere-activitypub.js +158 -0
- package/cleanup-test-data.js +204 -0
- package/examples/demo.html +1333 -0
- package/examples/example-bot.js +197 -0
- package/package.json +47 -87
- package/scripts/check-bundle-size.js +54 -0
- package/scripts/check-quest-ids.js +77 -0
- package/scripts/import-holons.js +578 -0
- package/scripts/publish-to-relay.js +101 -0
- package/scripts/read-example.js +186 -0
- package/scripts/relay-diagnostic.js +59 -0
- package/scripts/relay-example.js +179 -0
- package/scripts/resync-to-relay.js +245 -0
- package/scripts/revert-import.js +196 -0
- package/scripts/test-hybrid-mode.js +108 -0
- package/scripts/test-local-storage.js +63 -0
- package/scripts/test-nostr-direct.js +55 -0
- package/scripts/test-read-data.js +45 -0
- package/scripts/test-write-read.js +63 -0
- package/scripts/verify-import.js +95 -0
- package/scripts/verify-relay-data.js +139 -0
- package/src/ai/aggregation.js +319 -0
- package/src/ai/breakdown.js +511 -0
- package/src/ai/classifier.js +217 -0
- package/src/ai/council.js +228 -0
- package/src/ai/embeddings.js +279 -0
- package/src/ai/federation-ai.js +324 -0
- package/src/ai/h3-ai.js +955 -0
- package/src/ai/index.js +112 -0
- package/src/ai/json-ops.js +225 -0
- package/src/ai/llm-service.js +205 -0
- package/src/ai/nl-query.js +223 -0
- package/src/ai/relationships.js +353 -0
- package/src/ai/schema-extractor.js +218 -0
- package/src/ai/spatial.js +293 -0
- package/src/ai/tts.js +194 -0
- package/src/content/social-protocols.js +168 -0
- package/src/core/holosphere.js +273 -0
- package/src/crypto/secp256k1.js +259 -0
- package/src/federation/discovery.js +334 -0
- package/src/federation/hologram.js +1042 -0
- package/src/federation/registry.js +386 -0
- package/src/hierarchical/upcast.js +110 -0
- package/src/index.js +2669 -0
- package/src/schema/validator.js +91 -0
- package/src/spatial/h3-operations.js +110 -0
- package/src/storage/backend-factory.js +125 -0
- package/src/storage/backend-interface.js +142 -0
- package/src/storage/backends/activitypub/server.js +653 -0
- package/src/storage/backends/activitypub-backend.js +272 -0
- package/src/storage/backends/gundb-backend.js +233 -0
- package/src/storage/backends/nostr-backend.js +136 -0
- package/src/storage/filesystem-storage-browser.js +41 -0
- package/src/storage/filesystem-storage.js +138 -0
- package/src/storage/global-tables.js +81 -0
- package/src/storage/gun-async.js +281 -0
- package/src/storage/gun-wrapper.js +221 -0
- package/src/storage/indexeddb-storage.js +122 -0
- package/src/storage/key-storage-simple.js +76 -0
- package/src/storage/key-storage.js +136 -0
- package/src/storage/memory-storage.js +59 -0
- package/src/storage/migration.js +338 -0
- package/src/storage/nostr-async.js +811 -0
- package/src/storage/nostr-client.js +939 -0
- package/src/storage/nostr-wrapper.js +211 -0
- package/src/storage/outbox-queue.js +208 -0
- package/src/storage/persistent-storage.js +109 -0
- package/src/storage/sync-service.js +164 -0
- package/src/subscriptions/manager.js +142 -0
- package/test-ai-real-api.js +202 -0
- package/tests/unit/ai/aggregation.test.js +295 -0
- package/tests/unit/ai/breakdown.test.js +446 -0
- package/tests/unit/ai/classifier.test.js +294 -0
- package/tests/unit/ai/council.test.js +262 -0
- package/tests/unit/ai/embeddings.test.js +384 -0
- package/tests/unit/ai/federation-ai.test.js +344 -0
- package/tests/unit/ai/h3-ai.test.js +458 -0
- package/tests/unit/ai/index.test.js +304 -0
- package/tests/unit/ai/json-ops.test.js +307 -0
- package/tests/unit/ai/llm-service.test.js +390 -0
- package/tests/unit/ai/nl-query.test.js +383 -0
- package/tests/unit/ai/relationships.test.js +311 -0
- package/tests/unit/ai/schema-extractor.test.js +384 -0
- package/tests/unit/ai/spatial.test.js +279 -0
- package/tests/unit/ai/tts.test.js +279 -0
- package/tests/unit/content.test.js +332 -0
- package/tests/unit/contract/core.test.js +88 -0
- package/tests/unit/contract/crypto.test.js +198 -0
- package/tests/unit/contract/data.test.js +223 -0
- package/tests/unit/contract/federation.test.js +181 -0
- package/tests/unit/contract/hierarchical.test.js +113 -0
- package/tests/unit/contract/schema.test.js +114 -0
- package/tests/unit/contract/social.test.js +217 -0
- package/tests/unit/contract/spatial.test.js +110 -0
- package/tests/unit/contract/subscriptions.test.js +128 -0
- package/tests/unit/contract/utils.test.js +159 -0
- package/tests/unit/core.test.js +152 -0
- package/tests/unit/crypto.test.js +328 -0
- package/tests/unit/federation.test.js +234 -0
- package/tests/unit/gun-async.test.js +252 -0
- package/tests/unit/hierarchical.test.js +399 -0
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
- package/tests/unit/integration/scenario-02-federation.test.js +76 -0
- package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
- package/tests/unit/integration/scenario-04-validation.test.js +129 -0
- package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
- package/tests/unit/integration/scenario-06-social.test.js +135 -0
- package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
- package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
- package/tests/unit/performance/benchmark.test.js +85 -0
- package/tests/unit/schema.test.js +213 -0
- package/tests/unit/spatial.test.js +158 -0
- package/tests/unit/storage.test.js +195 -0
- package/tests/unit/subscriptions.test.js +328 -0
- package/tests/unit/test-data-permanence-debug.js +197 -0
- package/tests/unit/test-data-permanence.js +340 -0
- package/tests/unit/test-key-persistence-fixed.js +148 -0
- package/tests/unit/test-key-persistence.js +172 -0
- package/tests/unit/test-relay-permanence.js +376 -0
- package/tests/unit/test-second-node.js +95 -0
- package/tests/unit/test-simple-write.js +89 -0
- package/vite.config.js +49 -0
- package/vitest.config.js +20 -0
- package/FEDERATION.md +0 -213
- package/compute.js +0 -298
- package/content.js +0 -980
- package/federation.js +0 -1234
- package/global.js +0 -736
- package/hexlib.js +0 -335
- package/hologram.js +0 -183
- package/holosphere-bundle.esm.js +0 -33256
- package/holosphere-bundle.js +0 -33287
- package/holosphere-bundle.min.js +0 -39
- package/holosphere.d.ts +0 -601
- package/holosphere.js +0 -719
- package/node.js +0 -246
- package/schema.js +0 -139
- package/utils.js +0 -302
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityPub Storage Backend
|
|
3
|
+
* Client adapter for connecting to an ActivityPub server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageBackend } from '../backend-interface.js';
|
|
7
|
+
|
|
8
|
+
export class ActivityPubBackend extends StorageBackend {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
super(config);
|
|
11
|
+
this.serverUrl = config.serverUrl;
|
|
12
|
+
this.actorName = config.appName || 'holosphere';
|
|
13
|
+
this.apiKey = config.apiKey;
|
|
14
|
+
this.actorId = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async init() {
|
|
18
|
+
if (!this.serverUrl) {
|
|
19
|
+
throw new Error('ActivityPub backend requires serverUrl in config');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Ensure server URL doesn't have trailing slash
|
|
23
|
+
this.serverUrl = this.serverUrl.replace(/\/$/, '');
|
|
24
|
+
|
|
25
|
+
// Try to fetch or create actor
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(`${this.serverUrl}/actor/${this.actorName}`, {
|
|
28
|
+
headers: { 'Accept': 'application/activity+json' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
const actor = await response.json();
|
|
33
|
+
this.actorId = actor.id;
|
|
34
|
+
this.publicKey = actor.publicKey?.id || actor.id;
|
|
35
|
+
} else {
|
|
36
|
+
// Try to create actor
|
|
37
|
+
await this._createActor();
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Server might not be running yet, try to create actor
|
|
41
|
+
await this._createActor();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async _createActor() {
|
|
46
|
+
try {
|
|
47
|
+
const response = await this._fetch(`${this.serverUrl}/api/actors`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
name: this.actorName,
|
|
52
|
+
apiKey: this.apiKey,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
const result = await response.json();
|
|
58
|
+
this.actorId = result.id;
|
|
59
|
+
this.publicKey = result.publicKey || result.id;
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Failed to create actor: ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// If server is not available, set placeholder values
|
|
65
|
+
this.actorId = `${this.serverUrl}/actor/${this.actorName}`;
|
|
66
|
+
this.publicKey = this.actorId;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
buildPath(appName, holonId, lensName, key = null) {
|
|
71
|
+
// Use same path format as other backends
|
|
72
|
+
const encodedHolon = encodeURIComponent(holonId);
|
|
73
|
+
const encodedLens = encodeURIComponent(lensName);
|
|
74
|
+
|
|
75
|
+
if (key) {
|
|
76
|
+
const encodedKey = encodeURIComponent(key);
|
|
77
|
+
return `${appName}/${encodedHolon}/${encodedLens}/${encodedKey}`;
|
|
78
|
+
}
|
|
79
|
+
return `${appName}/${encodedHolon}/${encodedLens}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async write(path, data, options = {}) {
|
|
83
|
+
const response = await this._fetch(`${this.serverUrl}/api/data`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
path,
|
|
88
|
+
data,
|
|
89
|
+
actorName: this.actorName,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return response.ok;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async read(path, options = {}) {
|
|
97
|
+
const response = await this._fetch(`${this.serverUrl}/api/data/${path}`);
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
if (response.status === 404) return null;
|
|
101
|
+
throw new Error(`Read failed: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
return data._deleted ? null : data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async readAll(path, options = {}) {
|
|
109
|
+
const response = await this._fetch(`${this.serverUrl}/api/data/${path}`);
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
if (response.status === 404) return [];
|
|
113
|
+
throw new Error(`ReadAll failed: ${response.status}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
return Array.isArray(data) ? data.filter(item => !item._deleted) : [data];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async update(path, updates) {
|
|
121
|
+
// Read existing, merge, write back
|
|
122
|
+
const existing = await this.read(path);
|
|
123
|
+
if (!existing) return false;
|
|
124
|
+
|
|
125
|
+
const merged = { ...existing, ...updates };
|
|
126
|
+
return this.write(path, merged);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async delete(path) {
|
|
130
|
+
const response = await this._fetch(
|
|
131
|
+
`${this.serverUrl}/api/data/${path}?actor=${this.actorName}`,
|
|
132
|
+
{ method: 'DELETE' }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return response.ok;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async deleteAll(path) {
|
|
139
|
+
const items = await this.readAll(path);
|
|
140
|
+
let count = 0;
|
|
141
|
+
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
if (item && item.id) {
|
|
144
|
+
await this.delete(`${path}/${item.id}`);
|
|
145
|
+
count++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { success: true, count };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async subscribe(path, callback, options = {}) {
|
|
153
|
+
// Use Server-Sent Events for real-time updates
|
|
154
|
+
const url = `${this.serverUrl}/subscribe?path=${encodeURIComponent(path)}`;
|
|
155
|
+
|
|
156
|
+
// Use EventSource if available (browser) or polyfill
|
|
157
|
+
let eventSource;
|
|
158
|
+
|
|
159
|
+
if (typeof EventSource !== 'undefined') {
|
|
160
|
+
eventSource = new EventSource(url);
|
|
161
|
+
} else {
|
|
162
|
+
// In Node.js, we need to use a different approach
|
|
163
|
+
// For now, return a polling-based subscription
|
|
164
|
+
return this._pollSubscribe(path, callback, options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
eventSource.addEventListener('update', (event) => {
|
|
168
|
+
try {
|
|
169
|
+
const { path: eventPath, data } = JSON.parse(event.data);
|
|
170
|
+
const key = eventPath.split('/').pop();
|
|
171
|
+
callback(data, key);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('ActivityPub subscription error:', error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
eventSource.onerror = (error) => {
|
|
178
|
+
console.error('ActivityPub SSE error:', error);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
unsubscribe: () => eventSource.close(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async _pollSubscribe(path, callback, options = {}) {
|
|
187
|
+
// Fallback polling for Node.js environments
|
|
188
|
+
const interval = options.pollInterval || 5000;
|
|
189
|
+
let lastTimestamp = Date.now();
|
|
190
|
+
let running = true;
|
|
191
|
+
|
|
192
|
+
const poll = async () => {
|
|
193
|
+
while (running) {
|
|
194
|
+
try {
|
|
195
|
+
const items = await this.readAll(path);
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
if (item._meta?.timestamp > lastTimestamp) {
|
|
198
|
+
const key = item.id || item._meta?.path?.split('/').pop();
|
|
199
|
+
callback(item, key);
|
|
200
|
+
lastTimestamp = item._meta.timestamp;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Polling error:', error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Start polling
|
|
212
|
+
poll();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
unsubscribe: () => { running = false; },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async exportData(pathPrefix = '') {
|
|
220
|
+
const items = await this.readAll(pathPrefix || this.actorName);
|
|
221
|
+
|
|
222
|
+
return items.map(item => ({
|
|
223
|
+
path: item._meta?.path || `${pathPrefix}/${item.id}`,
|
|
224
|
+
data: item,
|
|
225
|
+
timestamp: item._meta?.timestamp || Date.now(),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async importData(records, options = {}) {
|
|
230
|
+
const results = { success: 0, failed: 0, errors: [] };
|
|
231
|
+
|
|
232
|
+
for (const record of records) {
|
|
233
|
+
try {
|
|
234
|
+
await this.write(record.path, record.data);
|
|
235
|
+
results.success++;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
results.failed++;
|
|
238
|
+
results.errors.push({ path: record.path, error: error.message });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async _fetch(url, options = {}) {
|
|
246
|
+
const headers = options.headers || {};
|
|
247
|
+
|
|
248
|
+
// Add authorization if we have an API key
|
|
249
|
+
if (this.apiKey) {
|
|
250
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return fetch(url, { ...options, headers });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
close() {
|
|
257
|
+
// Nothing to close for HTTP client
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getStatus() {
|
|
261
|
+
return {
|
|
262
|
+
type: 'activitypub',
|
|
263
|
+
serverUrl: this.serverUrl,
|
|
264
|
+
actorId: this.actorId,
|
|
265
|
+
actorName: this.actorName,
|
|
266
|
+
publicKey: this.publicKey,
|
|
267
|
+
connected: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default ActivityPubBackend;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GunDB Storage Backend
|
|
3
|
+
* Wraps existing gun-wrapper.js as a StorageBackend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageBackend } from '../backend-interface.js';
|
|
7
|
+
import * as wrapper from '../gun-wrapper.js';
|
|
8
|
+
import { gunPromise, gunPut, gunMap, gunCollect } from '../gun-async.js';
|
|
9
|
+
|
|
10
|
+
export class GunDBBackend extends StorageBackend {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super(config);
|
|
13
|
+
this.gun = null;
|
|
14
|
+
this.keyPair = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async init() {
|
|
18
|
+
// Dynamically import Gun to avoid issues if not installed
|
|
19
|
+
let Gun;
|
|
20
|
+
try {
|
|
21
|
+
const gunModule = await import('gun');
|
|
22
|
+
Gun = gunModule.default || gunModule;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'GunDB backend requires the "gun" package. Install it with: npm install gun'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const gunConfig = {
|
|
30
|
+
peers: this.config.peers || [],
|
|
31
|
+
radisk: this.config.radisk !== false,
|
|
32
|
+
localStorage: this.config.localStorage !== false,
|
|
33
|
+
file: this.config.dataDir,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.gun = Gun(gunConfig);
|
|
37
|
+
|
|
38
|
+
// Generate or use provided key pair using Gun's SEA
|
|
39
|
+
try {
|
|
40
|
+
const SEA = Gun.SEA;
|
|
41
|
+
if (SEA) {
|
|
42
|
+
if (this.config.privateKey) {
|
|
43
|
+
// Try to restore from existing key
|
|
44
|
+
this.keyPair = { priv: this.config.privateKey, pub: this.config.publicKey };
|
|
45
|
+
this.publicKey = this.config.publicKey || this.config.privateKey.substring(0, 32);
|
|
46
|
+
} else {
|
|
47
|
+
// Generate new key pair
|
|
48
|
+
this.keyPair = await SEA.pair();
|
|
49
|
+
this.publicKey = this.keyPair.pub;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// SEA not available, use simple identifier
|
|
53
|
+
this.publicKey = this.config.appName || 'gundb-user';
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// SEA might not be loaded
|
|
57
|
+
this.publicKey = this.config.appName || 'gundb-user';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
buildPath(appName, holonId, lensName, key = null) {
|
|
62
|
+
return wrapper.buildPath(appName, holonId, lensName, key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async write(path, data, options = {}) {
|
|
66
|
+
return wrapper.write(this.gun, path, data);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async read(path, options = {}) {
|
|
70
|
+
return wrapper.read(this.gun, path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async readAll(path, options = {}) {
|
|
74
|
+
return wrapper.readAll(this.gun, path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async update(path, updates) {
|
|
78
|
+
return wrapper.update(this.gun, path, updates);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delete(path) {
|
|
82
|
+
return wrapper.deleteData(this.gun, path);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async deleteAll(path) {
|
|
86
|
+
// Gun doesn't have a native deleteAll, so we iterate and delete
|
|
87
|
+
const items = await this.readAll(path);
|
|
88
|
+
let count = 0;
|
|
89
|
+
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
if (item && item.id) {
|
|
92
|
+
const itemPath = `${path}/${item.id}`;
|
|
93
|
+
await wrapper.deleteData(this.gun, itemPath);
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { success: true, count };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async subscribe(path, callback, options = {}) {
|
|
102
|
+
// Determine if this is a prefix subscription (no key) or single item
|
|
103
|
+
const pathParts = path.split('/');
|
|
104
|
+
const isPrefix = pathParts.length <= 3;
|
|
105
|
+
|
|
106
|
+
if (isPrefix) {
|
|
107
|
+
// Subscribe to all items under this prefix
|
|
108
|
+
const ref = this.gun.get(path);
|
|
109
|
+
const handlers = [];
|
|
110
|
+
|
|
111
|
+
ref.map().on((data, key) => {
|
|
112
|
+
if (data && !key.startsWith('_') && !data._deleted) {
|
|
113
|
+
callback(data, key);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Store reference for cleanup
|
|
118
|
+
handlers.push(ref);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
unsubscribe: () => {
|
|
122
|
+
handlers.forEach(h => {
|
|
123
|
+
try {
|
|
124
|
+
h.off();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Ignore cleanup errors
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
// Subscribe to single item
|
|
133
|
+
const unsubFn = wrapper.subscribe(this.gun, path, callback);
|
|
134
|
+
return {
|
|
135
|
+
unsubscribe: typeof unsubFn === 'function' ? unsubFn : () => {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async exportData(pathPrefix = '') {
|
|
141
|
+
const records = [];
|
|
142
|
+
|
|
143
|
+
// Get the app-level data
|
|
144
|
+
const appName = this.config.appName || 'holosphere';
|
|
145
|
+
const basePath = pathPrefix || appName;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const items = await gunMap(this.gun.get(basePath), 2000);
|
|
149
|
+
|
|
150
|
+
for (const [key, data] of Object.entries(items)) {
|
|
151
|
+
if (!key.startsWith('_') && data && typeof data === 'object' && !data._deleted) {
|
|
152
|
+
// Recursively collect nested data
|
|
153
|
+
await this._collectRecords(basePath, key, data, records);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.warn('Export data error:', error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return records;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _collectRecords(basePath, key, data, records) {
|
|
164
|
+
const currentPath = `${basePath}/${key}`;
|
|
165
|
+
|
|
166
|
+
if (data.id) {
|
|
167
|
+
// This is a leaf data node
|
|
168
|
+
records.push({
|
|
169
|
+
path: currentPath,
|
|
170
|
+
data: this._cleanGunData(data),
|
|
171
|
+
timestamp: data._meta?.timestamp || Date.now(),
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
// This might be a nested structure, explore further
|
|
175
|
+
try {
|
|
176
|
+
const nested = await gunMap(this.gun.get(currentPath), 500);
|
|
177
|
+
for (const [nestedKey, nestedData] of Object.entries(nested)) {
|
|
178
|
+
if (!nestedKey.startsWith('_') && nestedData) {
|
|
179
|
+
await this._collectRecords(currentPath, nestedKey, nestedData, records);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Ignore nested exploration errors
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_cleanGunData(data) {
|
|
189
|
+
// Remove Gun's internal metadata
|
|
190
|
+
const cleaned = { ...data };
|
|
191
|
+
delete cleaned['_'];
|
|
192
|
+
return cleaned;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async importData(records, options = {}) {
|
|
196
|
+
const results = { success: 0, failed: 0, errors: [] };
|
|
197
|
+
|
|
198
|
+
for (const record of records) {
|
|
199
|
+
try {
|
|
200
|
+
await wrapper.write(this.gun, record.path, record.data);
|
|
201
|
+
results.success++;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
results.failed++;
|
|
204
|
+
results.errors.push({ path: record.path, error: error.message });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
close() {
|
|
212
|
+
// Gun doesn't have explicit close, but we can try to clean up
|
|
213
|
+
if (this.gun) {
|
|
214
|
+
try {
|
|
215
|
+
// Attempt to close any connections
|
|
216
|
+
this.gun.off();
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// Ignore cleanup errors
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getStatus() {
|
|
224
|
+
return {
|
|
225
|
+
type: 'gundb',
|
|
226
|
+
publicKey: this.publicKey,
|
|
227
|
+
peers: this.config.peers || [],
|
|
228
|
+
connected: !!this.gun,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default GunDBBackend;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nostr Storage Backend
|
|
3
|
+
* Wraps existing nostr-wrapper.js and nostr-client.js as a StorageBackend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageBackend } from '../backend-interface.js';
|
|
7
|
+
import { createClient } from '../nostr-client.js';
|
|
8
|
+
import * as wrapper from '../nostr-wrapper.js';
|
|
9
|
+
|
|
10
|
+
export class NostrBackend extends StorageBackend {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super(config);
|
|
13
|
+
this.client = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async init() {
|
|
17
|
+
this.client = createClient({
|
|
18
|
+
relays: this.config.relays || ['wss://relay.holons.io'],
|
|
19
|
+
privateKey: this.config.privateKey,
|
|
20
|
+
enableReconnect: this.config.enableReconnect !== false,
|
|
21
|
+
enablePing: this.config.enablePing !== false,
|
|
22
|
+
appName: this.config.appName,
|
|
23
|
+
radisk: this.config.radisk !== false,
|
|
24
|
+
persistence: this.config.persistence !== false,
|
|
25
|
+
dataDir: this.config.dataDir,
|
|
26
|
+
backgroundSync: this.config.backgroundSync,
|
|
27
|
+
syncInterval: this.config.syncInterval,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Wait for client initialization
|
|
31
|
+
await this.client._initReady;
|
|
32
|
+
this.publicKey = this.client.publicKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
buildPath(appName, holonId, lensName, key = null) {
|
|
36
|
+
return wrapper.buildPath(appName, holonId, lensName, key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async write(path, data, options = {}) {
|
|
40
|
+
return wrapper.write(this.client, path, data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async read(path, options = {}) {
|
|
44
|
+
return wrapper.read(this.client, path, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async readAll(path, options = {}) {
|
|
48
|
+
return wrapper.readAll(this.client, path, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async update(path, updates) {
|
|
52
|
+
return wrapper.update(this.client, path, updates);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async delete(path) {
|
|
56
|
+
return wrapper.deleteData(this.client, path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async deleteAll(path) {
|
|
60
|
+
return wrapper.deleteAll(this.client, path);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async subscribe(path, callback, options = {}) {
|
|
64
|
+
return wrapper.subscribe(this.client, path, callback, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async exportData(pathPrefix = '') {
|
|
68
|
+
// Wait for client to be ready
|
|
69
|
+
await this.client._initReady;
|
|
70
|
+
|
|
71
|
+
// Query all events for this author
|
|
72
|
+
const events = await this.client.query({
|
|
73
|
+
kinds: [30000],
|
|
74
|
+
authors: [this.publicKey],
|
|
75
|
+
limit: 10000,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return events
|
|
79
|
+
.filter(e => {
|
|
80
|
+
const dTag = e.tags.find(t => t[0] === 'd');
|
|
81
|
+
if (!dTag) return false;
|
|
82
|
+
// Filter by prefix if provided
|
|
83
|
+
if (pathPrefix && !dTag[1].startsWith(pathPrefix)) return false;
|
|
84
|
+
return true;
|
|
85
|
+
})
|
|
86
|
+
.map(e => {
|
|
87
|
+
const dTag = e.tags.find(t => t[0] === 'd');
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(e.content);
|
|
91
|
+
} catch {
|
|
92
|
+
data = { content: e.content };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
path: dTag[1],
|
|
96
|
+
data,
|
|
97
|
+
timestamp: e.created_at * 1000,
|
|
98
|
+
author: e.pubkey,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.filter(record => !record.data._deleted); // Filter out deleted items
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async importData(records, options = {}) {
|
|
105
|
+
const results = { success: 0, failed: 0, errors: [] };
|
|
106
|
+
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
try {
|
|
109
|
+
await wrapper.write(this.client, record.path, record.data);
|
|
110
|
+
results.success++;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
results.failed++;
|
|
113
|
+
results.errors.push({ path: record.path, error: error.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
close() {
|
|
121
|
+
if (this.client) {
|
|
122
|
+
this.client.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getStatus() {
|
|
127
|
+
return {
|
|
128
|
+
type: 'nostr',
|
|
129
|
+
publicKey: this.publicKey,
|
|
130
|
+
relays: this.config.relays || [],
|
|
131
|
+
connected: !!this.client,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default NostrBackend;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser stub for FileSystemStorage
|
|
3
|
+
* Throws an error if used in browser environment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PersistentStorage } from './persistent-storage.js';
|
|
7
|
+
|
|
8
|
+
export class FileSystemStorage extends PersistentStorage {
|
|
9
|
+
constructor(baseDir = null) {
|
|
10
|
+
super();
|
|
11
|
+
throw new Error('FileSystemStorage is not available in browser environments. Use IndexedDBStorage or MemoryStorage instead.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async init(namespace) {
|
|
15
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async put(key, event) {
|
|
19
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(key) {
|
|
23
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getAll(prefix) {
|
|
27
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async delete(key) {
|
|
31
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async clear() {
|
|
35
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async close() {
|
|
39
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
40
|
+
}
|
|
41
|
+
}
|