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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system storage adapter for Node.js
|
|
3
|
+
* Stores events as JSON files in a directory structure
|
|
4
|
+
* This module is only loaded in Node.js environments
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Guard against browser environments
|
|
8
|
+
if (typeof window !== 'undefined') {
|
|
9
|
+
throw new Error('FileSystemStorage is not available in browser environments');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
import { PersistentStorage } from './persistent-storage.js';
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
export class FileSystemStorage extends PersistentStorage {
|
|
18
|
+
constructor(baseDir = null) {
|
|
19
|
+
super();
|
|
20
|
+
this.baseDir = baseDir || path.join(os.homedir(), '.holosphere');
|
|
21
|
+
this.namespace = null;
|
|
22
|
+
this.storageDir = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init(namespace) {
|
|
26
|
+
this.namespace = namespace;
|
|
27
|
+
this.storageDir = path.join(this.baseDir, namespace);
|
|
28
|
+
|
|
29
|
+
// Create directory if it doesn't exist
|
|
30
|
+
try {
|
|
31
|
+
await fs.mkdir(this.storageDir, { recursive: true });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code !== 'EEXIST') {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert key to safe filename
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
_keyToFilename(key) {
|
|
44
|
+
// Replace unsafe characters
|
|
45
|
+
return key.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get full file path for key
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
_getFilePath(key) {
|
|
53
|
+
return path.join(this.storageDir, this._keyToFilename(key));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async put(key, event) {
|
|
57
|
+
const filePath = this._getFilePath(key);
|
|
58
|
+
const data = JSON.stringify(event, null, 2);
|
|
59
|
+
await fs.writeFile(filePath, data, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async get(key) {
|
|
63
|
+
const filePath = this._getFilePath(key);
|
|
64
|
+
try {
|
|
65
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
66
|
+
return JSON.parse(data);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT') {
|
|
69
|
+
return null; // File doesn't exist
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getAll(prefix) {
|
|
76
|
+
const results = [];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const files = await fs.readdir(this.storageDir);
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
if (!file.endsWith('.json')) continue;
|
|
83
|
+
|
|
84
|
+
const filePath = path.join(this.storageDir, file);
|
|
85
|
+
try {
|
|
86
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
87
|
+
const event = JSON.parse(data);
|
|
88
|
+
|
|
89
|
+
// Check if this event matches the prefix
|
|
90
|
+
// The key is stored in the event's d-tag or we derive it from filename
|
|
91
|
+
const dTag = event.tags?.find(t => t[0] === 'd');
|
|
92
|
+
const key = dTag ? dTag[1] : file.replace('.json', '').replace(/_/g, '/');
|
|
93
|
+
|
|
94
|
+
if (key.startsWith(prefix)) {
|
|
95
|
+
results.push(event);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// Skip files that can't be read or parsed (silently)
|
|
99
|
+
// This is common for outbox queue entries and other non-event files
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code !== 'ENOENT') {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async delete(key) {
|
|
112
|
+
const filePath = this._getFilePath(key);
|
|
113
|
+
try {
|
|
114
|
+
await fs.unlink(filePath);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code !== 'ENOENT') {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async clear() {
|
|
123
|
+
try {
|
|
124
|
+
const files = await fs.readdir(this.storageDir);
|
|
125
|
+
await Promise.all(
|
|
126
|
+
files.map(file => fs.unlink(path.join(this.storageDir, file)))
|
|
127
|
+
);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error.code !== 'ENOENT') {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async close() {
|
|
136
|
+
// Nothing to close for file system storage
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Data Operations
|
|
3
|
+
* Non-location-specific storage (FR-044, FR-045)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { write, read, readAll, update, deleteData, deleteAll } from './nostr-wrapper.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Write data to global table
|
|
10
|
+
* @param {Object} client - Nostr client instance
|
|
11
|
+
* @param {string} appname - Application namespace
|
|
12
|
+
* @param {string} table - Global table name
|
|
13
|
+
* @param {Object} data - Data to write (must contain id field)
|
|
14
|
+
* @returns {Promise<boolean>} Success indicator
|
|
15
|
+
*/
|
|
16
|
+
export async function writeGlobal(client, appname, table, data) {
|
|
17
|
+
// Auto-generate ID if not provided
|
|
18
|
+
if (!data.id) {
|
|
19
|
+
data.id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const path = `${appname}/${table}/${data.id}`;
|
|
23
|
+
return write(client, path, data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read data from global table
|
|
28
|
+
* @param {Object} client - Nostr client instance
|
|
29
|
+
* @param {string} appname - Application namespace
|
|
30
|
+
* @param {string} table - Global table name
|
|
31
|
+
* @param {string} key - Data key (optional, if not provided returns all)
|
|
32
|
+
* @returns {Promise<Object|Object[]|null>} Data or null
|
|
33
|
+
*/
|
|
34
|
+
export async function readGlobal(client, appname, table, key = null) {
|
|
35
|
+
if (key) {
|
|
36
|
+
const path = `${appname}/${table}/${key}`;
|
|
37
|
+
return read(client, path);
|
|
38
|
+
} else {
|
|
39
|
+
const path = `${appname}/${table}`;
|
|
40
|
+
return readAll(client, path);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update data in global table
|
|
46
|
+
* @param {Object} client - Nostr client instance
|
|
47
|
+
* @param {string} appname - Application namespace
|
|
48
|
+
* @param {string} table - Global table name
|
|
49
|
+
* @param {string} key - Data key
|
|
50
|
+
* @param {Object} updates - Fields to update
|
|
51
|
+
* @returns {Promise<boolean>} Success indicator
|
|
52
|
+
*/
|
|
53
|
+
export async function updateGlobal(client, appname, table, key, updates) {
|
|
54
|
+
const path = `${appname}/${table}/${key}`;
|
|
55
|
+
return update(client, path, updates);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete data from global table
|
|
60
|
+
* @param {Object} client - Nostr client instance
|
|
61
|
+
* @param {string} appname - Application namespace
|
|
62
|
+
* @param {string} table - Global table name
|
|
63
|
+
* @param {string} key - Data key
|
|
64
|
+
* @returns {Promise<boolean>} Success indicator
|
|
65
|
+
*/
|
|
66
|
+
export async function deleteGlobal(client, appname, table, key) {
|
|
67
|
+
const path = `${appname}/${table}/${key}`;
|
|
68
|
+
return deleteData(client, path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Delete all data from global table
|
|
73
|
+
* @param {Object} client - Nostr client instance
|
|
74
|
+
* @param {string} appname - Application namespace
|
|
75
|
+
* @param {string} table - Global table name
|
|
76
|
+
* @returns {Promise<Object>} Deletion results
|
|
77
|
+
*/
|
|
78
|
+
export async function deleteAllGlobal(client, appname, table) {
|
|
79
|
+
const path = `${appname}/${table}`;
|
|
80
|
+
return deleteAll(client, path);
|
|
81
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gun Async Utilities
|
|
3
|
+
* Provides Promise-based wrappers and async patterns for Gun operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get data from Gun using native .then() support
|
|
8
|
+
* @param {Object} gunChain - Gun chain reference
|
|
9
|
+
* @param {number} timeout - Timeout in ms (default 1000ms)
|
|
10
|
+
* @returns {Promise<any>} Promise resolving to data
|
|
11
|
+
*/
|
|
12
|
+
export function gunPromise(gunChain, timeout = 1000) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let settled = false;
|
|
15
|
+
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
if (!settled) {
|
|
18
|
+
settled = true;
|
|
19
|
+
resolve(null);
|
|
20
|
+
}
|
|
21
|
+
}, timeout);
|
|
22
|
+
|
|
23
|
+
gunChain.once((data) => {
|
|
24
|
+
if (!settled) {
|
|
25
|
+
settled = true;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
resolve(data || null);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wait for Gun write acknowledgement
|
|
35
|
+
* @param {Object} gunChain - Gun chain reference
|
|
36
|
+
* @param {any} data - Data to write
|
|
37
|
+
* @param {number} timeout - Timeout in ms (default 1000ms)
|
|
38
|
+
* @returns {Promise<Object>} Promise resolving to ack object
|
|
39
|
+
*/
|
|
40
|
+
export function gunPut(gunChain, data, timeout = 1000) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
let settled = false;
|
|
43
|
+
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
if (!settled) {
|
|
46
|
+
settled = true;
|
|
47
|
+
resolve({ ok: true, timeout: true });
|
|
48
|
+
}
|
|
49
|
+
}, timeout);
|
|
50
|
+
|
|
51
|
+
gunChain.put(data, (ack) => {
|
|
52
|
+
if (!settled) {
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (ack.err) {
|
|
56
|
+
reject(new Error(ack.err));
|
|
57
|
+
} else {
|
|
58
|
+
resolve(ack);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all items from a Gun map
|
|
67
|
+
* @param {Object} gunChain - Gun chain reference
|
|
68
|
+
* @param {number} timeout - Timeout in ms (default 300ms)
|
|
69
|
+
* @returns {Promise<Object>} Promise resolving to map of items
|
|
70
|
+
*/
|
|
71
|
+
export async function gunMap(gunChain, timeout = 300) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const items = {};
|
|
74
|
+
|
|
75
|
+
gunChain.map().once((data, key) => {
|
|
76
|
+
if (data && !key.startsWith('_')) {
|
|
77
|
+
items[key] = data;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
resolve(items);
|
|
83
|
+
}, timeout);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load full chain data (follows all references)
|
|
89
|
+
* @param {Object} gunChain - Gun chain reference
|
|
90
|
+
* @param {number} depth - Max depth to traverse (default 3)
|
|
91
|
+
* @returns {Promise<any>} Promise resolving to loaded data
|
|
92
|
+
*/
|
|
93
|
+
export async function gunLoad(gunChain, depth = 3) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
let result = null;
|
|
96
|
+
|
|
97
|
+
gunChain.load((data) => {
|
|
98
|
+
result = data;
|
|
99
|
+
}, { wait: 100 });
|
|
100
|
+
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
resolve(result);
|
|
103
|
+
}, 100 * depth);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Async iterator for Gun map
|
|
109
|
+
* @param {Object} gunChain - Gun chain reference
|
|
110
|
+
* @returns {AsyncGenerator} Async generator yielding [key, value] pairs
|
|
111
|
+
*/
|
|
112
|
+
export async function* gunMapIterator(gunChain) {
|
|
113
|
+
const items = await gunMap(gunChain);
|
|
114
|
+
for (const [key, value] of Object.entries(items)) {
|
|
115
|
+
yield [key, value];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Collect Gun on() stream into array over time
|
|
121
|
+
* @param {Object} gunChain - Gun chain reference
|
|
122
|
+
* @param {number} duration - Collection duration in ms
|
|
123
|
+
* @returns {Promise<Array>} Promise resolving to array of data
|
|
124
|
+
*/
|
|
125
|
+
export async function gunCollect(gunChain, duration = 500) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const results = [];
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
|
|
130
|
+
const listener = gunChain.on((data, key) => {
|
|
131
|
+
if (data && !seen.has(key)) {
|
|
132
|
+
seen.add(key);
|
|
133
|
+
results.push({ key, data });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
listener.off();
|
|
139
|
+
resolve(results);
|
|
140
|
+
}, duration);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Wait for specific condition on Gun data
|
|
146
|
+
* @param {Object} gunChain - Gun chain reference
|
|
147
|
+
* @param {Function} predicate - Condition function (data) => boolean
|
|
148
|
+
* @param {number} timeout - Timeout in ms (default 5000ms)
|
|
149
|
+
* @returns {Promise<any>} Promise resolving when condition is met
|
|
150
|
+
*/
|
|
151
|
+
export async function gunWaitFor(gunChain, predicate, timeout = 5000) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
let timeoutId;
|
|
154
|
+
let listener;
|
|
155
|
+
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
158
|
+
if (listener) listener.off();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
listener = gunChain.on((data) => {
|
|
162
|
+
if (predicate(data)) {
|
|
163
|
+
cleanup();
|
|
164
|
+
resolve(data);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
timeoutId = setTimeout(() => {
|
|
169
|
+
cleanup();
|
|
170
|
+
reject(new Error('Timeout waiting for condition'));
|
|
171
|
+
}, timeout);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Batch read multiple Gun paths
|
|
177
|
+
* @param {Object} gun - Gun instance
|
|
178
|
+
* @param {string[]} paths - Array of paths to read
|
|
179
|
+
* @returns {Promise<Object>} Object mapping paths to data
|
|
180
|
+
*/
|
|
181
|
+
export async function gunBatchGet(gun, paths) {
|
|
182
|
+
const promises = paths.map(async (path) => {
|
|
183
|
+
const data = await gunPromise(gun.get(path));
|
|
184
|
+
return [path, data];
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const results = await Promise.all(promises);
|
|
188
|
+
return Object.fromEntries(results);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Batch write multiple Gun paths
|
|
193
|
+
* @param {Object} gun - Gun instance
|
|
194
|
+
* @param {Object} pathDataMap - Object mapping paths to data
|
|
195
|
+
* @returns {Promise<Object>} Object mapping paths to ack results
|
|
196
|
+
*/
|
|
197
|
+
export async function gunBatchPut(gun, pathDataMap) {
|
|
198
|
+
const promises = Object.entries(pathDataMap).map(async ([path, data]) => {
|
|
199
|
+
const ack = await gunPut(gun.get(path), data);
|
|
200
|
+
return [path, ack];
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const results = await Promise.all(promises);
|
|
204
|
+
return Object.fromEntries(results);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Retry Gun operation with exponential backoff
|
|
209
|
+
* @param {Function} operation - Async function to retry
|
|
210
|
+
* @param {number} maxRetries - Max retry attempts (default 3)
|
|
211
|
+
* @param {number} baseDelay - Base delay in ms (default 100ms)
|
|
212
|
+
* @returns {Promise<any>} Promise resolving to operation result
|
|
213
|
+
*/
|
|
214
|
+
export async function gunRetry(operation, maxRetries = 3, baseDelay = 100) {
|
|
215
|
+
let lastError;
|
|
216
|
+
|
|
217
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
218
|
+
try {
|
|
219
|
+
return await operation();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
lastError = error;
|
|
222
|
+
if (attempt < maxRetries) {
|
|
223
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
224
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw lastError;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create async stream from Gun on() callback
|
|
234
|
+
* @param {Object} gunChain - Gun chain reference
|
|
235
|
+
* @returns {Object} Stream object with async iteration support
|
|
236
|
+
*/
|
|
237
|
+
export function gunStream(gunChain) {
|
|
238
|
+
let listeners = [];
|
|
239
|
+
let buffer = [];
|
|
240
|
+
let ended = false;
|
|
241
|
+
|
|
242
|
+
const stream = {
|
|
243
|
+
[Symbol.asyncIterator]() {
|
|
244
|
+
return {
|
|
245
|
+
async next() {
|
|
246
|
+
if (buffer.length > 0) {
|
|
247
|
+
return { value: buffer.shift(), done: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (ended) {
|
|
251
|
+
return { done: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wait for next value
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
listeners.push((data) => {
|
|
257
|
+
resolve({ value: data, done: false });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
stop() {
|
|
265
|
+
ended = true;
|
|
266
|
+
if (this.listener) this.listener.off();
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
stream.listener = gunChain.on((data, key) => {
|
|
271
|
+
const item = { key, data };
|
|
272
|
+
if (listeners.length > 0) {
|
|
273
|
+
const listener = listeners.shift();
|
|
274
|
+
listener(item);
|
|
275
|
+
} else {
|
|
276
|
+
buffer.push(item);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return stream;
|
|
281
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GunDB Storage Wrapper with radisk persistence
|
|
3
|
+
* Handles path construction and CRUD operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { gunPromise, gunPut, gunCollect } from './gun-async.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build Gun path from components
|
|
10
|
+
* @param {string} appname - Application namespace
|
|
11
|
+
* @param {string} holon - Holon ID (H3 or URI)
|
|
12
|
+
* @param {string} lens - Lens name
|
|
13
|
+
* @param {string} key - Data key (optional)
|
|
14
|
+
* @returns {string} Gun path
|
|
15
|
+
*/
|
|
16
|
+
export function buildPath(appname, holon, lens, key = null) {
|
|
17
|
+
// Encode components to handle special characters
|
|
18
|
+
const encodedHolon = encodePathComponent(holon);
|
|
19
|
+
const encodedLens = encodePathComponent(lens);
|
|
20
|
+
|
|
21
|
+
if (key) {
|
|
22
|
+
const encodedKey = encodePathComponent(key);
|
|
23
|
+
return `${appname}/${encodedHolon}/${encodedLens}/${encodedKey}`;
|
|
24
|
+
}
|
|
25
|
+
return `${appname}/${encodedHolon}/${encodedLens}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Encode path component to handle special characters
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
function encodePathComponent(component) {
|
|
33
|
+
return encodeURIComponent(component).replace(/%2F/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Write data to Gun with radisk persistence
|
|
38
|
+
* @param {Object} gun - Gun instance
|
|
39
|
+
* @param {string} path - Gun path
|
|
40
|
+
* @param {Object} data - Data to write
|
|
41
|
+
* @returns {Promise<boolean>} Success indicator
|
|
42
|
+
*/
|
|
43
|
+
export async function write(gun, path, data) {
|
|
44
|
+
try {
|
|
45
|
+
await gunPut(gun.get(path), data, 2000);
|
|
46
|
+
// Delay to allow Gun to propagate the write (50ms for better reliability)
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read data from Gun
|
|
56
|
+
* @param {Object} gun - Gun instance
|
|
57
|
+
* @param {string} path - Gun path
|
|
58
|
+
* @returns {Promise<Object|null>} Data or null if not found
|
|
59
|
+
*/
|
|
60
|
+
export async function read(gun, path) {
|
|
61
|
+
const data = await gunPromise(gun.get(path), 2000);
|
|
62
|
+
|
|
63
|
+
// Return null if deleted or not found
|
|
64
|
+
if (!data || data._deleted) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read all data under a path (lens query)
|
|
73
|
+
* @param {Object} gun - Gun instance
|
|
74
|
+
* @param {string} path - Gun path
|
|
75
|
+
* @returns {Promise<Object[]>} Array of data objects
|
|
76
|
+
*/
|
|
77
|
+
export async function readAll(gun, path) {
|
|
78
|
+
const results = await gunCollect(gun.get(path), 1000);
|
|
79
|
+
|
|
80
|
+
// Filter out deleted items and Gun metadata
|
|
81
|
+
return results
|
|
82
|
+
.filter(({ data, key }) => {
|
|
83
|
+
if (!data || typeof data !== 'object') return false;
|
|
84
|
+
if (key.startsWith('_')) return false;
|
|
85
|
+
if (data._deleted) return false;
|
|
86
|
+
return true;
|
|
87
|
+
})
|
|
88
|
+
.map(({ data }) => data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update data (merge fields)
|
|
93
|
+
* @param {Object} gun - Gun instance
|
|
94
|
+
* @param {string} path - Gun path
|
|
95
|
+
* @param {Object} updates - Fields to update
|
|
96
|
+
* @returns {Promise<boolean>} Success indicator
|
|
97
|
+
*/
|
|
98
|
+
export async function update(gun, path, updates) {
|
|
99
|
+
const existing = await gunPromise(gun.get(path));
|
|
100
|
+
|
|
101
|
+
if (!existing || !existing.id || existing._deleted) {
|
|
102
|
+
return false; // Not found or deleted
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove Gun metadata before merging
|
|
106
|
+
const cleanExisting = { ...existing };
|
|
107
|
+
delete cleanExisting['_'];
|
|
108
|
+
|
|
109
|
+
// Merge updates
|
|
110
|
+
const merged = { ...cleanExisting, ...updates };
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await gunPut(gun.get(path), merged);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Delete data (tombstone)
|
|
122
|
+
* @param {Object} gun - Gun instance
|
|
123
|
+
* @param {string} path - Gun path
|
|
124
|
+
* @returns {Promise<boolean>} Success indicator
|
|
125
|
+
*/
|
|
126
|
+
export async function deleteData(gun, path) {
|
|
127
|
+
try {
|
|
128
|
+
// Gun requires tombstone to be an object with _deleted flag
|
|
129
|
+
// First read existing data to preserve metadata
|
|
130
|
+
const existing = await gunPromise(gun.get(path));
|
|
131
|
+
if (!existing) {
|
|
132
|
+
return true; // Already deleted/doesn't exist
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Create tombstone object
|
|
136
|
+
const tombstone = {
|
|
137
|
+
id: existing.id,
|
|
138
|
+
_deleted: true,
|
|
139
|
+
_deletedAt: Date.now()
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await gunPut(gun.get(path), tombstone);
|
|
143
|
+
return true;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Delete all data under path prefix (tombstone)
|
|
151
|
+
* @param {Object} gun - Gun instance
|
|
152
|
+
* @param {string} path - Gun path prefix
|
|
153
|
+
* @returns {Promise<Object>} Deletion results { success: boolean, count: number }
|
|
154
|
+
*/
|
|
155
|
+
export async function deleteAll(gun, path) {
|
|
156
|
+
const items = await readAll(gun, path);
|
|
157
|
+
let count = 0;
|
|
158
|
+
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
if (item && item.id) {
|
|
161
|
+
const itemPath = `${path}/${item.id}`;
|
|
162
|
+
await deleteData(gun, itemPath);
|
|
163
|
+
count++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { success: true, count };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Subscribe to data changes
|
|
172
|
+
* @param {Object} gun - Gun instance
|
|
173
|
+
* @param {string} path - Gun path
|
|
174
|
+
* @param {Function} callback - Called on data changes
|
|
175
|
+
* @param {Object} options - Subscription options
|
|
176
|
+
* @param {boolean} options.prefix - Subscribe to all items under path (default: auto-detect)
|
|
177
|
+
* @returns {Object} Subscription object with unsubscribe method
|
|
178
|
+
*/
|
|
179
|
+
export function subscribe(gun, path, callback, options = {}) {
|
|
180
|
+
// Detect if this is a prefix subscription
|
|
181
|
+
const pathParts = path.split('/');
|
|
182
|
+
const isPrefix = options.prefix !== undefined ? options.prefix : pathParts.length <= 3;
|
|
183
|
+
|
|
184
|
+
if (isPrefix) {
|
|
185
|
+
// Subscribe to all items under this prefix
|
|
186
|
+
const ref = gun.get(path);
|
|
187
|
+
|
|
188
|
+
ref.map().on((data, key) => {
|
|
189
|
+
if (data && !key.startsWith('_') && !data._deleted) {
|
|
190
|
+
callback(data, key);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
unsubscribe: () => {
|
|
196
|
+
try {
|
|
197
|
+
ref.off();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// Ignore cleanup errors
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
204
|
+
// Subscribe to single item
|
|
205
|
+
const listener = gun.get(path).on((data, key) => {
|
|
206
|
+
if (data && !data._deleted) {
|
|
207
|
+
callback(data, key);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
unsubscribe: () => {
|
|
213
|
+
try {
|
|
214
|
+
listener.off();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
// Ignore cleanup errors
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|