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,578 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Import script for holosphere from GUN folder structure
|
|
5
|
+
*
|
|
6
|
+
* Folder structure expected:
|
|
7
|
+
* - 3-level: {appOrHolon}/{lens}/{id}.json
|
|
8
|
+
* - 4-level: {app}/{holon}/{lens}/{id}.json
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/import-holons.js [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --source <path> Source directory (default: ../gun/Holons)
|
|
15
|
+
* --subfolder <path> Subfolder within source (e.g., "Appname/holonid")
|
|
16
|
+
* --app <name> App name for holosphere (default: use folder name or 'imported')
|
|
17
|
+
* --dry-run Show what would be imported without actually importing
|
|
18
|
+
* --verbose Show detailed progress
|
|
19
|
+
* --levels <3|4> Folder depth: 3 or 4 (default: auto-detect)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs/promises';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { HoloSphere } from '../src/index.js';
|
|
25
|
+
import dotenv from 'dotenv';
|
|
26
|
+
|
|
27
|
+
// Load environment variables
|
|
28
|
+
dotenv.config();
|
|
29
|
+
|
|
30
|
+
// Parse command line arguments
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const options = {
|
|
33
|
+
source: '../Holons',
|
|
34
|
+
subfolder: null,
|
|
35
|
+
app: 'Holons',
|
|
36
|
+
dryRun: false,
|
|
37
|
+
verbose: true,
|
|
38
|
+
levels: 'auto',
|
|
39
|
+
relays: [process.env.HOLOSPHERE_RELAY || 'wss://relay.holons.io'],
|
|
40
|
+
persistence: true,
|
|
41
|
+
// Retry and rate limiting options
|
|
42
|
+
maxRetries: 3,
|
|
43
|
+
retryDelay: 1000,
|
|
44
|
+
writeDelay: 100,
|
|
45
|
+
verifyAndResync: true
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
switch (args[i]) {
|
|
50
|
+
case '--source':
|
|
51
|
+
options.source = args[++i];
|
|
52
|
+
break;
|
|
53
|
+
case '--subfolder':
|
|
54
|
+
options.subfolder = args[++i];
|
|
55
|
+
break;
|
|
56
|
+
case '--app':
|
|
57
|
+
options.app = args[++i];
|
|
58
|
+
break;
|
|
59
|
+
case '--dry-run':
|
|
60
|
+
options.dryRun = true;
|
|
61
|
+
break;
|
|
62
|
+
case '--verbose':
|
|
63
|
+
options.verbose = true;
|
|
64
|
+
break;
|
|
65
|
+
case '--relay':
|
|
66
|
+
options.relays = [args[++i]];
|
|
67
|
+
break;
|
|
68
|
+
case '--levels':
|
|
69
|
+
options.levels = args[++i];
|
|
70
|
+
break;
|
|
71
|
+
case '--help':
|
|
72
|
+
console.log(`
|
|
73
|
+
Import holons from GUN folder structure into holosphere
|
|
74
|
+
|
|
75
|
+
Usage: node scripts/import-holons.js [options]
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
--source <path> Source directory (default: ../Holons)
|
|
79
|
+
--subfolder <path> Subfolder within source (e.g., "Appname/holonid")
|
|
80
|
+
--app <name> App name for holosphere (default: use folder name or 'Holons')
|
|
81
|
+
--dry-run Show what would be imported without actually importing
|
|
82
|
+
--verbose Show detailed progress
|
|
83
|
+
--relay <url> Relay URL (default: from HOLOSPHERE_RELAY env or wss://relay.holons.io)
|
|
84
|
+
--levels <3|4> Folder depth: 3 or 4 (default: auto-detect)
|
|
85
|
+
--help Show this help message
|
|
86
|
+
|
|
87
|
+
Features:
|
|
88
|
+
- Automatic retry logic (up to 3 retries per item)
|
|
89
|
+
- Rate limiting to prevent overwhelming the relay (100ms between writes)
|
|
90
|
+
- Post-import verification and automatic resync of missing items
|
|
91
|
+
- All data is guaranteed to be on the relay when import completes
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
# Import from default location
|
|
95
|
+
node scripts/import-holons.js
|
|
96
|
+
|
|
97
|
+
# Import with specific app name
|
|
98
|
+
node scripts/import-holons.js --app myapp
|
|
99
|
+
|
|
100
|
+
# Dry run to see what would be imported
|
|
101
|
+
node scripts/import-holons.js --dry-run --verbose
|
|
102
|
+
|
|
103
|
+
# Import from custom location
|
|
104
|
+
node scripts/import-holons.js --source /path/to/holons
|
|
105
|
+
|
|
106
|
+
# Import from specific subfolder (recommended for single holon)
|
|
107
|
+
node scripts/import-holons.js --subfolder 235114395
|
|
108
|
+
`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Statistics
|
|
114
|
+
const stats = {
|
|
115
|
+
apps: new Set(),
|
|
116
|
+
holons: new Set(),
|
|
117
|
+
lenses: new Set(),
|
|
118
|
+
items: 0,
|
|
119
|
+
errors: 0,
|
|
120
|
+
skipped: 0,
|
|
121
|
+
retries: 0,
|
|
122
|
+
resynced: 0
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sleep helper for delays
|
|
127
|
+
*/
|
|
128
|
+
async function sleep(ms) {
|
|
129
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Write with retry logic
|
|
134
|
+
*/
|
|
135
|
+
async function writeWithRetry(hs, holonId, lensName, data, retries = 0) {
|
|
136
|
+
try {
|
|
137
|
+
if (holonId === 'global') {
|
|
138
|
+
await hs.writeGlobal(lensName, data);
|
|
139
|
+
} else {
|
|
140
|
+
await hs.write(holonId, lensName, data);
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (retries < options.maxRetries) {
|
|
145
|
+
stats.retries++;
|
|
146
|
+
if (options.verbose) {
|
|
147
|
+
console.log(` Retry ${retries + 1}/${options.maxRetries} for ${lensName}/${data.id}`);
|
|
148
|
+
}
|
|
149
|
+
await sleep(options.retryDelay);
|
|
150
|
+
return writeWithRetry(hs, holonId, lensName, data, retries + 1);
|
|
151
|
+
} else {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if a string is a valid H3 cell ID
|
|
159
|
+
*/
|
|
160
|
+
function isH3Cell(str) {
|
|
161
|
+
return /^[0-9a-f]{15}$/i.test(str);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Recursively scan directory and import data
|
|
166
|
+
*/
|
|
167
|
+
async function scanAndImport(dir, hs, depth = 0, context = {}) {
|
|
168
|
+
try {
|
|
169
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
170
|
+
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const fullPath = path.join(dir, entry.name);
|
|
173
|
+
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
// Skip special directories
|
|
176
|
+
if (entry.name.startsWith('.') || entry.name === '_holograms') {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update context based on depth
|
|
181
|
+
const newContext = { ...context };
|
|
182
|
+
|
|
183
|
+
if (depth === 0) {
|
|
184
|
+
// First level: if we have a holon from subfolder, treat this as lens
|
|
185
|
+
if (context.holon) {
|
|
186
|
+
newContext.lens = entry.name;
|
|
187
|
+
stats.lenses.add(entry.name);
|
|
188
|
+
} else {
|
|
189
|
+
// Otherwise, treat as app name or holon ID
|
|
190
|
+
newContext.appOrHolon = entry.name;
|
|
191
|
+
stats.apps.add(entry.name);
|
|
192
|
+
}
|
|
193
|
+
} else if (depth === 1) {
|
|
194
|
+
// Second level: could be holon (if 4-level) or lens (if 3-level)
|
|
195
|
+
// If we already have a lens, this is just a container folder - keep the lens
|
|
196
|
+
if (context.lens) {
|
|
197
|
+
// Don't change lens, just keep recursing
|
|
198
|
+
// This handles cases like quests/1002/ where 1002 is just a container
|
|
199
|
+
} else if (isH3Cell(entry.name)) {
|
|
200
|
+
// Auto-detect: if it's an H3 cell, treat as holon
|
|
201
|
+
newContext.holon = entry.name;
|
|
202
|
+
stats.holons.add(entry.name);
|
|
203
|
+
} else {
|
|
204
|
+
// Otherwise treat as lens
|
|
205
|
+
newContext.lens = entry.name;
|
|
206
|
+
stats.lenses.add(entry.name);
|
|
207
|
+
}
|
|
208
|
+
} else if (depth === 2) {
|
|
209
|
+
// Third level or deeper: if we have a lens, keep it (container folders)
|
|
210
|
+
// Otherwise set as lens (4-level structure)
|
|
211
|
+
if (!context.lens) {
|
|
212
|
+
newContext.lens = entry.name;
|
|
213
|
+
stats.lenses.add(entry.name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Recurse into subdirectory
|
|
218
|
+
await scanAndImport(fullPath, hs, depth + 1, newContext);
|
|
219
|
+
|
|
220
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
221
|
+
// Import JSON file
|
|
222
|
+
await importFile(fullPath, hs, context, entry.name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(`Error scanning directory ${dir}:`, error.message);
|
|
227
|
+
stats.errors++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Import a single JSON file into holosphere
|
|
233
|
+
*/
|
|
234
|
+
async function importFile(filePath, hs, context, filename) {
|
|
235
|
+
try {
|
|
236
|
+
// Read and parse JSON file
|
|
237
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
238
|
+
let data = JSON.parse(content);
|
|
239
|
+
|
|
240
|
+
// Handle double-encoded JSON strings
|
|
241
|
+
if (typeof data === 'string') {
|
|
242
|
+
try {
|
|
243
|
+
data = JSON.parse(data);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
// If it fails to parse again, keep it as a string value
|
|
246
|
+
// Wrap it in an object
|
|
247
|
+
data = { value: data };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle non-object data
|
|
252
|
+
if (typeof data !== 'object' || data === null) {
|
|
253
|
+
data = { value: data };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Extract ID from filename (remove .json extension)
|
|
257
|
+
const id = path.basename(filename, '.json');
|
|
258
|
+
|
|
259
|
+
// Ensure data has an id field
|
|
260
|
+
if (!data.id) {
|
|
261
|
+
data.id = id;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Determine app, holon, and lens
|
|
265
|
+
let appName, holonId, lensName;
|
|
266
|
+
|
|
267
|
+
if (context.holon) {
|
|
268
|
+
// 4-level structure: app/holon/lens/id
|
|
269
|
+
appName = context.appOrHolon || options.app || 'imported';
|
|
270
|
+
holonId = context.holon;
|
|
271
|
+
lensName = context.lens;
|
|
272
|
+
} else if (context.lens) {
|
|
273
|
+
// 3-level structure: could be app/lens/id or holon/lens/id
|
|
274
|
+
// If the first level is an H3 cell, treat it as holon with default app
|
|
275
|
+
if (isH3Cell(context.appOrHolon)) {
|
|
276
|
+
appName = options.app || 'imported';
|
|
277
|
+
holonId = context.appOrHolon;
|
|
278
|
+
} else {
|
|
279
|
+
appName = context.appOrHolon || options.app || 'imported';
|
|
280
|
+
holonId = 'global'; // Use global for non-geographic data
|
|
281
|
+
}
|
|
282
|
+
lensName = context.lens;
|
|
283
|
+
} else if (context.appOrHolon) {
|
|
284
|
+
// 2-level structure: treat first level as lens, second as id
|
|
285
|
+
// This handles root-level files like /announcements/13.json
|
|
286
|
+
appName = options.app || 'imported';
|
|
287
|
+
holonId = 'global';
|
|
288
|
+
lensName = context.appOrHolon;
|
|
289
|
+
} else {
|
|
290
|
+
console.warn(`Skipping ${filePath}: unable to determine structure`);
|
|
291
|
+
stats.skipped++;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!lensName) {
|
|
296
|
+
console.warn(`Skipping ${filePath}: no lens found`);
|
|
297
|
+
stats.skipped++;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (options.verbose) {
|
|
302
|
+
console.log(`Importing: app=${appName}, holon=${holonId}, lens=${lensName}, id=${id}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!options.dryRun) {
|
|
306
|
+
// Import into holosphere with retry logic
|
|
307
|
+
await writeWithRetry(hs, holonId, lensName, data);
|
|
308
|
+
|
|
309
|
+
// Rate limiting between writes
|
|
310
|
+
if (options.writeDelay > 0) {
|
|
311
|
+
await sleep(options.writeDelay);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
stats.items++;
|
|
316
|
+
|
|
317
|
+
if (stats.items % 100 === 0) {
|
|
318
|
+
console.log(`Imported ${stats.items} items...`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(`Error importing ${filePath}:`, error.message);
|
|
323
|
+
stats.errors++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Verify and resync data to relay
|
|
329
|
+
*/
|
|
330
|
+
async function verifyAndResync(hs, initialContext) {
|
|
331
|
+
console.log('\n======================');
|
|
332
|
+
console.log('Verifying Relay Sync');
|
|
333
|
+
console.log('======================\n');
|
|
334
|
+
|
|
335
|
+
// Get the holon ID from context
|
|
336
|
+
const holonId = initialContext.holon;
|
|
337
|
+
if (!holonId) {
|
|
338
|
+
console.log('No holon ID found, skipping verification');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`Checking holon: ${holonId}`);
|
|
343
|
+
console.log('Waiting for relay sync...\n');
|
|
344
|
+
await sleep(3000); // Wait for initial sync
|
|
345
|
+
|
|
346
|
+
// Initialize a relay-only client to check what's actually on the relay
|
|
347
|
+
const hsRelay = new HoloSphere({
|
|
348
|
+
appName: options.app || 'imported',
|
|
349
|
+
relays: options.relays,
|
|
350
|
+
persistence: false, // Don't use local storage
|
|
351
|
+
logLevel: 'ERROR',
|
|
352
|
+
privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await sleep(2000);
|
|
356
|
+
|
|
357
|
+
const lensesToCheck = Array.from(stats.lenses);
|
|
358
|
+
let totalMissing = 0;
|
|
359
|
+
const missingByLens = {};
|
|
360
|
+
|
|
361
|
+
console.log('Comparing local vs relay data:\n');
|
|
362
|
+
console.log('Lens'.padEnd(20) + 'Local'.padEnd(10) + 'Relay'.padEnd(10) + 'Missing');
|
|
363
|
+
console.log('-'.repeat(50));
|
|
364
|
+
|
|
365
|
+
for (const lens of lensesToCheck) {
|
|
366
|
+
try {
|
|
367
|
+
// Read from local (hs has both local and relay)
|
|
368
|
+
const localData = await hs.read(holonId, lens);
|
|
369
|
+
// Read from relay only
|
|
370
|
+
const relayData = await hsRelay.read(holonId, lens);
|
|
371
|
+
|
|
372
|
+
const localMap = new Map();
|
|
373
|
+
const relayMap = new Map();
|
|
374
|
+
|
|
375
|
+
// Build local map
|
|
376
|
+
if (Array.isArray(localData)) {
|
|
377
|
+
localData.forEach(item => {
|
|
378
|
+
if (item && item.id) localMap.set(item.id.toString(), item);
|
|
379
|
+
});
|
|
380
|
+
} else if (localData && typeof localData === 'object') {
|
|
381
|
+
Object.keys(localData).forEach(key => {
|
|
382
|
+
const item = localData[key];
|
|
383
|
+
if (item && item.id) localMap.set(item.id.toString(), item);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Build relay map
|
|
388
|
+
if (Array.isArray(relayData)) {
|
|
389
|
+
relayData.forEach(item => {
|
|
390
|
+
if (item && item.id) relayMap.set(item.id.toString(), item);
|
|
391
|
+
});
|
|
392
|
+
} else if (relayData && typeof relayData === 'object') {
|
|
393
|
+
Object.keys(relayData).forEach(key => {
|
|
394
|
+
const item = relayData[key];
|
|
395
|
+
if (item && item.id) relayMap.set(item.id.toString(), item);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const missing = [];
|
|
400
|
+
for (const [id, item] of localMap) {
|
|
401
|
+
if (!relayMap.has(id)) {
|
|
402
|
+
missing.push(item);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const missingCount = missing.length;
|
|
407
|
+
totalMissing += missingCount;
|
|
408
|
+
|
|
409
|
+
if (missingCount > 0) {
|
|
410
|
+
missingByLens[lens] = missing;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(
|
|
414
|
+
lens.padEnd(20) +
|
|
415
|
+
localMap.size.toString().padEnd(10) +
|
|
416
|
+
relayMap.size.toString().padEnd(10) +
|
|
417
|
+
missingCount.toString()
|
|
418
|
+
);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.log(lens.padEnd(20) + 'Error: ' + error.message);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log('-'.repeat(50));
|
|
425
|
+
console.log(`Total missing from relay: ${totalMissing}`);
|
|
426
|
+
|
|
427
|
+
// Resync missing items
|
|
428
|
+
if (totalMissing > 0) {
|
|
429
|
+
console.log('\n======================');
|
|
430
|
+
console.log('Re-syncing Missing Items');
|
|
431
|
+
console.log('======================\n');
|
|
432
|
+
|
|
433
|
+
for (const [lens, missing] of Object.entries(missingByLens)) {
|
|
434
|
+
console.log(`\nSyncing ${missing.length} items for lens: ${lens}`);
|
|
435
|
+
|
|
436
|
+
for (const item of missing) {
|
|
437
|
+
try {
|
|
438
|
+
await writeWithRetry(hsRelay, holonId, lens, item);
|
|
439
|
+
stats.resynced++;
|
|
440
|
+
if (options.verbose) {
|
|
441
|
+
console.log(` ✓ ${lens}/${item.id}`);
|
|
442
|
+
}
|
|
443
|
+
await sleep(options.writeDelay);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.log(` ✗ ${lens}/${item.id}: ${error.message}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log(`\n✓ Re-synced ${stats.resynced} items to relay`);
|
|
451
|
+
} else {
|
|
452
|
+
console.log('\n✓ All data is already on the relay!');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Main import function
|
|
458
|
+
*/
|
|
459
|
+
async function main() {
|
|
460
|
+
console.log('Holosphere Import Tool');
|
|
461
|
+
console.log('======================\n');
|
|
462
|
+
|
|
463
|
+
// Resolve source path
|
|
464
|
+
let sourcePath = path.resolve(options.source);
|
|
465
|
+
let initialContext = {};
|
|
466
|
+
let startDepth = 0;
|
|
467
|
+
|
|
468
|
+
// Handle subfolder as holon ID
|
|
469
|
+
if (options.subfolder) {
|
|
470
|
+
sourcePath = path.join(sourcePath, options.subfolder);
|
|
471
|
+
|
|
472
|
+
// Extract holon ID from subfolder (last component of path)
|
|
473
|
+
const holonId = path.basename(options.subfolder);
|
|
474
|
+
|
|
475
|
+
// Pre-populate context with holon ID
|
|
476
|
+
// When holon is set, folders will be treated as lenses even at depth 0
|
|
477
|
+
initialContext = {
|
|
478
|
+
holon: holonId
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
stats.holons.add(holonId);
|
|
482
|
+
|
|
483
|
+
console.log(`Using subfolder as holon ID: ${holonId}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await fs.access(sourcePath);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error(`Error: Source directory not found: ${sourcePath}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
console.log(`Source: ${sourcePath}`);
|
|
494
|
+
if (options.subfolder) {
|
|
495
|
+
console.log(`Subfolder: ${options.subfolder}`);
|
|
496
|
+
console.log(`Holon ID: ${initialContext.holon}`);
|
|
497
|
+
}
|
|
498
|
+
console.log(`App: ${options.app || 'auto-detect'}`);
|
|
499
|
+
console.log(`Dry run: ${options.dryRun ? 'Yes' : 'No'}`);
|
|
500
|
+
console.log('');
|
|
501
|
+
|
|
502
|
+
// Initialize holosphere
|
|
503
|
+
let hs = null;
|
|
504
|
+
if (!options.dryRun) {
|
|
505
|
+
console.log('Initializing holosphere...');
|
|
506
|
+
hs = new HoloSphere({
|
|
507
|
+
appName: options.app || 'imported',
|
|
508
|
+
relays: options.relays,
|
|
509
|
+
persistence: options.persistence,
|
|
510
|
+
logLevel: options.verbose ? 'DEBUG' : 'INFO',
|
|
511
|
+
privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
|
|
512
|
+
});
|
|
513
|
+
console.log('Holosphere initialized.\n');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Start import
|
|
517
|
+
const startTime = Date.now();
|
|
518
|
+
console.log('Starting import...\n');
|
|
519
|
+
|
|
520
|
+
await scanAndImport(sourcePath, hs, startDepth, initialContext);
|
|
521
|
+
|
|
522
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
523
|
+
|
|
524
|
+
// Print statistics
|
|
525
|
+
console.log('\n======================');
|
|
526
|
+
console.log('Import Complete');
|
|
527
|
+
console.log('======================');
|
|
528
|
+
console.log(`Duration: ${duration}s`);
|
|
529
|
+
console.log(`Apps: ${stats.apps.size}`);
|
|
530
|
+
console.log(`Holons: ${stats.holons.size}`);
|
|
531
|
+
console.log(`Lenses: ${stats.lenses.size}`);
|
|
532
|
+
console.log(`Items imported: ${stats.items}`);
|
|
533
|
+
console.log(`Errors: ${stats.errors}`);
|
|
534
|
+
console.log(`Skipped: ${stats.skipped}`);
|
|
535
|
+
if (stats.retries > 0) {
|
|
536
|
+
console.log(`Retries: ${stats.retries}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (options.verbose) {
|
|
540
|
+
console.log('\nApps:', Array.from(stats.apps).join(', '));
|
|
541
|
+
console.log('Holons:', Array.from(stats.holons).join(', '));
|
|
542
|
+
console.log('Lenses:', Array.from(stats.lenses).join(', '));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!options.dryRun && hs) {
|
|
546
|
+
console.log('\nHolosphere metrics:', hs.metrics());
|
|
547
|
+
|
|
548
|
+
// Verify and resync if enabled
|
|
549
|
+
if (options.verifyAndResync && options.relays.length > 0) {
|
|
550
|
+
await verifyAndResync(hs, initialContext);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Cleanup: Close connections and exit
|
|
555
|
+
if (!options.dryRun && hs) {
|
|
556
|
+
console.log('\nClosing connections...');
|
|
557
|
+
if (hs.client && hs.client.pool && hs.client.pool.close) {
|
|
558
|
+
try {
|
|
559
|
+
hs.client.pool.close(hs.client.relays);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
// Ignore errors during cleanup
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (stats.resynced > 0) {
|
|
567
|
+
console.log(`\n✓ Import complete - ${stats.items} items imported, ${stats.resynced} re-synced to relay\n`);
|
|
568
|
+
} else {
|
|
569
|
+
console.log('\n✓ Import complete\n');
|
|
570
|
+
}
|
|
571
|
+
process.exit(0);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Run main function
|
|
575
|
+
main().catch(error => {
|
|
576
|
+
console.error('Fatal error:', error);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publish a sample of imported data to relay.holons.io
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { HoloSphere } from '../src/index.js';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
|
|
10
|
+
// Load environment variables
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
console.log('Publishing Sample Data to relay.holons.io');
|
|
15
|
+
console.log('==========================================\n');
|
|
16
|
+
|
|
17
|
+
// First, read from local cache (no relays)
|
|
18
|
+
console.log('Step 1: Reading sample data from local cache...');
|
|
19
|
+
const hsLocal = new HoloSphere({
|
|
20
|
+
appName: 'imported',
|
|
21
|
+
relays: [],
|
|
22
|
+
persistence: true,
|
|
23
|
+
logLevel: 'INFO',
|
|
24
|
+
privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
28
|
+
|
|
29
|
+
const quests = await hsLocal.readGlobal('quests');
|
|
30
|
+
const questArray = Object.values(quests || {});
|
|
31
|
+
|
|
32
|
+
console.log(`✓ Found ${questArray.length} quests locally\n`);
|
|
33
|
+
|
|
34
|
+
if (questArray.length === 0) {
|
|
35
|
+
console.log('No quests found to publish.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Take first 5 quests
|
|
40
|
+
const sampleQuests = questArray.slice(0, 5);
|
|
41
|
+
console.log(`Selected ${sampleQuests.length} quests to publish:\n`);
|
|
42
|
+
|
|
43
|
+
sampleQuests.forEach((q, idx) => {
|
|
44
|
+
console.log(`${idx + 1}. ${q.title || q.id} (${q.status || 'unknown'})`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log('\n');
|
|
48
|
+
|
|
49
|
+
// Now publish to relay.holons.io
|
|
50
|
+
console.log('Step 2: Publishing to relay.holons.io...');
|
|
51
|
+
const hsRelay = new HoloSphere({
|
|
52
|
+
appName: 'imported',
|
|
53
|
+
relays: ['wss://relay.holons.io'],
|
|
54
|
+
persistence: true,
|
|
55
|
+
logLevel: 'INFO',
|
|
56
|
+
privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let successCount = 0;
|
|
60
|
+
for (const quest of sampleQuests) {
|
|
61
|
+
try {
|
|
62
|
+
const success = await hsRelay.writeGlobal('quests', quest);
|
|
63
|
+
if (success) {
|
|
64
|
+
console.log(`✓ Published: ${quest.title || quest.id}`);
|
|
65
|
+
successCount++;
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`✗ Failed: ${quest.title || quest.id}`);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.log(`✗ Error: ${quest.title || quest.id} - ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`\n✓ Published ${successCount}/${sampleQuests.length} quests\n`);
|
|
75
|
+
|
|
76
|
+
// Wait for propagation
|
|
77
|
+
console.log('Step 3: Waiting for relay propagation...');
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
79
|
+
console.log('✓ Wait complete\n');
|
|
80
|
+
|
|
81
|
+
// Read back from relay
|
|
82
|
+
console.log('Step 4: Reading back from relay.holons.io...');
|
|
83
|
+
const retrievedQuests = await hsRelay.readGlobal('quests');
|
|
84
|
+
const retrievedArray = Object.values(retrievedQuests || {});
|
|
85
|
+
|
|
86
|
+
console.log(`✓ Found ${retrievedArray.length} quests on relay\n`);
|
|
87
|
+
|
|
88
|
+
if (retrievedArray.length > 0) {
|
|
89
|
+
console.log('Retrieved quests:');
|
|
90
|
+
retrievedArray.forEach((q, idx) => {
|
|
91
|
+
console.log(`${idx + 1}. ${q.title || q.id}`);
|
|
92
|
+
console.log(` Status: ${q.status || 'unknown'}`);
|
|
93
|
+
console.log(` ID: ${q.id}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('✓ Complete!');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main().catch(console.error);
|