holosphere 1.1.19 → 2.0.0-alpha0

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.
Files changed (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +476 -531
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -1022
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -34549
  140. package/holosphere-bundle.js +0 -34580
  141. package/holosphere-bundle.min.js +0 -49
  142. package/holosphere.d.ts +0 -604
  143. package/holosphere.js +0 -739
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
package/federation.js DELETED
@@ -1,1234 +0,0 @@
1
- /**
2
- * Federation functionality for HoloSphere
3
- * Provides methods for creating, managing, and using federated spaces
4
- */
5
-
6
- import * as h3 from 'h3-js';
7
-
8
- /**
9
- * Creates a federation relationship between two spaces
10
- * Federation is bidirectional by default, and data propagation uses soul references by default.
11
- *
12
- * @param {object} holosphere - The HoloSphere instance
13
- * @param {string} spaceId1 - The first space ID
14
- * @param {string} spaceId2 - The second space ID
15
- * @param {string} [password1] - Optional password for the first space
16
- * @param {string} [password2] - Optional password for the second space
17
- * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
18
- * @param {object} [lensConfig] - Optional lens-specific configuration
19
- * @param {string[]} [lensConfig.federate] - List of lenses to federate (default: all)
20
- * @param {string[]} [lensConfig.notify] - List of lenses to notify (default: all)
21
- * @returns {Promise<boolean>} - True if federation was created successfully
22
- */
23
- export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
24
- if (!spaceId1 || !spaceId2) {
25
- throw new Error('federate: Missing required space IDs');
26
- }
27
-
28
- // Prevent self-federation
29
- if (spaceId1 === spaceId2) {
30
- throw new Error('Cannot federate a space with itself');
31
- }
32
-
33
- // Validate lens configuration
34
- const { federate = [], notify = [] } = lensConfig;
35
- if (!Array.isArray(federate) || !Array.isArray(notify)) {
36
- throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays');
37
- }
38
-
39
- // Use the provided lens configurations directly
40
- const federateLenses = federate;
41
- const notifyLenses = notify;
42
-
43
- try {
44
- // Get or create federation info for first space (A)
45
- let fedInfo1 = null;
46
-
47
- try {
48
- fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
49
- } catch (error) {
50
- }
51
-
52
- if (fedInfo1 == null) {
53
- fedInfo1 = {
54
- id: spaceId1,
55
- name: spaceId1,
56
- federation: [],
57
- notify: [],
58
- lensConfig: {}, // New field for lens-specific settings
59
- timestamp: Date.now()
60
- };
61
- }
62
-
63
- // Ensure arrays and lensConfig exist
64
- if (!fedInfo1.federation) fedInfo1.federation = [];
65
- if (!fedInfo1.notify) fedInfo1.notify = [];
66
- if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
67
-
68
- // Add space2 to space1's federation list if not already present
69
- if (!fedInfo1.federation.includes(spaceId2)) {
70
- fedInfo1.federation.push(spaceId2);
71
- }
72
-
73
- // Add space2 to space1's notify list if not already present
74
- if (!fedInfo1.notify.includes(spaceId2)) {
75
- fedInfo1.notify.push(spaceId2);
76
- }
77
-
78
- // Store lens configuration for space2
79
- const newLensConfigsForSpace1 = { ...(fedInfo1.lensConfig || {}) }; // Shallow copy existing lensConfigs for space1
80
- newLensConfigsForSpace1[spaceId2] = { // Add/update config for the target spaceId2
81
- federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter
82
- notify: [...notifyLenses],
83
- timestamp: Date.now()
84
- };
85
- fedInfo1.lensConfig = newLensConfigsForSpace1; // Assign the new/modified object back to fedInfo1.lensConfig
86
-
87
- // Update timestamp
88
- fedInfo1.timestamp = Date.now();
89
-
90
- // Save updated federation info for space1
91
- try {
92
- await holosphere.putGlobal('federation', fedInfo1, password1);
93
- } catch (error) {
94
- throw new Error(`Failed to create federation: ${error.message}`);
95
- }
96
-
97
- // If bidirectional is true, handle space2 (B) as well
98
- {
99
- let fedInfo2 = null;
100
- try {
101
- fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
102
- } catch (error) {
103
- }
104
-
105
- if (fedInfo2 == null) {
106
- fedInfo2 = {
107
- id: spaceId2,
108
- name: spaceId2,
109
- federation: [],
110
- notify: [],
111
- lensConfig: {}, // New field for lens-specific settings
112
- timestamp: Date.now()
113
- };
114
- }
115
-
116
- // Ensure arrays and lensConfig exist
117
- if (!fedInfo2.federation) fedInfo2.federation = [];
118
- if (!fedInfo2.notify) fedInfo2.notify = [];
119
- if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
120
-
121
- // Add space1 to space2's federation list if bidirectional
122
- if (bidirectional && !fedInfo2.federation.includes(spaceId1)) {
123
- fedInfo2.federation.push(spaceId1);
124
- }
125
-
126
- // Add space1 to space2's notify list if not already present
127
- if (!fedInfo2.notify.includes(spaceId1)) {
128
- fedInfo2.notify.push(spaceId1);
129
- }
130
-
131
- // Store lens configuration for space1
132
- fedInfo2.lensConfig[spaceId1] = {
133
- federate: bidirectional ? [...federateLenses] : [], // Create a copy of the array
134
- notify: bidirectional ? [...notifyLenses] : [], // Create a copy of the array
135
- timestamp: Date.now()
136
- };
137
-
138
- // Update timestamp
139
- fedInfo2.timestamp = Date.now();
140
-
141
- // Save updated federation info for space2
142
- try {
143
- await holosphere.putGlobal('federation', fedInfo2, password2);
144
- } catch (error) {
145
- }
146
- }
147
-
148
- // Create federation metadata record
149
- const federationMeta = {
150
- id: `${spaceId1}_${spaceId2}`,
151
- space1: spaceId1,
152
- space2: spaceId2,
153
- created: Date.now(),
154
- status: 'active',
155
- bidirectional: bidirectional,
156
- lensConfig: {
157
- federate: [...federateLenses], // Create a copy of the array
158
- notify: [...notifyLenses] // Create a copy of the array
159
- }
160
- };
161
-
162
- try {
163
- await holosphere.putGlobal('federationMeta', federationMeta);
164
- } catch (error) {
165
- }
166
-
167
- return true;
168
- } catch (error) {
169
- throw error;
170
- }
171
- }
172
-
173
- /**
174
- * Subscribes to federation notifications for a space
175
- * @param {object} holosphere - The HoloSphere instance
176
- * @param {string} spaceId - The space ID to subscribe to
177
- * @param {string} [password] - Optional password for the space
178
- * @param {function} callback - The callback to execute on notifications
179
- * @param {object} [options] - Subscription options
180
- * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
181
- * @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
182
- * @returns {Promise<object>} - Subscription object with unsubscribe() method
183
- */
184
- export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) {
185
- if (!spaceId || !callback) {
186
- throw new Error('subscribeFederation: Missing required parameters');
187
- }
188
-
189
- const { lenses = ['*'], throttle = 0 } = options;
190
-
191
- // Get federation info
192
- const fedInfo = await holosphere.getGlobal('federation', spaceId, password);
193
- if (!fedInfo) {
194
- throw new Error('No federation info found for space');
195
- }
196
-
197
- // Create subscription for each federated space
198
- const subscriptions = [];
199
- let lastNotificationTime = {};
200
-
201
- if (fedInfo.federation && fedInfo.federation.length > 0) {
202
- for (const federatedSpace of fedInfo.federation) {
203
- // For each lens specified (or all if '*')
204
- for (const lens of lenses) {
205
- try {
206
- const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => {
207
- try {
208
- // Skip if data is missing or not from federated space
209
- if (!data || !data.id) return;
210
-
211
- // Apply throttling if configured
212
- const now = Date.now();
213
- const key = `${federatedSpace}_${lens}_${data.id}`;
214
-
215
- if (throttle > 0) {
216
- if (lastNotificationTime[key] &&
217
- (now - lastNotificationTime[key]) < throttle) {
218
- return; // Skip this notification (throttled)
219
- }
220
- lastNotificationTime[key] = now;
221
- }
222
-
223
- // Add federation metadata if not present
224
- if (!data.federation) {
225
- data.federation = {
226
- origin: federatedSpace,
227
- timestamp: now
228
- };
229
- }
230
-
231
- // Execute callback with the data
232
- await callback(data, federatedSpace, lens);
233
- } catch (error) {
234
- }
235
- });
236
-
237
- if (sub && typeof sub.unsubscribe === 'function') {
238
- subscriptions.push(sub);
239
- }
240
- } catch (error) {
241
- }
242
- }
243
- }
244
- }
245
-
246
- // Return combined subscription object
247
- return {
248
- unsubscribe: () => {
249
- subscriptions.forEach(sub => {
250
- try {
251
- if (sub && typeof sub.unsubscribe === 'function') {
252
- sub.unsubscribe();
253
- }
254
- } catch (error) {
255
- }
256
- });
257
- // Clear the subscriptions array
258
- subscriptions.length = 0;
259
- // Clear throttling data
260
- lastNotificationTime = {};
261
- },
262
- getSubscriptionCount: () => subscriptions.length
263
- };
264
- }
265
-
266
- /**
267
- * Gets federation info for a space
268
- * @param {object} holosphere - The HoloSphere instance
269
- * @param {string} spaceId - The space ID
270
- * @param {string} [password] - Optional password for the space
271
- * @returns {Promise<object|null>} - Federation info or null if not found
272
- */
273
- export async function getFederation(holosphere, spaceId, password = null) {
274
- if (!spaceId) {
275
- throw new Error('getFederation: Missing space ID');
276
- }
277
- return await holosphere.getGlobal('federation', spaceId, password);
278
- }
279
-
280
- /**
281
- * Retrieves the lens-specific configuration for a federation link between two spaces.
282
- * @param {object} holosphere - The HoloSphere instance
283
- * @param {string} spaceId - The ID of the source space.
284
- * @param {string} targetSpaceId - The ID of the target space in the federation link.
285
- * @param {string} [password] - Optional password for the source space.
286
- * @returns {Promise<object|null>} - An object with 'federate' and 'notify' arrays, or null if not found.
287
- */
288
- export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) {
289
- if (!holosphere || !spaceId || !targetSpaceId) {
290
- throw new Error('getFederatedConfig: Missing required parameters');
291
- }
292
-
293
- try {
294
- const fedInfo = await getFederation(holosphere, spaceId, password);
295
-
296
- if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) {
297
- return {
298
- federate: fedInfo.lensConfig[targetSpaceId].federate || [],
299
- notify: fedInfo.lensConfig[targetSpaceId].notify || []
300
- };
301
- }
302
- return null; // Or return an empty config: { federate: [], notify: [] }
303
- } catch (error) {
304
- console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`);
305
- throw error;
306
- }
307
- }
308
-
309
- /**
310
- * Removes a federation relationship between spaces
311
- * @param {object} holosphere - The HoloSphere instance
312
- * @param {string} spaceId1 - The first space ID
313
- * @param {string} spaceId2 - The second space ID
314
- * @param {string} [password1] - Optional password for the first space
315
- * @param {string} [password2] - Optional password for the second space
316
- * @returns {Promise<boolean>} - True if federation was removed successfully
317
- */
318
- export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
319
- if (!spaceId1 || !spaceId2) {
320
- throw new Error('unfederate: Missing required space IDs');
321
- }
322
-
323
- try {
324
- // Get federation info for first space
325
- let fedInfo1 = null;
326
- try {
327
- fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
328
- } catch (error) {
329
- console.error(`Error getting fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
330
- // If we can't get fedInfo1, we can't modify it. Decide if this is a critical failure.
331
- // For now, we'll let it proceed to attempt metadata cleanup, but a throw here might be valid.
332
- }
333
-
334
- if (!fedInfo1) {
335
- // If fedInfo1 doesn't exist, log and proceed to metadata cleanup.
336
- console.warn(`No federation info found for ${spaceId1}. Skipping its update.`);
337
- } else {
338
- // Ensure arrays exist
339
- if (!fedInfo1.federation) fedInfo1.federation = [];
340
- if (!fedInfo1.notify) fedInfo1.notify = [];
341
-
342
- // Update first space federation info - remove from both federation and notify arrays
343
- const originalFederationLength = fedInfo1.federation.length;
344
- const originalNotifyLength = fedInfo1.notify.length;
345
-
346
- fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
347
- fedInfo1.notify = fedInfo1.notify.filter(id => id !== spaceId2);
348
- fedInfo1.timestamp = Date.now();
349
-
350
- console.log(`Unfederate: Removed ${spaceId2} from ${spaceId1}: federation ${originalFederationLength} -> ${fedInfo1.federation.length}, notify ${originalNotifyLength} -> ${fedInfo1.notify.length}`);
351
-
352
- try {
353
- await holosphere.putGlobal('federation', fedInfo1, password1);
354
- } catch (error) {
355
- console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
356
- throw error; // RE-THROW to signal failure
357
- }
358
- }
359
-
360
- // Update second space federation info (remove spaceId1 from spaceId2's notify list)
361
- // This part is usually for full bidirectional unfederation cleanup.
362
- // The original code only did this if password2 was provided.
363
- if (password2) { // Retaining original condition for this block
364
- let fedInfo2 = null;
365
- try {
366
- fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
367
- } catch (error) {
368
- console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
369
- }
370
-
371
- if (!fedInfo2 || !fedInfo2.notify) {
372
- console.warn(`No notify array found for ${spaceId2} or fedInfo2 is null. Skipping its update.`);
373
- } else {
374
- fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
375
- fedInfo2.timestamp = Date.now();
376
-
377
- try {
378
- await holosphere.putGlobal('federation', fedInfo2, password2);
379
- } catch (error) {
380
- console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
381
- throw error; // RE-THROW to signal failure
382
- }
383
- }
384
- }
385
-
386
- // Update federation metadata
387
- const metaId = `${spaceId1}_${spaceId2}`;
388
- const altMetaId = `${spaceId2}_${spaceId1}`;
389
-
390
- try {
391
- const meta = await holosphere.getGlobal('federationMeta', metaId) ||
392
- await holosphere.getGlobal('federationMeta', altMetaId);
393
-
394
- if (meta) {
395
- meta.status = 'inactive';
396
- meta.endedAt = Date.now();
397
- await holosphere.putGlobal('federationMeta', meta); // Not re-throwing here as it's metadata cleanup
398
- }
399
- } catch (error) {
400
- console.warn(`Failed to update federationMeta during unfederate: ${error.message}`);
401
- }
402
-
403
- return true;
404
- } catch (error) {
405
- // This will catch errors re-thrown from putGlobal or from getGlobal if they occur before specific catches.
406
- console.error(`Critical error during unfederate operation for ${spaceId1}-${spaceId2}: ${error.message}`);
407
- throw error; // Ensure the main operation failure is propagated
408
- }
409
- }
410
-
411
- /**
412
- * Removes a notification relationship between two spaces
413
- * This removes spaceId2 from the notify list of spaceId1
414
- *
415
- * @param {object} holosphere - The HoloSphere instance
416
- * @param {string} spaceId1 - The space to modify (remove from its notify list)
417
- * @param {string} spaceId2 - The space to be removed from notifications
418
- * @param {string} [password1] - Optional password for the first space
419
- * @returns {Promise<boolean>} - True if notification was removed successfully
420
- */
421
- export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
422
- if (!spaceId1 || !spaceId2) {
423
- throw new Error('removeNotify: Missing required space IDs');
424
- }
425
-
426
- try {
427
- // Get federation info for space
428
- let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
429
-
430
- if (!fedInfo) {
431
- throw new Error(`No federation info found for ${spaceId1}`);
432
- }
433
-
434
- // Ensure notify array exists
435
- if (!fedInfo.notify) fedInfo.notify = [];
436
-
437
- // Remove space2 from space1's notify list if present
438
- if (fedInfo.notify.includes(spaceId2)) {
439
- fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2);
440
-
441
- // Update timestamp
442
- fedInfo.timestamp = Date.now();
443
-
444
- // Save updated federation info
445
- await holosphere.putGlobal('federation', fedInfo, password1);
446
- return true;
447
- } else {
448
- return false;
449
- }
450
- } catch (error) {
451
- throw error;
452
- }
453
- }
454
-
455
- /**
456
- * Get and combine data from local and federated sources.
457
- * If `options.queryIds` is provided, fetches only those specific IDs using `get()`.
458
- * Otherwise, falls back to fetching all data using `getAll()` (potentially inefficient).
459
- *
460
- * @param {HoloSphere} holosphere The HoloSphere instance
461
- * @param {string} holon The local holon name (used as the space ID for federation info)
462
- * @param {string} lens The lens to query
463
- * @param {Object} options Options for data retrieval and aggregation
464
- * @param {string[]} [options.queryIds] Optional array of specific item IDs to fetch.
465
- * @param {boolean} [options.aggregate=false] Whether to aggregate results by ID
466
- * @param {string} [options.idField='id'] The field to use as ID
467
- * @param {string[]} [options.sumFields=[]] Fields to sum during aggregation
468
- * @param {string[]} [options.concatArrays=[]] Array fields to concatenate during aggregation
469
- * @param {boolean} [options.removeDuplicates=true] Whether to remove duplicates in concatenated arrays
470
- * @param {Function} [options.mergeStrategy=null] Custom merge function for aggregation
471
- * @param {boolean} [options.includeLocal=true] Whether to include local data
472
- * @param {boolean} [options.includeFederated=true] Whether to include federated data
473
- * @param {boolean} [options.resolveReferences=true] Whether to resolve federation references
474
- * @param {number} [options.maxFederatedSpaces=-1] Maximum number of federated spaces to query (-1 for all)
475
- * @param {number} [options.timeout=10000] Timeout in milliseconds for federated queries
476
- * @returns {Promise<Array>} Combined array of local and federated data
477
- */
478
- export async function getFederated(holosphere, holon, lens, options = {}) {
479
- // Set default options and extract queryIds
480
- const {
481
- queryIds = null, // New option
482
- aggregate = false,
483
- idField = 'id',
484
- sumFields = [],
485
- concatArrays = [],
486
- removeDuplicates = true,
487
- mergeStrategy = null,
488
- includeLocal = true,
489
- includeFederated = true,
490
- resolveReferences = true,
491
- maxFederatedSpaces = -1,
492
- timeout = 10000
493
- } = options;
494
-
495
- console.log(`resolveReferences option: ${resolveReferences}`);
496
- console.log(`Querying specific IDs:`, queryIds ? queryIds.join(', ') : 'No (fetching all)');
497
-
498
- // Validate required parameters
499
- if (!holosphere || !holon || !lens) {
500
- throw new Error('Missing required parameters: holosphere, holon, and lens are required');
501
- }
502
-
503
- // Get federation info for current space (using holon as spaceId)
504
- const spaceId = holon;
505
- const fedInfo = await getFederation(holosphere, spaceId);
506
-
507
- // Initialize result array and track processed IDs to avoid duplicates/redundant fetches
508
- const fetchedItems = new Map(); // Use Map to store fetched items by ID
509
- const processedIds = new Set(); // Track IDs added to the final result
510
-
511
- const fetchPromises = [];
512
-
513
- // Determine list of spaces to query (local + federated)
514
- let spacesToQuery = [];
515
- if (includeLocal) {
516
- spacesToQuery.push(holon); // Add local holon first
517
- }
518
- if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) {
519
- const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.federation : fedInfo.federation.slice(0, maxFederatedSpaces);
520
- spacesToQuery = spacesToQuery.concat(federatedSpaces);
521
- }
522
-
523
- // Fetch data from all relevant spaces
524
- for (const currentSpace of spacesToQuery) {
525
- if (queryIds && Array.isArray(queryIds)) {
526
- // --- Fetch specific IDs using holosphere.get ---
527
- console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`);
528
- for (const itemId of queryIds) {
529
- if (fetchedItems.has(itemId)) continue; // Skip if already fetched
530
- fetchPromises.push(
531
- holosphere.get(currentSpace, lens, itemId)
532
- .then(item => {
533
- if (item) {
534
- fetchedItems.set(itemId, item);
535
- }
536
- })
537
- .catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
538
- );
539
- }
540
- } else {
541
- // --- Fetch all data using holosphere.getAll (Fallback - inefficient) ---
542
- if(currentSpace === holon && includeLocal) { // Only warn once for local
543
- console.warn(`getFederated: No queryIds provided. Falling back to fetching ALL items from ${currentSpace} using getAll. This can be inefficient.`);
544
- }
545
- console.log(`Fetching ALL items from ${currentSpace}`);
546
- fetchPromises.push(
547
- holosphere.getAll(currentSpace, lens)
548
- .then(items => {
549
- for (const item of items) {
550
- if (item && item[idField] && !fetchedItems.has(item[idField])) {
551
- fetchedItems.set(item[idField], item);
552
- }
553
- }
554
- })
555
- .catch(err => console.warn(`Error fetching all items from ${currentSpace}: ${err.message}`))
556
- );
557
- }
558
- }
559
-
560
- // Wait for all fetches to complete
561
- await Promise.all(fetchPromises);
562
-
563
- // Convert Map values to array for processing
564
- const result = Array.from(fetchedItems.values());
565
-
566
- // Now resolve references if needed
567
- if (resolveReferences && result.length > 0) {
568
- console.log(`Resolving references for ${result.length} fetched items`);
569
-
570
- for (let i = 0; i < result.length; i++) {
571
- const item = result[i];
572
-
573
- // Check for simplified reference (item with id and soul)
574
- if (item.soul && item.id) {
575
- console.log(`Found simple reference with soul: ${item.soul}`);
576
-
577
- try {
578
- // Parse the soul to get the components
579
- const soulParts = item.soul.split('/');
580
- if (soulParts.length >= 4) {
581
- const originHolon = soulParts[1];
582
- const originLens = soulParts[2];
583
- const originKey = soulParts[3];
584
-
585
- console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
586
-
587
- // Get original data using the extracted path - always resolve references
588
- const originalData = await holosphere.get(
589
- originHolon,
590
- originLens,
591
- originKey,
592
- null,
593
- { resolveReferences: true } // Always resolve nested references
594
- );
595
-
596
- console.log(`Original data found via soul path:`, JSON.stringify(originalData));
597
-
598
- if (originalData) {
599
- // Replace the reference with the original data
600
- result[i] = {
601
- ...originalData,
602
- _federation: {
603
- isReference: true,
604
- resolved: true,
605
- soul: item.soul,
606
- timestamp: Date.now()
607
- }
608
- };
609
- } else {
610
- // Instead of leaving the original reference, create an error object
611
- result[i] = {
612
- id: item.id,
613
- _federation: {
614
- isReference: true,
615
- resolved: false,
616
- soul: item.soul,
617
- error: 'Referenced data not found',
618
- timestamp: Date.now()
619
- }
620
- };
621
- }
622
- } else {
623
- console.warn(`Soul doesn't match expected format: ${item.soul}`);
624
- // Instead of leaving the original reference, create an error object
625
- result[i] = {
626
- id: item.id,
627
- _federation: {
628
- isReference: true,
629
- resolved: false,
630
- soul: item.soul,
631
- error: 'Invalid soul format',
632
- timestamp: Date.now()
633
- }
634
- };
635
- }
636
- } catch (refError) {
637
- // Instead of leaving the original reference, create an error object
638
- result[i] = {
639
- id: item.id,
640
- _federation: {
641
- isReference: true,
642
- resolved: false,
643
- soul: item.soul,
644
- error: refError.message || 'Error resolving reference',
645
- timestamp: Date.now()
646
- }
647
- };
648
- }
649
- }
650
- // For backward compatibility, check for old-style references
651
- else if (item._federation && item._federation.isReference) {
652
- console.log(`Found legacy reference: ${item._federation.origin}/${item._federation.lens}/${item[idField]}`);
653
-
654
- try {
655
- const reference = item._federation;
656
- console.log(`Getting original data from ${reference.origin} / ${reference.lens} / ${item[idField]}`);
657
-
658
- // Get original data
659
- const originalData = await holosphere.get(
660
- reference.origin,
661
- reference.lens,
662
- item[idField],
663
- null,
664
- { resolveReferences: false } // Prevent infinite recursion
665
- );
666
-
667
- console.log(`Original data found:`, JSON.stringify(originalData));
668
-
669
- if (originalData) {
670
- // Add federation information to the resolved data
671
- result[i] = {
672
- ...originalData,
673
- _federation: {
674
- ...reference,
675
- resolved: true,
676
- timestamp: Date.now()
677
- }
678
- };
679
- } else {
680
- console.warn(`Could not resolve legacy reference: original data not found`);
681
- }
682
- } catch (refError) {
683
- }
684
- }
685
- }
686
- }
687
-
688
- // Apply aggregation if requested
689
- if (aggregate && result.length > 0) {
690
- // Group items by ID
691
- const groupedById = result.reduce((acc, item) => {
692
- const id = item[idField];
693
- if (!acc[id]) {
694
- acc[id] = [];
695
- }
696
- acc[id].push(item);
697
- return acc;
698
- }, {});
699
-
700
- // Aggregate each group
701
- const aggregatedData = Object.values(groupedById).map(group => {
702
- // If only one item in group, no aggregation needed
703
- if (group.length === 1) return group[0];
704
-
705
- // Use custom merge strategy if provided
706
- if (mergeStrategy && typeof mergeStrategy === 'function') {
707
- return mergeStrategy(group);
708
- }
709
-
710
- // Default aggregation strategy
711
- const base = { ...group[0] };
712
-
713
- // Sum numeric fields
714
- for (const field of sumFields) {
715
- if (typeof base[field] === 'number') {
716
- base[field] = group.reduce((sum, item) => sum + (Number(item[field]) || 0), 0);
717
- }
718
- }
719
-
720
- // Concatenate array fields
721
- for (const field of concatArrays) {
722
- if (Array.isArray(base[field])) {
723
- const allValues = group.reduce((all, item) => {
724
- return Array.isArray(item[field]) ? [...all, ...item[field]] : all;
725
- }, []);
726
-
727
- // Remove duplicates if requested
728
- base[field] = removeDuplicates ? Array.from(new Set(allValues)) : allValues;
729
- }
730
- }
731
-
732
- // Add aggregation metadata
733
- base._aggregated = {
734
- count: group.length,
735
- timestamp: Date.now()
736
- };
737
-
738
- return base;
739
- });
740
-
741
- return aggregatedData;
742
- }
743
-
744
- return result;
745
- }
746
-
747
- /**
748
- * Propagates data to federated spaces
749
- * @param {object} holosphere - The HoloSphere instance
750
- * @param {string} holon - The holon identifier
751
- * @param {string} lens - The lens identifier
752
- * @param {object} data - The data to propagate
753
- * @param {object} [options] - Propagation options
754
- * @param {boolean} [options.useHolograms=true] - Use holograms for propagation (default: true)
755
- * @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces)
756
- * @param {string} [options.password] - Password for accessing the source holon (if needed)
757
- * @param {boolean} [options.propagateToParents=true] - Whether to automatically propagate to parent hexagons (default: true)
758
- * @param {number} [options.maxParentLevels=15] - Maximum number of parent levels to propagate to (default: 15)
759
- * @returns {Promise<object>} - Result with success count and errors
760
- */
761
- export async function propagate(holosphere, holon, lens, data, options = {}) {
762
- if (!holosphere || !holon || !lens || !data) {
763
- throw new Error('propagate: Missing required parameters');
764
- }
765
- // Default propagation options
766
- const {
767
- useHolograms = true,
768
- targetSpaces = null,
769
- password = null,
770
- propagateToParents = true,
771
- maxParentLevels = 15
772
- } = options;
773
-
774
- const result = {
775
- success: 0,
776
- errors: 0,
777
- skipped: 0,
778
- messages: [],
779
- parentPropagation: {
780
- success: 0,
781
- errors: 0,
782
- skipped: 0,
783
- messages: []
784
- }
785
- };
786
-
787
- try {
788
- // ================================ FEDERATION PROPAGATION ================================
789
-
790
- // Get federation info for this holon using getFederation
791
- const fedInfo = await getFederation(holosphere, holon, password);
792
-
793
- // Only perform federation propagation if there's valid federation info
794
- if (fedInfo && fedInfo.federation && fedInfo.federation.length > 0 && fedInfo.notify && fedInfo.notify.length > 0) {
795
- // Filter federation spaces to those in notify list
796
- let spaces = fedInfo.notify;
797
-
798
- // Further filter by targetSpaces if provided
799
- if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
800
- spaces = spaces.filter(space => targetSpaces.includes(space));
801
- }
802
-
803
- if (spaces.length > 0) {
804
- // Filter spaces based on lens configuration
805
- spaces = spaces.filter(targetSpace => {
806
- const spaceConfig = fedInfo.lensConfig?.[targetSpace];
807
- if (!spaceConfig) {
808
- result.messages.push(`No lens configuration for target space ${targetSpace}. Skipping propagation of lens '${lens}'.`);
809
- result.skipped++;
810
- return false;
811
- }
812
-
813
- // Ensure .federate is an array before calling .includes
814
- const federateLenses = Array.isArray(spaceConfig.federate) ? spaceConfig.federate : [];
815
-
816
- const shouldFederate = federateLenses.includes('*') || federateLenses.includes(lens);
817
-
818
- // Propagation now only depends on the 'federate' list configuration for the lens
819
- const shouldPropagate = shouldFederate;
820
-
821
- if (!shouldPropagate) {
822
- result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'federate' configuration.`);
823
- result.skipped++;
824
- }
825
-
826
- return shouldPropagate;
827
- });
828
-
829
- if (spaces.length > 0) {
830
- // Check if data is already a hologram
831
- const isAlreadyHologram = holosphere.isHologram(data);
832
-
833
- // For each target space, propagate the data
834
- const propagatePromises = spaces.map(async (targetSpace) => {
835
- try {
836
- let payloadToPut;
837
- const federationMeta = {
838
- origin: holon, // The space from which this data is being propagated
839
- sourceLens: lens, // The lens from which this data is being propagated
840
- propagatedAt: Date.now(),
841
- originalId: data.id
842
- };
843
-
844
- if (useHolograms && !isAlreadyHologram) {
845
- // Create a new hologram referencing the original data
846
- const newHologram = holosphere.createHologram(holon, lens, data);
847
- payloadToPut = {
848
- ...newHologram, // This will be { id: data.id, soul: 'path/to/original' }
849
- _federation: federationMeta
850
- };
851
- } else {
852
- // Propagate existing data (could be a full object or an existing hologram)
853
- // Make a shallow copy and update/add _federation metadata
854
- payloadToPut = {
855
- ...data,
856
- _federation: {
857
- ...(data._federation || {}), // Preserve existing _federation fields if any
858
- ...federationMeta // Add/overwrite with current propagation info
859
- }
860
- };
861
- }
862
-
863
- // Store in the target space with redirection disabled and no further auto-propagation
864
- await holosphere.put(targetSpace, lens, payloadToPut, null, {
865
- disableHologramRedirection: true,
866
- autoPropagate: false
867
- });
868
-
869
- result.success++;
870
- return true;
871
- } catch (error) {
872
- result.errors++;
873
- result.messages.push(`Error propagating ${data.id} to ${targetSpace}: ${error.message}`);
874
- return false;
875
- }
876
- });
877
-
878
- await Promise.all(propagatePromises);
879
- } else {
880
- result.messages.push('No valid target spaces for federation propagation after lens filtering.');
881
- }
882
- } else {
883
- result.messages.push('No valid target spaces found for federation propagation.');
884
- }
885
- } else {
886
- result.messages.push(`No federation found for ${holon} or no notification targets available.`);
887
- }
888
-
889
- // ================================ PARENT PROPAGATION ================================
890
-
891
- // Check if we should propagate to parent hexagons
892
- if (propagateToParents) {
893
-
894
- try {
895
- // Validate if the holon is a proper H3 hexagon
896
- // H3 hexagons should start with '8' and have a valid format
897
- let holonResolution;
898
- let isValidH3 = false;
899
-
900
- // First check: H3 hexagons should start with '8' and be at least 15 characters long
901
- if (typeof holon === 'string' && /^[8][0-9A-Fa-f]+$/.test(holon) && holon.length >= 15) {
902
- try {
903
- holonResolution = h3.getResolution(holon);
904
- // Additional validation: resolution should be >= 0 and <= 15
905
- if (holonResolution >= 0 && holonResolution <= 15) {
906
- isValidH3 = true;
907
- } else {
908
- console.log(`[Federation] Holon ${holon} has invalid resolution: ${holonResolution}`);
909
- }
910
- } catch (error) {
911
- console.log(`[Federation] Holon ${holon} failed H3 validation: ${error.message}`);
912
- }
913
- } else {
914
- }
915
-
916
- if (!isValidH3) {
917
- result.parentPropagation.messages.push(`Holon ${holon} is not a valid H3 hexagon. Skipping parent propagation.`);
918
- result.parentPropagation.skipped++;
919
- }
920
-
921
- if (isValidH3 && holonResolution !== undefined) {
922
- // Get all parent hexagons up to the specified max levels
923
- const parentHexagons = [];
924
- let currentHolon = holon;
925
- let currentRes = holonResolution;
926
- let levelsProcessed = 0;
927
-
928
- while (currentRes > 0 && levelsProcessed < maxParentLevels) {
929
- try {
930
- const parent = h3.cellToParent(currentHolon, currentRes - 1);
931
- parentHexagons.push(parent);
932
- currentHolon = parent;
933
- currentRes--;
934
- levelsProcessed++;
935
- } catch (error) {
936
- console.error(`[Federation] Error getting parent for ${currentHolon}: ${error.message}`);
937
- result.parentPropagation.messages.push(`Error getting parent for ${currentHolon}: ${error.message}`);
938
- result.parentPropagation.errors++;
939
- break;
940
- }
941
- }
942
-
943
- if (parentHexagons.length > 0) {
944
- result.parentPropagation.messages.push(`Found ${parentHexagons.length} parent hexagons to propagate to: ${parentHexagons.join(', ')}`);
945
-
946
- // Check if data is already a hologram (reuse from federation section)
947
- const isAlreadyHologram = holosphere.isHologram(data);
948
-
949
- // Propagate to each parent hexagon
950
- const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => {
951
- try {
952
- let payloadToPut;
953
- const parentFederationMeta = {
954
- origin: holon, // The original holon from which this data is being propagated
955
- sourceLens: lens, // The lens from which this data is being propagated
956
- propagatedAt: Date.now(),
957
- originalId: data.id,
958
- propagationType: 'parent', // Indicate this is parent propagation
959
- parentLevel: holonResolution - h3.getResolution(parentHexagon) // How many levels up
960
- };
961
-
962
- if (useHolograms && !isAlreadyHologram) {
963
- // Create a new hologram referencing the original data
964
- const newHologram = holosphere.createHologram(holon, lens, data);
965
- payloadToPut = {
966
- ...newHologram, // This will be { id: data.id, soul: 'path/to/original' }
967
- _federation: parentFederationMeta
968
- };
969
- } else {
970
- // Propagate existing data (could be a full object or an existing hologram)
971
- // Make a shallow copy and update/add _federation metadata
972
- payloadToPut = {
973
- ...data,
974
- _federation: {
975
- ...(data._federation || {}), // Preserve existing _federation fields if any
976
- ...parentFederationMeta // Add/overwrite with current propagation info
977
- }
978
- };
979
- }
980
-
981
- // Store in the parent hexagon with redirection disabled and no further auto-propagation
982
- await holosphere.put(parentHexagon, lens, payloadToPut, null, {
983
- disableHologramRedirection: true,
984
- autoPropagate: false
985
- });
986
-
987
- console.log(`[Federation] Successfully propagated to parent hexagon: ${parentHexagon}`);
988
- result.parentPropagation.success++;
989
- return true;
990
- } catch (error) {
991
- console.error(`[Federation] Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`);
992
- result.parentPropagation.errors++;
993
- result.parentPropagation.messages.push(`Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`);
994
- return false;
995
- }
996
- });
997
-
998
- await Promise.all(parentPropagatePromises);
999
- } else {
1000
- result.parentPropagation.messages.push(`No parent hexagons found for ${holon} (already at resolution 0 or max levels reached)`);
1001
- result.parentPropagation.skipped++;
1002
- }
1003
- }
1004
- } catch (error) {
1005
- console.error(`[Federation] Error during parent propagation: ${error.message}`);
1006
- result.parentPropagation.errors++;
1007
- result.parentPropagation.messages.push(`Error during parent propagation: ${error.message}`);
1008
- }
1009
- }
1010
-
1011
- // ================================ END PARENT PROPAGATION ================================
1012
-
1013
- result.propagated = result.success > 0 || result.parentPropagation.success > 0;
1014
- return result;
1015
- } catch (error) {
1016
- return {
1017
- ...result,
1018
- error: error.message
1019
- };
1020
- }
1021
- }
1022
-
1023
- /**
1024
- * Tracks a federated message across different chats
1025
- * @param {object} holosphere - The HoloSphere instance
1026
- * @param {string} originalChatId - The ID of the original chat
1027
- * @param {string} messageId - The ID of the original message
1028
- * @param {string} federatedChatId - The ID of the federated chat
1029
- * @param {string} federatedMessageId - The ID of the message in the federated chat
1030
- * @param {string} type - The type of message (e.g., 'quest', 'announcement')
1031
- * @returns {Promise<void>}
1032
- */
1033
- export async function federateMessage(holosphere, originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') {
1034
- const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
1035
- const tracking = await holosphere.getGlobal('federation_messages', trackingKey) || {
1036
- id: trackingKey,
1037
- originalChatId,
1038
- originalMessageId: messageId,
1039
- type,
1040
- messages: []
1041
- };
1042
-
1043
- // Update or add the federated message info
1044
- const existingMsg = tracking.messages.find(m => m.chatId === federatedChatId);
1045
- if (existingMsg) {
1046
- existingMsg.messageId = federatedMessageId;
1047
- existingMsg.timestamp = Date.now();
1048
- } else {
1049
- tracking.messages.push({
1050
- chatId: federatedChatId,
1051
- messageId: federatedMessageId,
1052
- timestamp: Date.now()
1053
- });
1054
- }
1055
-
1056
- await holosphere.putGlobal('federation_messages', tracking);
1057
- }
1058
-
1059
- /**
1060
- * Gets all federated messages for a given original message
1061
- * @param {object} holosphere - The HoloSphere instance
1062
- * @param {string} originalChatId - The ID of the original chat
1063
- * @param {string} messageId - The ID of the original message
1064
- * @returns {Promise<Object|null>} The tracking information for the message
1065
- */
1066
- export async function getFederatedMessages(holosphere, originalChatId, messageId) {
1067
- const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
1068
- return await holosphere.getGlobal('federation_messages', trackingKey);
1069
- }
1070
-
1071
- /**
1072
- * Updates a federated message across all federated chats
1073
- * @param {object} holosphere - The HoloSphere instance
1074
- * @param {string} originalChatId - The ID of the original chat
1075
- * @param {string} messageId - The ID of the original message
1076
- * @param {Function} updateCallback - Function to update the message in each chat
1077
- * @returns {Promise<void>}
1078
- */
1079
- export async function updateFederatedMessages(holosphere, originalChatId, messageId, updateCallback) {
1080
- const tracking = await getFederatedMessages(holosphere, originalChatId, messageId);
1081
- if (!tracking?.messages) return;
1082
-
1083
- for (const msg of tracking.messages) {
1084
- try {
1085
- await updateCallback(msg.chatId, msg.messageId);
1086
- } catch (error) {
1087
- }
1088
- }
1089
- }
1090
-
1091
- /**
1092
- * Resets all federation relationships for a space
1093
- * @param {object} holosphere - The HoloSphere instance
1094
- * @param {string} spaceId - The ID of the space to reset federation for
1095
- * @param {string} [password] - Optional password for the space
1096
- * @param {object} [options] - Reset options
1097
- * @param {boolean} [options.notifyPartners=true] - Whether to notify federation partners about the reset
1098
- * @param {string} [options.spaceName] - Optional name for the space (defaults to spaceId if not provided)
1099
- * @returns {Promise<object>} - Result object with success/error info
1100
- */
1101
- export async function resetFederation(holosphere, spaceId, password = null, options = {}) {
1102
- if (!spaceId) {
1103
- throw new Error('resetFederation: Missing required space ID');
1104
- }
1105
-
1106
- const {
1107
- notifyPartners = true,
1108
- spaceName = null
1109
- } = options;
1110
-
1111
- const result = {
1112
- success: false,
1113
- federatedCount: 0,
1114
- notifyCount: 0,
1115
- partnersNotified: 0,
1116
- errors: []
1117
- };
1118
-
1119
- try {
1120
- // Get current federation info to know what we're clearing
1121
- const fedInfo = await getFederation(holosphere, spaceId, password);
1122
-
1123
- if (!fedInfo) {
1124
- return {
1125
- ...result,
1126
- success: true,
1127
- message: 'No federation configuration found for this space'
1128
- };
1129
- }
1130
-
1131
- // Store counts for reporting
1132
- result.federatedCount = fedInfo.federation?.length || 0;
1133
- result.notifyCount = fedInfo.notify?.length || 0;
1134
-
1135
- // Create empty federation record
1136
- const emptyFedInfo = {
1137
- id: spaceId,
1138
- name: spaceName || spaceId,
1139
- federation: [],
1140
- notify: [],
1141
- timestamp: Date.now()
1142
- };
1143
-
1144
- // Update federation record with empty lists
1145
- await holosphere.putGlobal('federation', emptyFedInfo, password);
1146
-
1147
- // Notify federation partners if requested
1148
- if (notifyPartners && fedInfo.federation && fedInfo.federation.length > 0) {
1149
- const updatePromises = fedInfo.federation.map(async (partnerSpace) => {
1150
- try {
1151
- // Get partner's federation info
1152
- const partnerFedInfo = await getFederation(holosphere, partnerSpace);
1153
-
1154
- if (partnerFedInfo) {
1155
- // Remove this space from partner's federation list
1156
- if (partnerFedInfo.federation) {
1157
- partnerFedInfo.federation = partnerFedInfo.federation.filter(
1158
- id => id !== spaceId.toString()
1159
- );
1160
- }
1161
-
1162
- // Remove this space from partner's notify list
1163
- if (partnerFedInfo.notify) {
1164
- partnerFedInfo.notify = partnerFedInfo.notify.filter(
1165
- id => id !== spaceId.toString()
1166
- );
1167
- }
1168
-
1169
- partnerFedInfo.timestamp = Date.now();
1170
-
1171
- // Save partner's updated federation info
1172
- await holosphere.putGlobal('federation', partnerFedInfo);
1173
- result.partnersNotified++;
1174
- return true;
1175
- }
1176
- return false;
1177
- } catch (error) {
1178
- result.errors.push({
1179
- partner: partnerSpace,
1180
- error: error.message
1181
- });
1182
- return false;
1183
- }
1184
- });
1185
-
1186
- await Promise.all(updatePromises);
1187
- }
1188
-
1189
- // Update federation metadata records if they exist
1190
- if (fedInfo.federation && fedInfo.federation.length > 0) {
1191
- for (const partnerSpace of fedInfo.federation) {
1192
- try {
1193
- const metaId = `${spaceId}_${partnerSpace}`;
1194
- const altMetaId = `${partnerSpace}_${spaceId}`;
1195
-
1196
- const meta = await holosphere.getGlobal('federationMeta', metaId) ||
1197
- await holosphere.getGlobal('federationMeta', altMetaId);
1198
-
1199
- if (meta) {
1200
- meta.status = 'inactive';
1201
- meta.endedAt = Date.now();
1202
- await holosphere.putGlobal('federationMeta', meta);
1203
- }
1204
- } catch (error) {
1205
- }
1206
- }
1207
- }
1208
-
1209
- result.success = true;
1210
- return result;
1211
- } catch (error) {
1212
- return {
1213
- ...result,
1214
- success: false,
1215
- error: error.message
1216
- };
1217
- }
1218
- }
1219
-
1220
- // Export all federation operations as default
1221
- export default {
1222
- federate,
1223
- subscribeFederation,
1224
- getFederation,
1225
- getFederatedConfig,
1226
- unfederate,
1227
- removeNotify,
1228
- getFederated,
1229
- propagate,
1230
- federateMessage,
1231
- getFederatedMessages,
1232
- updateFederatedMessages,
1233
- resetFederation
1234
- };