holosphere 1.1.3 → 1.1.5

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/holosphere.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as h3 from 'h3-js';
2
2
  import OpenAI from 'openai';
3
3
  import Gun from 'gun'
4
- import 'gun/sea' // Import SEA module
4
+ import SEA from 'gun/sea.js'
5
5
  import Ajv2019 from 'ajv/dist/2019.js'
6
6
 
7
7
 
@@ -14,6 +14,7 @@ class HoloSphere {
14
14
  * @param {Gun|null} gunInstance - The Gun instance to use.
15
15
  */
16
16
  constructor(appname, strict = false, openaikey = null, gunInstance = null) {
17
+ console.log('HoloSphere v1.1.8');
17
18
  this.appname = appname
18
19
  this.strict = strict;
19
20
  this.validator = new Ajv2019({
@@ -21,10 +22,10 @@ class HoloSphere {
21
22
  strict: false, // Keep this false to avoid Ajv strict mode issues
22
23
  validateSchema: true // Always validate schemas
23
24
  });
24
-
25
+
25
26
  // Use provided Gun instance or create new one
26
27
  this.gun = gunInstance || Gun({
27
- peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
28
+ peers: ['https://gun.holons.io/gun'],
28
29
  axe: false,
29
30
  // uuid: (content) => { // generate a unique id for each node
30
31
  // console.log('uuid', content);
@@ -32,7 +33,7 @@ class HoloSphere {
32
33
  });
33
34
 
34
35
  // Initialize SEA
35
- this.sea = Gun.SEA;
36
+ this.sea = SEA;
36
37
 
37
38
  if (openaikey != null) {
38
39
  this.openai = new OpenAI({
@@ -42,6 +43,9 @@ class HoloSphere {
42
43
 
43
44
  // Add currentSpace property to track logged in space
44
45
  this.currentSpace = null;
46
+
47
+ // Initialize subscriptions
48
+ this.subscriptions = {};
45
49
  }
46
50
 
47
51
  // ================================ SCHEMA FUNCTIONS ================================
@@ -62,68 +66,50 @@ class HoloSphere {
62
66
  throw new Error('setSchema: Schema must have a type field');
63
67
  }
64
68
 
65
- if (this.strict) {
66
- const metaSchema = {
67
- type: 'object',
68
- required: ['type', 'properties'],
69
+ const metaSchema = {
70
+ type: 'object',
71
+ required: ['type', 'properties'],
72
+ properties: {
73
+ type: { type: 'string' },
69
74
  properties: {
70
- type: { type: 'string' },
71
- properties: {
75
+ type: 'object',
76
+ additionalProperties: {
72
77
  type: 'object',
73
- additionalProperties: {
74
- type: 'object',
75
- required: ['type'],
76
- properties: {
77
- type: { type: 'string' }
78
- }
78
+ required: ['type'],
79
+ properties: {
80
+ type: { type: 'string' }
79
81
  }
80
- },
81
- required: {
82
- type: 'array',
83
- items: { type: 'string' }
84
82
  }
83
+ },
84
+ required: {
85
+ type: 'array',
86
+ items: { type: 'string' }
85
87
  }
86
- };
87
-
88
- const valid = this.validator.validate(metaSchema, schema);
89
- if (!valid) {
90
- throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
91
88
  }
89
+ };
92
90
 
93
- if (!schema.properties || typeof schema.properties !== 'object') {
94
- throw new Error('Schema must have properties in strict mode');
95
- }
91
+ const valid = this.validator.validate(metaSchema, schema);
92
+ if (!valid) {
93
+ throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
94
+ }
96
95
 
97
- if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
98
- throw new Error('Schema must have required fields in strict mode');
99
- }
96
+ if (!schema.properties || typeof schema.properties !== 'object') {
97
+ throw new Error('Schema must have properties in strict mode');
100
98
  }
101
99
 
102
- return new Promise((resolve, reject) => {
103
- try {
104
- const schemaString = JSON.stringify(schema);
105
- const schemaData = {
106
- schema: schemaString,
107
- timestamp: Date.now(),
108
- // Only set owner if there's an authenticated space
109
- ...(this.currentSpace && { owner: this.currentSpace.alias })
110
- };
111
-
112
- this.gun.get(this.appname)
113
- .get(lens)
114
- .get('schema')
115
- .put(schemaData, ack => {
116
- if (ack.err) {
117
- reject(new Error(ack.err));
118
- } else {
119
- // Add small delay to ensure data is written
120
- setTimeout(() => resolve(true), 50);
121
- }
122
- });
123
- } catch (error) {
124
- reject(error);
125
- }
100
+ if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
101
+ throw new Error('Schema must have required fields in strict mode');
102
+ }
103
+
104
+ // Store schema in global table with lens as key
105
+ await this.putGlobal('schemas', {
106
+ id: lens,
107
+ schema: schema,
108
+ timestamp: Date.now(),
109
+ owner: this.currentSpace?.alias
126
110
  });
111
+
112
+ return true;
127
113
  }
128
114
 
129
115
  /**
@@ -136,39 +122,12 @@ class HoloSphere {
136
122
  throw new Error('getSchema: Missing lens parameter');
137
123
  }
138
124
 
139
- return new Promise((resolve) => {
140
- let timeout = setTimeout(() => {
141
- console.warn('getSchema: Operation timed out');
142
- resolve(null);
143
- }, 5000);
144
-
145
- this.gun.get(this.appname)
146
- .get(lens)
147
- .get('schema')
148
- .once(data => {
149
- clearTimeout(timeout);
150
- if (!data) {
151
- resolve(null);
152
- return;
153
- }
125
+ const schemaData = await this.getGlobal('schemas', lens);
126
+ if (!schemaData || !schemaData.schema) {
127
+ return null;
128
+ }
154
129
 
155
- try {
156
- // Handle both new format and legacy format
157
- if (data.schema) {
158
- // New format with timestamp
159
- resolve(JSON.parse(data.schema));
160
- } else {
161
- // Legacy format or direct string
162
- const schemaStr = typeof data === 'string' ? data :
163
- Object.values(data).find(v => typeof v === 'string' && v.includes('"type":'));
164
- resolve(schemaStr ? JSON.parse(schemaStr) : null);
165
- }
166
- } catch (error) {
167
- console.error('getSchema: Error parsing schema:', error);
168
- resolve(null);
169
- }
170
- });
171
- });
130
+ return schemaData.schema;
172
131
  }
173
132
 
174
133
  // ================================ CONTENT FUNCTIONS ================================
@@ -190,8 +149,8 @@ class HoloSphere {
190
149
  // If updating existing data, check ownership
191
150
  if (data.id) {
192
151
  const existing = await this.get(holon, lens, data.id);
193
- if (existing && existing.owner &&
194
- existing.owner !== this.currentSpace.alias &&
152
+ if (existing && existing.owner &&
153
+ existing.owner !== this.currentSpace.alias &&
195
154
  !existing.federation) { // Skip ownership check for federated data
196
155
  throw new Error('Unauthorized to modify this data');
197
156
  }
@@ -221,7 +180,7 @@ class HoloSphere {
221
180
  // Deep clone data to avoid modifying the original
222
181
  const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
223
182
  const valid = this.validator.validate(schema, dataToValidate);
224
-
183
+
225
184
  if (!valid) {
226
185
  const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
227
186
  // Always throw on schema validation failure, regardless of strict mode
@@ -243,6 +202,12 @@ class HoloSphere {
243
202
  if (ack.err) {
244
203
  reject(new Error(ack.err));
245
204
  } else {
205
+ // Notify subscribers after successful put
206
+ this.notifySubscribers({
207
+ holon,
208
+ lens,
209
+ ...dataWithMeta
210
+ });
246
211
  resolve(true);
247
212
  }
248
213
  });
@@ -275,7 +240,7 @@ class HoloSphere {
275
240
  }
276
241
 
277
242
  // Propagate to each federated space
278
- const propagationPromises = fedInfo.notify.map(spaceId =>
243
+ const propagationPromises = fedInfo.notify.map(spaceId =>
279
244
  new Promise((resolve) => {
280
245
  // Store data in the federated space's lens
281
246
  this.gun.get(this.appname)
@@ -335,7 +300,7 @@ class HoloSphere {
335
300
 
336
301
  // Get local data
337
302
  const localData = await this._getAllLocal(holon, lens, schema);
338
-
303
+
339
304
  // If authenticated, get federated data
340
305
  let federatedData = [];
341
306
  if (this.currentSpace) {
@@ -344,7 +309,7 @@ class HoloSphere {
344
309
 
345
310
  // Combine and deduplicate data based on ID
346
311
  const combined = new Map();
347
-
312
+
348
313
  // Add local data first
349
314
  localData.forEach(item => {
350
315
  if (item.id) {
@@ -356,7 +321,7 @@ class HoloSphere {
356
321
  federatedData.forEach(item => {
357
322
  if (item.id) {
358
323
  const existing = combined.get(item.id);
359
- if (!existing ||
324
+ if (!existing ||
360
325
  (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
361
326
  combined.set(item.id, item);
362
327
  }
@@ -426,7 +391,7 @@ class HoloSphere {
426
391
  const output = new Map();
427
392
  let isResolved = false;
428
393
  let listener = null;
429
-
394
+
430
395
  const hardTimeout = setTimeout(() => {
431
396
  cleanup();
432
397
  resolve(Array.from(output.values()));
@@ -505,7 +470,7 @@ class HoloSphere {
505
470
  }
506
471
 
507
472
  const federatedData = new Map();
508
-
473
+
509
474
  // Get data from each federated space
510
475
  const fedPromises = fedInfo.federation.map(spaceId =>
511
476
  new Promise((resolve) => {
@@ -558,11 +523,19 @@ class HoloSphere {
558
523
  * @returns {Promise<object>} - The parsed data.
559
524
  */
560
525
  async parse(rawData) {
526
+ let parsedData = {};
527
+
561
528
  if (!rawData) {
562
529
  throw new Error('parse: No data provided');
563
530
  }
564
531
 
565
532
  try {
533
+
534
+ if (typeof rawData === 'string') {
535
+ parsedData = await JSON.parse(rawData);
536
+ }
537
+
538
+
566
539
  if (rawData.soul) {
567
540
  const data = await this.getNodeRef(rawData.soul).once();
568
541
  if (!data) {
@@ -571,7 +544,7 @@ class HoloSphere {
571
544
  return JSON.parse(data);
572
545
  }
573
546
 
574
- let parsedData = {};
547
+
575
548
  if (typeof rawData === 'object' && rawData !== null) {
576
549
  if (rawData._ && rawData._["#"]) {
577
550
  const pathParts = rawData._['#'].split('/');
@@ -591,11 +564,10 @@ class HoloSphere {
591
564
  } else {
592
565
  parsedData = rawData;
593
566
  }
594
- } else {
595
- parsedData = JSON.parse(rawData);
596
567
  }
597
568
 
598
569
  return parsedData;
570
+
599
571
  } catch (error) {
600
572
  console.log("Parsing not a JSON, returning raw data", rawData);
601
573
  return rawData;
@@ -632,7 +604,7 @@ class HoloSphere {
632
604
  .get(key)
633
605
  .once(async (data) => {
634
606
  clearTimeout(timeout);
635
-
607
+
636
608
  if (!data) {
637
609
  resolve(null);
638
610
  return;
@@ -658,7 +630,7 @@ class HoloSphere {
658
630
  // 2. User is the owner
659
631
  // 3. User is in shared list
660
632
  // 4. Data is from federation
661
- if (parsed.owner &&
633
+ if (parsed.owner &&
662
634
  this.currentSpace?.alias !== parsed.owner &&
663
635
  (!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
664
636
  (!parsed.federation || !parsed.federation.origin)) {
@@ -711,7 +683,7 @@ class HoloSphere {
711
683
  if (!data) {
712
684
  return true; // Nothing to delete
713
685
  }
714
-
686
+
715
687
  if (data.owner && data.owner !== this.currentSpace.alias) {
716
688
  throw new Error('Unauthorized to delete this data');
717
689
  }
@@ -780,7 +752,7 @@ class HoloSphere {
780
752
  .catch(error => {
781
753
  console.error('Error in deleteAll:', error);
782
754
  resolve(false);
783
- });
755
+ });
784
756
  });
785
757
  });
786
758
  }
@@ -985,12 +957,12 @@ class HoloSphere {
985
957
  }
986
958
 
987
959
  const keys = Object.keys(data).filter(key => key !== '_');
988
- const promises = keys.map(key =>
960
+ const promises = keys.map(key =>
989
961
  new Promise(async (resolveItem) => {
990
962
  const itemData = await new Promise(resolveData => {
991
963
  this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
992
964
  });
993
-
965
+
994
966
  if (itemData) {
995
967
  try {
996
968
  const parsed = await this.parse(itemData);
@@ -1088,7 +1060,7 @@ class HoloSphere {
1088
1060
  }
1089
1061
 
1090
1062
  const keys = Object.keys(data).filter(key => key !== '_');
1091
- const promises = keys.map(key =>
1063
+ const promises = keys.map(key =>
1092
1064
  new Promise((resolveDelete) => {
1093
1065
  this.gun.get(this.appname)
1094
1066
  .get(tableName)
@@ -1120,21 +1092,62 @@ class HoloSphere {
1120
1092
 
1121
1093
  // ================================ COMPUTE FUNCTIONS ================================
1122
1094
  /**
1123
- * Computes summaries based on the content within a holon and lens.
1095
+
1096
+ /**
1097
+ * Computes operations across multiple layers up the hierarchy
1098
+ * @param {string} holon - Starting holon identifier
1099
+ * @param {string} lens - The lens to compute
1100
+ * @param {object} options - Computation options
1101
+ * @param {number} [maxLevels=15] - Maximum levels to compute up
1102
+ */
1103
+ async computeHierarchy(holon, lens, options, maxLevels = 15) {
1104
+ let currentHolon = holon;
1105
+ let currentRes = h3.getResolution(currentHolon);
1106
+ const results = [];
1107
+
1108
+ while (currentRes > 0 && maxLevels > 0) {
1109
+ try {
1110
+ const result = await this.compute(currentHolon, lens, options);
1111
+ if (result) {
1112
+ results.push(result);
1113
+ }
1114
+ currentHolon = h3.cellToParent(currentHolon, currentRes - 1);
1115
+ currentRes--;
1116
+ maxLevels--;
1117
+ } catch (error) {
1118
+ console.error('Error in compute hierarchy:', error);
1119
+ break;
1120
+ }
1121
+ }
1122
+
1123
+ return results;
1124
+ }
1125
+
1126
+ /* Computes operations on content within a holon and lens for one layer up.
1124
1127
  * @param {string} holon - The holon identifier.
1125
1128
  * @param {string} lens - The lens to compute.
1126
- * @param {string} operation - The operation to perform.
1127
- * @param {number} [depth=0] - Current recursion depth.
1128
- * @param {number} [maxDepth=15] - Maximum recursion depth.
1129
+ * @param {object} options - Computation options
1130
+ * @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
1131
+ * @param {string[]} [options.fields] - Fields to perform operation on
1132
+ * @param {string} [options.targetField] - Field to store the result in
1129
1133
  * @throws {Error} If parameters are invalid or missing
1130
1134
  */
1131
- async compute(holon, lens, operation, depth = 0, maxDepth = 15) {
1135
+ async compute(holon, lens, options) {
1132
1136
  // Validate required parameters
1133
- if (!holon || !lens || !operation) {
1137
+ if (!holon || !lens) {
1138
+ throw new Error('compute: Missing required parameters');
1139
+ }
1140
+
1141
+ // Convert string operation to options object
1142
+ if (typeof options === 'string') {
1143
+ options = { operation: options };
1144
+ }
1145
+
1146
+ if (!options?.operation) {
1134
1147
  throw new Error('compute: Missing required parameters');
1135
1148
  }
1136
1149
 
1137
- // Validate holon format and resolution
1150
+ // Validate holon format and resolution first
1138
1151
  let res;
1139
1152
  try {
1140
1153
  res = h3.getResolution(holon);
@@ -1146,141 +1159,108 @@ class HoloSphere {
1146
1159
  throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
1147
1160
  }
1148
1161
 
1149
- // Validate depth parameters
1150
- if (typeof depth !== 'number' || depth < 0) {
1162
+ const {
1163
+ operation,
1164
+ fields = [],
1165
+ targetField,
1166
+ depth,
1167
+ maxDepth
1168
+ } = options;
1169
+
1170
+ // Validate depth parameters if provided
1171
+ if (depth !== undefined && depth < 0) {
1151
1172
  throw new Error('compute: Invalid depth parameter');
1152
1173
  }
1153
1174
 
1154
- if (typeof maxDepth !== 'number' || maxDepth < 1 || maxDepth > 15) {
1175
+ if (maxDepth !== undefined && (maxDepth < 1 || maxDepth > 15)) {
1155
1176
  throw new Error('compute: Invalid maxDepth parameter (must be between 1 and 15)');
1156
1177
  }
1157
1178
 
1158
- if (depth >= maxDepth) {
1159
- return;
1160
- }
1161
-
1162
1179
  // Validate operation
1163
- if (typeof operation !== 'string' || !['summarize'].includes(operation)) {
1164
- throw new Error('compute: Invalid operation (must be "summarize")');
1180
+ const validOperations = ['summarize', 'aggregate', 'concatenate'];
1181
+ if (!validOperations.includes(operation)) {
1182
+ throw new Error(`compute: Invalid operation (must be one of ${validOperations.join(', ')})`);
1165
1183
  }
1166
1184
 
1167
1185
  const parent = h3.cellToParent(holon, res - 1);
1168
1186
  const siblings = h3.cellToChildren(parent, res);
1169
1187
 
1170
- const content = [];
1171
- const promises = siblings.map(sibling =>
1172
- new Promise((resolve) => {
1173
- const timeout = setTimeout(() => {
1174
- console.warn(`Timeout for sibling ${sibling}`);
1175
- resolve();
1176
- }, 10000);
1177
-
1178
- this.gun.get(this.appname)
1179
- .get(sibling)
1180
- .get(lens)
1181
- .map()
1182
- .once((data) => {
1183
- clearTimeout(timeout);
1184
- if (!data) {
1185
- resolve();
1186
- return;
1187
- }
1188
-
1189
- try {
1190
- // Parse the data if it's a string
1191
- const parsed = typeof data === 'string' ? JSON.parse(data) : data;
1192
- if (parsed && parsed.content) {
1193
- content.push(parsed.content);
1194
- }
1195
- } catch (error) {
1196
- console.warn('Error parsing data:', error);
1197
- }
1198
- resolve();
1199
- });
1200
- })
1188
+ // Collect all content from siblings
1189
+ const contents = await Promise.all(
1190
+ siblings.map(sibling => this.getAll(sibling, lens))
1201
1191
  );
1202
1192
 
1203
- await Promise.all(promises);
1193
+ const flatContents = contents.flat().filter(Boolean);
1204
1194
 
1205
- if (content.length > 0) {
1195
+ if (flatContents.length > 0) {
1206
1196
  try {
1207
- const computed = await this.summarize(content.join('\n'));
1197
+ let computed;
1198
+ switch (operation) {
1199
+ case 'summarize':
1200
+ // For summarize, concatenate specified fields or use entire content
1201
+ const textToSummarize = fields.length > 0
1202
+ ? flatContents.map(item => fields.map(field => item[field]).filter(Boolean).join('\n')).join('\n')
1203
+ : JSON.stringify(flatContents);
1204
+ computed = await this.summarize(textToSummarize);
1205
+ break;
1206
+
1207
+ case 'aggregate':
1208
+ // For aggregate, sum numeric fields
1209
+ computed = fields.reduce((acc, field) => {
1210
+ acc[field] = flatContents.reduce((sum, item) => {
1211
+ return sum + (Number(item[field]) || 0);
1212
+ }, 0);
1213
+ return acc;
1214
+ }, {});
1215
+ break;
1216
+
1217
+ case 'concatenate':
1218
+ // For concatenate, combine arrays or strings
1219
+ computed = fields.reduce((acc, field) => {
1220
+ acc[field] = flatContents.reduce((combined, item) => {
1221
+ const value = item[field];
1222
+ if (Array.isArray(value)) {
1223
+ return [...combined, ...value];
1224
+ } else if (value) {
1225
+ return [...combined, value];
1226
+ }
1227
+ return combined;
1228
+ }, []);
1229
+ // Remove duplicates if array
1230
+ acc[field] = Array.from(new Set(acc[field]));
1231
+ return acc;
1232
+ }, {});
1233
+ break;
1234
+ }
1235
+
1208
1236
  if (computed) {
1209
- const summaryId = `${parent}_summary`;
1210
- await this.put(parent, lens, {
1211
- id: summaryId,
1212
- content: computed,
1237
+ const resultId = `${parent}_${operation}`;
1238
+ const result = {
1239
+ id: resultId,
1213
1240
  timestamp: Date.now()
1214
- });
1241
+ };
1215
1242
 
1216
- if (res > 1) { // Only recurse if not at top level
1217
- await this.compute(parent, lens, operation, depth + 1, maxDepth);
1243
+ // Store result in targetField if specified, otherwise at root level
1244
+ if (targetField) {
1245
+ result[targetField] = computed;
1246
+ } else if (typeof computed === 'object') {
1247
+ Object.assign(result, computed);
1248
+ } else {
1249
+ result.value = computed;
1218
1250
  }
1251
+
1252
+ await this.put(parent, lens, result);
1253
+ return result;
1219
1254
  }
1220
1255
  } catch (error) {
1221
1256
  console.warn('Error in compute operation:', error);
1222
- // Don't throw here to maintain graceful handling of compute errors
1257
+ throw error;
1223
1258
  }
1224
1259
  }
1225
1260
 
1226
- // Return successfully even if no content was found or processed
1227
- return;
1228
- }
1229
-
1230
- /**
1231
- * Clears all entities under a specific holon and lens.
1232
- * @param {string} holon - The holon identifier.
1233
- * @param {string} lens - The lens to clear.
1234
- */
1235
- async clearlens(holon, lens) {
1236
- if (!holon || !lens) {
1237
- throw new Error('clearlens: Missing required parameters');
1238
- }
1239
-
1240
- return new Promise((resolve, reject) => {
1241
- try {
1242
- const deletions = new Set();
1243
- const timeout = setTimeout(() => {
1244
- if (deletions.size === 0) {
1245
- resolve(); // No data to delete
1246
- }
1247
- }, 1000);
1248
-
1249
- this.gun.get(this.appname)
1250
- .get(holon)
1251
- .get(lens)
1252
- .map()
1253
- .once((data, key) => {
1254
- if (data) {
1255
- const deletion = new Promise((resolveDelete) => {
1256
- this.gun.get(this.appname)
1257
- .get(holon)
1258
- .get(lens)
1259
- .get(key)
1260
- .put(null, ack => {
1261
- if (ack.err) {
1262
- console.error(`Failed to delete ${key}:`, ack.err);
1263
- }
1264
- resolveDelete();
1265
- });
1266
- });
1267
- deletions.add(deletion);
1268
- deletion.finally(() => {
1269
- deletions.delete(deletion);
1270
- if (deletions.size === 0) {
1271
- clearTimeout(timeout);
1272
- resolve();
1273
- }
1274
- });
1275
- }
1276
- });
1277
- } catch (error) {
1278
- reject(error);
1279
- }
1280
- });
1261
+ return null;
1281
1262
  }
1282
1263
 
1283
-
1284
1264
  /**
1285
1265
  * Summarizes provided history text using OpenAI.
1286
1266
  * @param {string} history - The history text to summarize.
@@ -1406,40 +1386,33 @@ class HoloSphere {
1406
1386
  * @param {function} callback - The callback to execute on changes.
1407
1387
  */
1408
1388
  async subscribe(holon, lens, callback) {
1409
- if (!holon || !lens || !callback) {
1410
- throw new Error('subscribe: Missing required parameters');
1411
- }
1412
-
1413
- const ref = this.gun.get(this.appname)
1414
- .get(holon)
1415
- .get(lens);
1416
-
1417
- // Create a more robust handler
1418
- const handler = async (data, key) => {
1419
- if (!data || key === '_') return; // Skip empty data or Gun metadata
1420
-
1421
- try {
1422
- const parsed = typeof data === 'string' ? await this.parse(data) : data;
1423
- if (parsed) {
1424
- await callback(parsed);
1389
+ const subscriptionId = this.generateId();
1390
+ this.subscriptions[subscriptionId] =
1391
+ this.gun.get(this.appname).get(holon).get(lens).map().on( async (data, key) => {
1392
+ if (data) {
1393
+ try {
1394
+ let parsed = await this.parse(data)
1395
+ callback(parsed, key)
1396
+ } catch (error) {
1397
+ console.error('Error in subscribe:', error);
1398
+ }
1425
1399
  }
1426
- } catch (error) {
1427
- console.warn('Subscription handler error:', error);
1400
+ })
1401
+ return {
1402
+ unsubscribe: () => {
1403
+ this.gun.get(this.appname).get(holon).get(lens).map().off()
1404
+ delete this.subscriptions[subscriptionId];
1428
1405
  }
1429
- };
1406
+ }
1407
+ }
1430
1408
 
1431
- // Subscribe using Gun's map() and on()
1432
- const chain = ref.map();
1433
- chain.on(handler);
1434
1409
 
1435
- // Return subscription object
1436
- return {
1437
- off: () => {
1438
- if (chain) {
1439
- chain.off();
1440
- }
1410
+ notifySubscribers(data) {
1411
+ Object.values(this.subscriptions).forEach(subscription => {
1412
+ if (subscription.active && this.matchesQuery(data, subscription.query)) {
1413
+ subscription.callback(data);
1441
1414
  }
1442
- };
1415
+ });
1443
1416
  }
1444
1417
 
1445
1418
  // Add ID generation method
@@ -1447,6 +1420,12 @@ class HoloSphere {
1447
1420
  return Date.now().toString(10) + Math.random().toString(2);
1448
1421
  }
1449
1422
 
1423
+ matchesQuery(data, query) {
1424
+ return data && query &&
1425
+ data.holon === query.holon &&
1426
+ data.lens === query.lens;
1427
+ }
1428
+
1450
1429
  /**
1451
1430
  * Creates a new space with the given credentials
1452
1431
  * @param {string} spacename - The space identifier/username
@@ -1467,7 +1446,7 @@ class HoloSphere {
1467
1446
  try {
1468
1447
  // Generate key pair
1469
1448
  const pair = await Gun.SEA.pair();
1470
-
1449
+
1471
1450
  // Create auth record with SEA
1472
1451
  const salt = await Gun.SEA.random(64).toString('base64');
1473
1452
  const hash = await Gun.SEA.work(password, salt);
@@ -1505,8 +1484,8 @@ class HoloSphere {
1505
1484
  */
1506
1485
  async login(spacename, password) {
1507
1486
  // Validate input
1508
- if (!spacename || !password ||
1509
- typeof spacename !== 'string' ||
1487
+ if (!spacename || !password ||
1488
+ typeof spacename !== 'string' ||
1510
1489
  typeof password !== 'string') {
1511
1490
  throw new Error('Invalid credentials format');
1512
1491
  }
@@ -1738,10 +1717,10 @@ class HoloSphere {
1738
1717
 
1739
1718
  // Get federation info for current space
1740
1719
  const fedInfo = await this.getFederation(this.currentSpace?.alias);
1741
-
1720
+
1742
1721
  // Get local data
1743
1722
  const localData = await this.getAll(holon, lens);
1744
-
1723
+
1745
1724
  // If no federation or not authenticated, return local data only
1746
1725
  if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
1747
1726
  return localData;
@@ -1828,15 +1807,15 @@ class HoloSphere {
1828
1807
  if (!removeDuplicates) {
1829
1808
  return allData;
1830
1809
  }
1831
-
1810
+
1832
1811
  // Remove duplicates keeping the most recent version
1833
1812
  const uniqueMap = new Map();
1834
1813
  allData.forEach(item => {
1835
1814
  const id = item[idField];
1836
1815
  if (!id) return;
1837
-
1816
+
1838
1817
  const existing = uniqueMap.get(id);
1839
- if (!existing ||
1818
+ if (!existing ||
1840
1819
  (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
1841
1820
  uniqueMap.set(id, item);
1842
1821
  }