holosphere 2.0.0-alpha1 → 2.0.0-alpha4

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 (154) hide show
  1. package/dist/2019-D2OG2idw.js +6680 -0
  2. package/dist/2019-D2OG2idw.js.map +1 -0
  3. package/dist/2019-EION3wKo.cjs +8 -0
  4. package/dist/2019-EION3wKo.cjs.map +1 -0
  5. package/dist/_commonjsHelpers-C37NGDzP.cjs +2 -0
  6. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +1 -0
  7. package/dist/_commonjsHelpers-CUmg6egw.js +7 -0
  8. package/dist/_commonjsHelpers-CUmg6egw.js.map +1 -0
  9. package/dist/browser-BSniCNqO.js +3058 -0
  10. package/dist/browser-BSniCNqO.js.map +1 -0
  11. package/dist/browser-Cq59Ij19.cjs +2 -0
  12. package/dist/browser-Cq59Ij19.cjs.map +1 -0
  13. package/dist/cjs/holosphere.cjs +2 -0
  14. package/dist/cjs/holosphere.cjs.map +1 -0
  15. package/dist/esm/holosphere.js +53 -0
  16. package/dist/esm/holosphere.js.map +1 -0
  17. package/dist/index-BB_vVJgv.cjs +5 -0
  18. package/dist/index-BB_vVJgv.cjs.map +1 -0
  19. package/dist/index-CBitK71M.cjs +12 -0
  20. package/dist/index-CBitK71M.cjs.map +1 -0
  21. package/dist/index-CV0eOogK.js +37423 -0
  22. package/dist/index-CV0eOogK.js.map +1 -0
  23. package/dist/index-Cz-PLCUR.js +15104 -0
  24. package/dist/index-Cz-PLCUR.js.map +1 -0
  25. package/dist/indexeddb-storage-CRsZyB2f.cjs +2 -0
  26. package/dist/indexeddb-storage-CRsZyB2f.cjs.map +1 -0
  27. package/dist/indexeddb-storage-DZaGlY_a.js +132 -0
  28. package/dist/indexeddb-storage-DZaGlY_a.js.map +1 -0
  29. package/dist/memory-storage-BkUi6sZG.js +51 -0
  30. package/dist/memory-storage-BkUi6sZG.js.map +1 -0
  31. package/dist/memory-storage-C0DuUsdY.cjs +2 -0
  32. package/dist/memory-storage-C0DuUsdY.cjs.map +1 -0
  33. package/dist/secp256k1-0kPdAVkK.cjs +12 -0
  34. package/dist/secp256k1-0kPdAVkK.cjs.map +1 -0
  35. package/dist/secp256k1-DN4FVXcv.js +1890 -0
  36. package/dist/secp256k1-DN4FVXcv.js.map +1 -0
  37. package/docs/CONTRACTS.md +797 -0
  38. package/docs/FOSDEM_PROPOSAL.md +388 -0
  39. package/docs/LOCALFIRST.md +266 -0
  40. package/docs/contracts/api-interface.md +793 -0
  41. package/docs/data-model.md +476 -0
  42. package/docs/gun-async-usage.md +338 -0
  43. package/docs/plan.md +349 -0
  44. package/docs/quickstart.md +674 -0
  45. package/docs/research.md +362 -0
  46. package/docs/spec.md +244 -0
  47. package/docs/storage-backends.md +326 -0
  48. package/docs/tasks.md +947 -0
  49. package/examples/demo.html +47 -0
  50. package/package.json +10 -5
  51. package/src/contracts/abis/Appreciative.json +1280 -0
  52. package/src/contracts/abis/AppreciativeFactory.json +101 -0
  53. package/src/contracts/abis/Bundle.json +1435 -0
  54. package/src/contracts/abis/BundleFactory.json +106 -0
  55. package/src/contracts/abis/Holon.json +881 -0
  56. package/src/contracts/abis/Holons.json +330 -0
  57. package/src/contracts/abis/Managed.json +1262 -0
  58. package/src/contracts/abis/ManagedFactory.json +149 -0
  59. package/src/contracts/abis/Membrane.json +261 -0
  60. package/src/contracts/abis/Splitter.json +1624 -0
  61. package/src/contracts/abis/SplitterFactory.json +220 -0
  62. package/src/contracts/abis/TestToken.json +321 -0
  63. package/src/contracts/abis/Zoned.json +1461 -0
  64. package/src/contracts/abis/ZonedFactory.json +154 -0
  65. package/src/contracts/chain-manager.js +375 -0
  66. package/src/contracts/deployer.js +443 -0
  67. package/src/contracts/event-listener.js +507 -0
  68. package/src/contracts/holon-contracts.js +344 -0
  69. package/src/contracts/index.js +83 -0
  70. package/src/contracts/networks.js +224 -0
  71. package/src/contracts/operations.js +670 -0
  72. package/src/contracts/queries.js +589 -0
  73. package/src/core/holosphere.js +453 -1
  74. package/src/crypto/nostr-utils.js +263 -0
  75. package/src/federation/handshake.js +455 -0
  76. package/src/federation/hologram.js +1 -1
  77. package/src/hierarchical/upcast.js +6 -5
  78. package/src/index.js +463 -1939
  79. package/src/lib/ai-methods.js +308 -0
  80. package/src/lib/contract-methods.js +293 -0
  81. package/src/lib/errors.js +23 -0
  82. package/src/lib/federation-methods.js +238 -0
  83. package/src/lib/index.js +26 -0
  84. package/src/spatial/h3-operations.js +2 -2
  85. package/src/storage/backends/gundb-backend.js +377 -46
  86. package/src/storage/global-tables.js +28 -1
  87. package/src/storage/gun-auth.js +303 -0
  88. package/src/storage/gun-federation.js +776 -0
  89. package/src/storage/gun-references.js +198 -0
  90. package/src/storage/gun-schema.js +291 -0
  91. package/src/storage/gun-wrapper.js +347 -31
  92. package/src/storage/indexeddb-storage.js +49 -11
  93. package/src/storage/memory-storage.js +5 -0
  94. package/src/storage/nostr-async.js +45 -23
  95. package/src/storage/nostr-client.js +11 -5
  96. package/src/storage/persistent-storage.js +6 -1
  97. package/src/storage/unified-storage.js +119 -0
  98. package/src/subscriptions/manager.js +1 -1
  99. package/types/index.d.ts +133 -0
  100. package/tests/unit/ai/aggregation.test.js +0 -295
  101. package/tests/unit/ai/breakdown.test.js +0 -446
  102. package/tests/unit/ai/classifier.test.js +0 -294
  103. package/tests/unit/ai/council.test.js +0 -262
  104. package/tests/unit/ai/embeddings.test.js +0 -384
  105. package/tests/unit/ai/federation-ai.test.js +0 -344
  106. package/tests/unit/ai/h3-ai.test.js +0 -458
  107. package/tests/unit/ai/index.test.js +0 -304
  108. package/tests/unit/ai/json-ops.test.js +0 -307
  109. package/tests/unit/ai/llm-service.test.js +0 -390
  110. package/tests/unit/ai/nl-query.test.js +0 -383
  111. package/tests/unit/ai/relationships.test.js +0 -311
  112. package/tests/unit/ai/schema-extractor.test.js +0 -384
  113. package/tests/unit/ai/spatial.test.js +0 -279
  114. package/tests/unit/ai/tts.test.js +0 -279
  115. package/tests/unit/content.test.js +0 -332
  116. package/tests/unit/contract/core.test.js +0 -88
  117. package/tests/unit/contract/crypto.test.js +0 -198
  118. package/tests/unit/contract/data.test.js +0 -223
  119. package/tests/unit/contract/federation.test.js +0 -181
  120. package/tests/unit/contract/hierarchical.test.js +0 -113
  121. package/tests/unit/contract/schema.test.js +0 -114
  122. package/tests/unit/contract/social.test.js +0 -217
  123. package/tests/unit/contract/spatial.test.js +0 -110
  124. package/tests/unit/contract/subscriptions.test.js +0 -128
  125. package/tests/unit/contract/utils.test.js +0 -159
  126. package/tests/unit/core.test.js +0 -152
  127. package/tests/unit/crypto.test.js +0 -328
  128. package/tests/unit/federation.test.js +0 -234
  129. package/tests/unit/gun-async.test.js +0 -252
  130. package/tests/unit/hierarchical.test.js +0 -399
  131. package/tests/unit/integration/scenario-01-geographic-storage.test.js +0 -74
  132. package/tests/unit/integration/scenario-02-federation.test.js +0 -76
  133. package/tests/unit/integration/scenario-03-subscriptions.test.js +0 -102
  134. package/tests/unit/integration/scenario-04-validation.test.js +0 -129
  135. package/tests/unit/integration/scenario-05-hierarchy.test.js +0 -125
  136. package/tests/unit/integration/scenario-06-social.test.js +0 -135
  137. package/tests/unit/integration/scenario-07-persistence.test.js +0 -130
  138. package/tests/unit/integration/scenario-08-authorization.test.js +0 -161
  139. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +0 -139
  140. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +0 -357
  141. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +0 -410
  142. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +0 -719
  143. package/tests/unit/performance/benchmark.test.js +0 -85
  144. package/tests/unit/schema.test.js +0 -213
  145. package/tests/unit/spatial.test.js +0 -158
  146. package/tests/unit/storage.test.js +0 -195
  147. package/tests/unit/subscriptions.test.js +0 -328
  148. package/tests/unit/test-data-permanence-debug.js +0 -197
  149. package/tests/unit/test-data-permanence.js +0 -340
  150. package/tests/unit/test-key-persistence-fixed.js +0 -148
  151. package/tests/unit/test-key-persistence.js +0 -172
  152. package/tests/unit/test-relay-permanence.js +0 -376
  153. package/tests/unit/test-second-node.js +0 -95
  154. package/tests/unit/test-simple-write.js +0 -89
@@ -0,0 +1,776 @@
1
+ /**
2
+ * GunDB Federation Module
3
+ * Provides methods for creating, managing, and using federated spaces
4
+ *
5
+ * Ported from holosphere/federation.js to work with the GunDBBackend
6
+ */
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} backend - The GunDBBackend instance
13
+ * @param {string} spaceId1 - The first space ID
14
+ * @param {string} spaceId2 - The second space ID
15
+ * @param {boolean} bidirectional - Whether to set up bidirectional notifications (default: true)
16
+ * @returns {Promise<boolean>} True if federation was created successfully
17
+ */
18
+ export async function federate(backend, spaceId1, spaceId2, bidirectional = true) {
19
+ if (!spaceId1 || !spaceId2) {
20
+ throw new Error('federate: Missing required space IDs');
21
+ }
22
+
23
+ if (spaceId1 === spaceId2) {
24
+ throw new Error('Cannot federate a space with itself');
25
+ }
26
+
27
+ try {
28
+ // Get or create federation info for first space (A)
29
+ let fedInfo1 = await backend.readGlobal('federation', spaceId1);
30
+
31
+ if (!fedInfo1) {
32
+ fedInfo1 = {
33
+ id: spaceId1,
34
+ name: spaceId1,
35
+ federation: [],
36
+ notify: [],
37
+ timestamp: Date.now()
38
+ };
39
+ }
40
+
41
+ // Ensure arrays exist
42
+ if (!fedInfo1.federation) fedInfo1.federation = [];
43
+ if (!fedInfo1.notify) fedInfo1.notify = [];
44
+
45
+ // Add space2 to space1's federation list if not already present
46
+ if (!fedInfo1.federation.includes(spaceId2)) {
47
+ fedInfo1.federation.push(spaceId2);
48
+ }
49
+
50
+ fedInfo1.timestamp = Date.now();
51
+ await backend.writeGlobal('federation', fedInfo1);
52
+
53
+ // Handle space2 (B) - add to notify list
54
+ let fedInfo2 = await backend.readGlobal('federation', spaceId2);
55
+
56
+ if (!fedInfo2) {
57
+ fedInfo2 = {
58
+ id: spaceId2,
59
+ name: spaceId2,
60
+ federation: [],
61
+ notify: [],
62
+ timestamp: Date.now()
63
+ };
64
+ }
65
+
66
+ if (!fedInfo2.notify) fedInfo2.notify = [];
67
+
68
+ // Add space1 to space2's notify list
69
+ if (!fedInfo2.notify.includes(spaceId1)) {
70
+ fedInfo2.notify.push(spaceId1);
71
+ }
72
+
73
+ fedInfo2.timestamp = Date.now();
74
+ await backend.writeGlobal('federation', fedInfo2);
75
+
76
+ // Create federation metadata record
77
+ const federationMeta = {
78
+ id: `${spaceId1}_${spaceId2}`,
79
+ space1: spaceId1,
80
+ space2: spaceId2,
81
+ created: Date.now(),
82
+ status: 'active',
83
+ bidirectional: bidirectional
84
+ };
85
+
86
+ await backend.writeGlobal('federationMeta', federationMeta);
87
+
88
+ return true;
89
+ } catch (error) {
90
+ console.error(`Federation creation failed: ${error.message}`);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Gets federation info for a space
97
+ * @param {Object} backend - The GunDBBackend instance
98
+ * @param {string} spaceId - The space ID
99
+ * @returns {Promise<Object|null>} Federation info or null if not found
100
+ */
101
+ export async function getFederation(backend, spaceId) {
102
+ if (!spaceId) {
103
+ throw new Error('getFederation: Missing space ID');
104
+ }
105
+ return backend.readGlobal('federation', spaceId);
106
+ }
107
+
108
+ /**
109
+ * Removes a federation relationship between spaces
110
+ * @param {Object} backend - The GunDBBackend instance
111
+ * @param {string} spaceId1 - The first space ID
112
+ * @param {string} spaceId2 - The second space ID
113
+ * @returns {Promise<boolean>} True if federation was removed successfully
114
+ */
115
+ export async function unfederate(backend, spaceId1, spaceId2) {
116
+ if (!spaceId1 || !spaceId2) {
117
+ throw new Error('unfederate: Missing required space IDs');
118
+ }
119
+
120
+ try {
121
+ // Update first space federation info
122
+ let fedInfo1 = await backend.readGlobal('federation', spaceId1);
123
+
124
+ if (fedInfo1 && fedInfo1.federation) {
125
+ fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
126
+ fedInfo1.timestamp = Date.now();
127
+ await backend.writeGlobal('federation', fedInfo1);
128
+ }
129
+
130
+ // Update second space federation info
131
+ let fedInfo2 = await backend.readGlobal('federation', spaceId2);
132
+
133
+ if (fedInfo2 && fedInfo2.notify) {
134
+ fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
135
+ fedInfo2.timestamp = Date.now();
136
+ await backend.writeGlobal('federation', fedInfo2);
137
+ }
138
+
139
+ // Update federation metadata
140
+ const metaId = `${spaceId1}_${spaceId2}`;
141
+ const altMetaId = `${spaceId2}_${spaceId1}`;
142
+
143
+ let meta = await backend.readGlobal('federationMeta', metaId);
144
+ if (!meta) {
145
+ meta = await backend.readGlobal('federationMeta', altMetaId);
146
+ }
147
+
148
+ if (meta) {
149
+ meta.status = 'inactive';
150
+ meta.endedAt = Date.now();
151
+ await backend.writeGlobal('federationMeta', meta);
152
+ }
153
+
154
+ return true;
155
+ } catch (error) {
156
+ console.error(`Federation removal failed: ${error.message}`);
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Removes a notification relationship between two spaces
163
+ * @param {Object} backend - The GunDBBackend instance
164
+ * @param {string} spaceId1 - The space to modify
165
+ * @param {string} spaceId2 - The space to be removed from notifications
166
+ * @returns {Promise<boolean>} True if notification was removed successfully
167
+ */
168
+ export async function removeNotify(backend, spaceId1, spaceId2) {
169
+ if (!spaceId1 || !spaceId2) {
170
+ throw new Error('removeNotify: Missing required space IDs');
171
+ }
172
+
173
+ try {
174
+ let fedInfo = await backend.readGlobal('federation', spaceId1);
175
+
176
+ if (!fedInfo) {
177
+ throw new Error(`No federation info found for ${spaceId1}`);
178
+ }
179
+
180
+ if (!fedInfo.notify) fedInfo.notify = [];
181
+
182
+ if (fedInfo.notify.includes(spaceId2)) {
183
+ fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2);
184
+ fedInfo.timestamp = Date.now();
185
+ await backend.writeGlobal('federation', fedInfo);
186
+ return true;
187
+ }
188
+
189
+ return false;
190
+ } catch (error) {
191
+ console.error(`Remove notification failed: ${error.message}`);
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Resets all federation relationships for a space
198
+ * @param {Object} backend - The GunDBBackend instance
199
+ * @param {string} spaceId - The ID of the space to reset
200
+ * @param {Object} options - Reset options
201
+ * @param {boolean} options.notifyPartners - Whether to notify federation partners (default: true)
202
+ * @param {string} options.spaceName - Optional name for the space
203
+ * @returns {Promise<Object>} Result object with success/error info
204
+ */
205
+ export async function resetFederation(backend, spaceId, options = {}) {
206
+ if (!spaceId) {
207
+ throw new Error('resetFederation: Missing required space ID');
208
+ }
209
+
210
+ const {
211
+ notifyPartners = true,
212
+ spaceName = null
213
+ } = options;
214
+
215
+ const result = {
216
+ success: false,
217
+ federatedCount: 0,
218
+ notifyCount: 0,
219
+ partnersNotified: 0,
220
+ errors: []
221
+ };
222
+
223
+ try {
224
+ const fedInfo = await getFederation(backend, spaceId);
225
+
226
+ if (!fedInfo) {
227
+ return {
228
+ ...result,
229
+ success: true,
230
+ message: 'No federation configuration found for this space'
231
+ };
232
+ }
233
+
234
+ result.federatedCount = fedInfo.federation?.length || 0;
235
+ result.notifyCount = fedInfo.notify?.length || 0;
236
+
237
+ // Create empty federation record
238
+ const emptyFedInfo = {
239
+ id: spaceId,
240
+ name: spaceName || spaceId,
241
+ federation: [],
242
+ notify: [],
243
+ timestamp: Date.now()
244
+ };
245
+
246
+ await backend.writeGlobal('federation', emptyFedInfo);
247
+
248
+ // Notify federation partners if requested
249
+ if (notifyPartners && fedInfo.federation && fedInfo.federation.length > 0) {
250
+ for (const partnerSpace of fedInfo.federation) {
251
+ try {
252
+ const partnerFedInfo = await getFederation(backend, partnerSpace);
253
+
254
+ if (partnerFedInfo) {
255
+ if (partnerFedInfo.federation) {
256
+ partnerFedInfo.federation = partnerFedInfo.federation.filter(
257
+ id => id !== spaceId.toString()
258
+ );
259
+ }
260
+
261
+ if (partnerFedInfo.notify) {
262
+ partnerFedInfo.notify = partnerFedInfo.notify.filter(
263
+ id => id !== spaceId.toString()
264
+ );
265
+ }
266
+
267
+ partnerFedInfo.timestamp = Date.now();
268
+ await backend.writeGlobal('federation', partnerFedInfo);
269
+ result.partnersNotified++;
270
+ }
271
+ } catch (error) {
272
+ result.errors.push({
273
+ partner: partnerSpace,
274
+ error: error.message
275
+ });
276
+ }
277
+ }
278
+ }
279
+
280
+ // Update federation metadata records
281
+ if (fedInfo.federation && fedInfo.federation.length > 0) {
282
+ for (const partnerSpace of fedInfo.federation) {
283
+ try {
284
+ const metaId = `${spaceId}_${partnerSpace}`;
285
+ const altMetaId = `${partnerSpace}_${spaceId}`;
286
+
287
+ let meta = await backend.readGlobal('federationMeta', metaId);
288
+ if (!meta) {
289
+ meta = await backend.readGlobal('federationMeta', altMetaId);
290
+ }
291
+
292
+ if (meta) {
293
+ meta.status = 'inactive';
294
+ meta.endedAt = Date.now();
295
+ await backend.writeGlobal('federationMeta', meta);
296
+ }
297
+ } catch (error) {
298
+ // Ignore metadata errors
299
+ }
300
+ }
301
+ }
302
+
303
+ result.success = true;
304
+ return result;
305
+ } catch (error) {
306
+ console.error(`Federation reset failed: ${error.message}`);
307
+ return {
308
+ ...result,
309
+ success: false,
310
+ error: error.message
311
+ };
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Propagates data to federated spaces
317
+ * @param {Object} backend - The GunDBBackend instance
318
+ * @param {string} holon - The holon identifier
319
+ * @param {string} lens - The lens identifier
320
+ * @param {Object} data - The data to propagate
321
+ * @param {Object} options - Propagation options
322
+ * @param {boolean} options.useReferences - Whether to use references instead of duplicating data (default: true)
323
+ * @param {string[]} options.targetSpaces - Specific target spaces to propagate to
324
+ * @returns {Promise<Object>} Result with success count and errors
325
+ */
326
+ export async function propagate(backend, holon, lens, data, options = {}) {
327
+ if (!backend || !holon || !lens || !data) {
328
+ throw new Error('propagate: Missing required parameters');
329
+ }
330
+
331
+ const {
332
+ useReferences = true,
333
+ targetSpaces = null
334
+ } = options;
335
+
336
+ const result = {
337
+ success: 0,
338
+ errors: 0,
339
+ errorDetails: [],
340
+ propagated: false,
341
+ referencesUsed: useReferences
342
+ };
343
+
344
+ try {
345
+ const fedInfo = await getFederation(backend, holon);
346
+
347
+ if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
348
+ return {
349
+ ...result,
350
+ message: `No federation found for ${holon}`
351
+ };
352
+ }
353
+
354
+ if (!fedInfo.notify || fedInfo.notify.length === 0) {
355
+ return {
356
+ ...result,
357
+ message: `No notification targets found for ${holon}`
358
+ };
359
+ }
360
+
361
+ // Filter spaces to those in notify list
362
+ let spaces = fedInfo.notify;
363
+
364
+ if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
365
+ spaces = spaces.filter(space => targetSpaces.includes(space));
366
+ }
367
+
368
+ if (spaces.length === 0) {
369
+ return {
370
+ ...result,
371
+ message: 'No valid target spaces found after filtering'
372
+ };
373
+ }
374
+
375
+ const isAlreadyReference = backend.isReference(data);
376
+
377
+ // Propagate to each target space
378
+ for (const targetSpace of spaces) {
379
+ try {
380
+ if (useReferences && !isAlreadyReference) {
381
+ // Create a reference
382
+ const reference = backend.createReference(holon, lens, data);
383
+
384
+ reference._federation = {
385
+ origin: holon,
386
+ lens: lens,
387
+ timestamp: Date.now()
388
+ };
389
+
390
+ // Build path and write reference
391
+ const path = backend.buildPath(backend.appName, targetSpace, lens, reference.id);
392
+ await backend.write(path, reference);
393
+
394
+ result.success++;
395
+ } else if (isAlreadyReference) {
396
+ // Propagate existing reference
397
+ const referenceToStore = {
398
+ ...data,
399
+ _federation: data._federation || {
400
+ origin: holon,
401
+ lens: lens,
402
+ timestamp: Date.now()
403
+ }
404
+ };
405
+
406
+ const path = backend.buildPath(backend.appName, targetSpace, lens, data.id);
407
+ await backend.write(path, referenceToStore);
408
+ result.success++;
409
+ } else {
410
+ // Store full copy
411
+ const dataToStore = {
412
+ ...data,
413
+ _federation: {
414
+ origin: holon,
415
+ lens: lens,
416
+ timestamp: Date.now()
417
+ }
418
+ };
419
+
420
+ const path = backend.buildPath(backend.appName, targetSpace, lens, data.id);
421
+ await backend.write(path, dataToStore);
422
+ result.success++;
423
+ }
424
+ } catch (error) {
425
+ result.errors++;
426
+ result.errorDetails.push({
427
+ space: targetSpace,
428
+ error: error.message
429
+ });
430
+ }
431
+ }
432
+
433
+ result.propagated = result.success > 0;
434
+ return result;
435
+ } catch (error) {
436
+ console.error('Error in propagate:', error);
437
+ return {
438
+ ...result,
439
+ error: error.message
440
+ };
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Get and combine data from local and federated sources
446
+ * @param {Object} backend - The GunDBBackend instance
447
+ * @param {string} holon - The local holon name
448
+ * @param {string} lens - The lens to query
449
+ * @param {Object} options - Options for data retrieval and aggregation
450
+ * @returns {Promise<Array>} Combined array of local and federated data
451
+ */
452
+ export async function getFederated(backend, holon, lens, options = {}) {
453
+ const {
454
+ aggregate = false,
455
+ idField = 'id',
456
+ sumFields = [],
457
+ concatArrays = [],
458
+ removeDuplicates = true,
459
+ mergeStrategy = null,
460
+ includeLocal = true,
461
+ includeFederated = true,
462
+ resolveReferences = true,
463
+ maxFederatedSpaces = -1,
464
+ timeout = 10000
465
+ } = options;
466
+
467
+ if (!backend || !holon || !lens) {
468
+ throw new Error('Missing required parameters: backend, holon, and lens are required');
469
+ }
470
+
471
+ const fedInfo = await getFederation(backend, holon);
472
+
473
+ const result = [];
474
+ const processedIds = new Set();
475
+
476
+ // Process federated spaces first
477
+ if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) {
478
+ const federatedSpaces = maxFederatedSpaces === -1
479
+ ? fedInfo.federation
480
+ : fedInfo.federation.slice(0, maxFederatedSpaces);
481
+
482
+ for (const federatedSpace of federatedSpaces) {
483
+ try {
484
+ const path = backend.buildPath(backend.appName, federatedSpace, lens);
485
+ const federatedItems = await backend.readAll(path);
486
+
487
+ for (const item of federatedItems) {
488
+ if (!item || !item[idField]) continue;
489
+
490
+ result.push(item);
491
+ processedIds.add(item[idField]);
492
+ }
493
+ } catch (error) {
494
+ console.warn(`Error processing federated space ${federatedSpace}: ${error.message}`);
495
+ }
496
+ }
497
+ }
498
+
499
+ // Get local data
500
+ if (includeLocal) {
501
+ const localPath = backend.buildPath(backend.appName, holon, lens);
502
+ const localData = await backend.readAll(localPath);
503
+
504
+ for (const item of localData) {
505
+ if (item && item[idField] && !processedIds.has(item[idField])) {
506
+ result.push(item);
507
+ processedIds.add(item[idField]);
508
+ }
509
+ }
510
+ }
511
+
512
+ // Resolve references
513
+ if (resolveReferences) {
514
+ for (let i = 0; i < result.length; i++) {
515
+ const item = result[i];
516
+
517
+ if (item.soul && item.id) {
518
+ // Simple reference format
519
+ try {
520
+ const resolved = await backend.resolveReference(item);
521
+ if (resolved) {
522
+ result[i] = resolved;
523
+ }
524
+ } catch (error) {
525
+ result[i] = {
526
+ id: item.id,
527
+ _federation: {
528
+ isReference: true,
529
+ resolved: false,
530
+ soul: item.soul,
531
+ error: error.message,
532
+ timestamp: Date.now()
533
+ }
534
+ };
535
+ }
536
+ } else if (item._federation && item._federation.isReference) {
537
+ // Legacy reference format
538
+ try {
539
+ const resolved = await backend.resolveReference(item);
540
+ if (resolved) {
541
+ result[i] = resolved;
542
+ }
543
+ } catch (error) {
544
+ console.warn(`Error resolving legacy reference: ${error.message}`);
545
+ }
546
+ }
547
+ }
548
+ }
549
+
550
+ // Apply aggregation if requested
551
+ if (aggregate && result.length > 0) {
552
+ const groupedById = result.reduce((acc, item) => {
553
+ const id = item[idField];
554
+ if (!acc[id]) {
555
+ acc[id] = [];
556
+ }
557
+ acc[id].push(item);
558
+ return acc;
559
+ }, {});
560
+
561
+ const aggregatedData = Object.values(groupedById).map(group => {
562
+ if (group.length === 1) return group[0];
563
+
564
+ if (mergeStrategy && typeof mergeStrategy === 'function') {
565
+ return mergeStrategy(group);
566
+ }
567
+
568
+ const base = { ...group[0] };
569
+
570
+ for (const field of sumFields) {
571
+ if (typeof base[field] === 'number') {
572
+ base[field] = group.reduce((sum, item) => sum + (Number(item[field]) || 0), 0);
573
+ }
574
+ }
575
+
576
+ for (const field of concatArrays) {
577
+ if (Array.isArray(base[field])) {
578
+ const allValues = group.reduce((all, item) => {
579
+ return Array.isArray(item[field]) ? [...all, ...item[field]] : all;
580
+ }, []);
581
+
582
+ base[field] = removeDuplicates ? Array.from(new Set(allValues)) : allValues;
583
+ }
584
+ }
585
+
586
+ base._aggregated = {
587
+ count: group.length,
588
+ timestamp: Date.now()
589
+ };
590
+
591
+ return base;
592
+ });
593
+
594
+ return aggregatedData;
595
+ }
596
+
597
+ return result;
598
+ }
599
+
600
+ /**
601
+ * Subscribes to federation notifications for a space
602
+ * @param {Object} backend - The GunDBBackend instance
603
+ * @param {string} spaceId - The space ID to subscribe to
604
+ * @param {Function} callback - The callback to execute on notifications
605
+ * @param {Object} options - Subscription options
606
+ * @returns {Promise<Object>} Subscription object with unsubscribe() method
607
+ */
608
+ export async function subscribeFederation(backend, spaceId, callback, options = {}) {
609
+ if (!spaceId || !callback) {
610
+ throw new Error('subscribeFederation: Missing required parameters');
611
+ }
612
+
613
+ const { lenses = ['*'], throttle = 0, resolveReferences = true } = options;
614
+
615
+ const fedInfo = await getFederation(backend, spaceId);
616
+ if (!fedInfo) {
617
+ throw new Error('No federation info found for space');
618
+ }
619
+
620
+ const subscriptions = [];
621
+ let lastNotificationTime = {};
622
+
623
+ if (fedInfo.federation && fedInfo.federation.length > 0) {
624
+ for (const federatedSpace of fedInfo.federation) {
625
+ for (const lens of lenses) {
626
+ try {
627
+ const path = backend.buildPath(backend.appName, federatedSpace, lens);
628
+
629
+ const sub = await backend.subscribe(path, async (data, key) => {
630
+ try {
631
+ if (!data || !data.id) return;
632
+
633
+ // Apply throttling
634
+ const now = Date.now();
635
+ const throttleKey = `${federatedSpace}_${lens}_${data.id}`;
636
+
637
+ if (throttle > 0) {
638
+ if (lastNotificationTime[throttleKey] &&
639
+ (now - lastNotificationTime[throttleKey]) < throttle) {
640
+ return;
641
+ }
642
+ lastNotificationTime[throttleKey] = now;
643
+ }
644
+
645
+ // Resolve reference if needed
646
+ let resolvedData = data;
647
+ if (resolveReferences && backend.isReference(data)) {
648
+ resolvedData = await backend.resolveReference(data);
649
+ }
650
+
651
+ if (!resolvedData._federation) {
652
+ resolvedData._federation = {
653
+ origin: federatedSpace,
654
+ timestamp: now
655
+ };
656
+ }
657
+
658
+ await callback(resolvedData, federatedSpace, lens);
659
+ } catch (error) {
660
+ console.warn('Federation notification error:', error);
661
+ }
662
+ }, { resolveReferences: false }); // Don't resolve in subscribe, we handle it
663
+
664
+ if (sub && typeof sub.unsubscribe === 'function') {
665
+ subscriptions.push(sub);
666
+ }
667
+ } catch (error) {
668
+ console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ return {
675
+ unsubscribe: () => {
676
+ subscriptions.forEach(sub => {
677
+ try {
678
+ if (sub && typeof sub.unsubscribe === 'function') {
679
+ sub.unsubscribe();
680
+ }
681
+ } catch (error) {
682
+ console.warn('Error unsubscribing:', error);
683
+ }
684
+ });
685
+ subscriptions.length = 0;
686
+ lastNotificationTime = {};
687
+ },
688
+ getSubscriptionCount: () => subscriptions.length
689
+ };
690
+ }
691
+
692
+ /**
693
+ * Tracks a federated message across different chats
694
+ * @param {Object} backend - The GunDBBackend instance
695
+ * @param {string} originalChatId - The ID of the original chat
696
+ * @param {string} messageId - The ID of the original message
697
+ * @param {string} federatedChatId - The ID of the federated chat
698
+ * @param {string} federatedMessageId - The ID of the message in the federated chat
699
+ * @param {string} type - The type of message
700
+ * @returns {Promise<void>}
701
+ */
702
+ export async function federateMessage(backend, originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') {
703
+ const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
704
+ let tracking = await backend.readGlobal('federation_messages', trackingKey);
705
+
706
+ if (!tracking) {
707
+ tracking = {
708
+ id: trackingKey,
709
+ originalChatId,
710
+ originalMessageId: messageId,
711
+ type,
712
+ messages: []
713
+ };
714
+ }
715
+
716
+ const existingMsg = tracking.messages.find(m => m.chatId === federatedChatId);
717
+ if (existingMsg) {
718
+ existingMsg.messageId = federatedMessageId;
719
+ existingMsg.timestamp = Date.now();
720
+ } else {
721
+ tracking.messages.push({
722
+ chatId: federatedChatId,
723
+ messageId: federatedMessageId,
724
+ timestamp: Date.now()
725
+ });
726
+ }
727
+
728
+ await backend.writeGlobal('federation_messages', tracking);
729
+ }
730
+
731
+ /**
732
+ * Gets all federated messages for a given original message
733
+ * @param {Object} backend - The GunDBBackend instance
734
+ * @param {string} originalChatId - The ID of the original chat
735
+ * @param {string} messageId - The ID of the original message
736
+ * @returns {Promise<Object|null>} The tracking information for the message
737
+ */
738
+ export async function getFederatedMessages(backend, originalChatId, messageId) {
739
+ const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
740
+ return backend.readGlobal('federation_messages', trackingKey);
741
+ }
742
+
743
+ /**
744
+ * Updates a federated message across all federated chats
745
+ * @param {Object} backend - The GunDBBackend instance
746
+ * @param {string} originalChatId - The ID of the original chat
747
+ * @param {string} messageId - The ID of the original message
748
+ * @param {Function} updateCallback - Function to update the message in each chat
749
+ * @returns {Promise<void>}
750
+ */
751
+ export async function updateFederatedMessages(backend, originalChatId, messageId, updateCallback) {
752
+ const tracking = await getFederatedMessages(backend, originalChatId, messageId);
753
+ if (!tracking?.messages) return;
754
+
755
+ for (const msg of tracking.messages) {
756
+ try {
757
+ await updateCallback(msg.chatId, msg.messageId);
758
+ } catch (error) {
759
+ console.warn(`Failed to update federated message in chat ${msg.chatId}:`, error);
760
+ }
761
+ }
762
+ }
763
+
764
+ export default {
765
+ federate,
766
+ unfederate,
767
+ getFederation,
768
+ resetFederation,
769
+ removeNotify,
770
+ propagate,
771
+ getFederated,
772
+ subscribeFederation,
773
+ federateMessage,
774
+ getFederatedMessages,
775
+ updateFederatedMessages
776
+ };