holosphere 1.1.10 → 1.1.11

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.
@@ -6,22 +6,39 @@ jest.setTimeout(30000);
6
6
 
7
7
  describe('Subscription Tests', () => {
8
8
  let holosphere;
9
- const testHolon = `test_subscription_${Date.now()}`;
9
+ let testAppName; // Make app name dynamic
10
+ const testHolonBase = 'test_subscription_holon';
10
11
  const testLens = 'items';
12
+ let testHolon; // Make holon dynamic
11
13
 
12
- beforeEach(() => {
13
- // Create a fresh HoloSphere instance for each test
14
- holosphere = new HoloSphere('testApp', false);
14
+ beforeEach(async () => {
15
+ // Create a fresh HoloSphere instance with unique names for each test
16
+ testAppName = `testApp_${Date.now()}_${Math.random().toString(36).substring(7)}`;
17
+ testHolon = `${testHolonBase}_${Date.now()}`;
18
+ holosphere = new HoloSphere(testAppName, false);
19
+ // Add a small delay after initialization
20
+ await new Promise(resolve => setTimeout(resolve, 100));
15
21
  });
16
22
 
17
23
  afterEach(async () => {
18
- // Clean up resources
24
+ // Clean up resources and potentially test data
19
25
  try {
20
- await holosphere.close();
26
+ if (holosphere) {
27
+ // Attempt to delete data created in the test holon/lenses
28
+ try {
29
+ await holosphere.deleteAll(testHolon, testLens);
30
+ await holosphere.deleteAll(testHolon, 'differentLens');
31
+ // Wait a bit for deletes to process
32
+ await new Promise(resolve => setTimeout(resolve, 200));
33
+ } catch (deleteError) {
34
+ console.warn(`Error during test cleanup deleteAll:`, deleteError);
35
+ }
36
+ await holosphere.close();
37
+ }
21
38
  } catch (error) {
22
- console.warn('Error closing HoloSphere:', error);
39
+ console.warn('Error during afterEach cleanup:', error);
23
40
  }
24
- jest.clearAllMocks();
41
+ // jest.clearAllMocks(); // Not needed if not using jest.fn()
25
42
  });
26
43
 
27
44
  test('should properly clean up subscription when unsubscribing', async () => {
@@ -32,7 +49,7 @@ describe('Subscription Tests', () => {
32
49
  };
33
50
 
34
51
  // Create a mock callback function
35
- function mockCallback (data) {
52
+ function mockCallback (data) {
36
53
  console.log('Callback received:', data);
37
54
  }
38
55
 
@@ -47,11 +64,10 @@ describe('Subscription Tests', () => {
47
64
 
48
65
  // Verify the subscription object has the expected structure
49
66
  expect(holosphere.subscriptions[subscriptionId]).toBeDefined();
50
- expect(holosphere.subscriptions[subscriptionId].active).toBe(true);
51
67
  expect(holosphere.subscriptions[subscriptionId].holon).toBe(testHolon);
52
68
  expect(holosphere.subscriptions[subscriptionId].lens).toBe(testLens);
53
69
  expect(holosphere.subscriptions[subscriptionId].callback).toBe(mockCallback);
54
- expect(holosphere.subscriptions[subscriptionId].gunSubscription).toBeDefined();
70
+ expect(holosphere.subscriptions[subscriptionId].mapChain).toBeDefined();
55
71
 
56
72
  // Now unsubscribe
57
73
  await subscription.unsubscribe();
@@ -64,104 +80,123 @@ describe('Subscription Tests', () => {
64
80
  test('should not receive updates after unsubscribing', async () => {
65
81
  // Create test data
66
82
  const testData = {
67
- id: 'test-item',
68
- value: 'Test value'
83
+ id: 'test-item-unsub',
84
+ value: 'Initial value for unsub test'
69
85
  };
70
-
71
- // Create a mock callback function
72
- const mockCallback = jest.fn((data) => {
73
- console.log('Callback received:', data);
74
- });
75
-
86
+
87
+ // Use a flag to track callback execution
88
+ let callbackFired = false;
89
+ let receivedData = null;
90
+ const dataCallback = (data) => {
91
+ console.log('Callback received (unsub test):', data);
92
+ callbackFired = true;
93
+ receivedData = data;
94
+ };
95
+
76
96
  // Set up subscription
77
- const subscription = await holosphere.subscribe(testHolon, testLens, mockCallback);
78
-
97
+ const subscription = await holosphere.subscribe(testHolon, testLens, dataCallback);
98
+
79
99
  // Store data to trigger subscription
80
100
  await holosphere.put(testHolon, testLens, testData);
81
-
101
+
82
102
  // Wait a bit for the subscription to trigger
83
103
  await new Promise(resolve => setTimeout(resolve, 1000));
84
-
104
+
85
105
  // Verify callback was called
86
- expect(mockCallback).toHaveBeenCalled();
87
-
88
- // Reset the mock
89
- mockCallback.mockClear();
90
-
106
+ expect(callbackFired).toBe(true);
107
+ expect(receivedData).toEqual(expect.objectContaining(testData));
108
+
109
+ // Reset the flag
110
+ callbackFired = false;
111
+ receivedData = null;
112
+
91
113
  // Unsubscribe
92
114
  await subscription.unsubscribe();
93
-
115
+
94
116
  // Store more data
95
- const newData = { ...testData, value: 'Updated value' };
117
+ const newData = { ...testData, value: 'Updated value after unsub' };
96
118
  await holosphere.put(testHolon, testLens, newData);
97
-
119
+
98
120
  // Wait a bit to ensure no callbacks are triggered
99
121
  await new Promise(resolve => setTimeout(resolve, 1000));
100
-
122
+
101
123
  // Verify callback was NOT called after unsubscribing
102
- expect(mockCallback).not.toHaveBeenCalled();
124
+ expect(callbackFired).toBe(false);
125
+ expect(receivedData).toBeNull();
103
126
  });
104
127
 
105
128
  test('should handle multiple subscriptions and unsubscriptions correctly', async () => {
106
- // Create mock callbacks
107
- const mockCallback1 = jest.fn((data) => {
108
- console.log('Callback 1 received:', data);
109
- });
110
- const mockCallback2 = jest.fn((data) => {
111
- console.log('Callback 2 received:', data);
112
- });
113
- const mockCallback3 = jest.fn((data) => {
114
- console.log('Callback 3 received:', data);
115
- });
116
-
129
+ // Use flags to track callback execution
130
+ let callback1Fired = false;
131
+ let callback2Fired = false;
132
+ let callback3Fired = false;
133
+ let dataForCb1 = null;
134
+
135
+ const cb1 = (data) => {
136
+ console.log('Callback 1 received (multi test):', data);
137
+ callback1Fired = true;
138
+ dataForCb1 = data;
139
+ };
140
+ const cb2 = (data) => {
141
+ console.log('Callback 2 received (multi test):', data);
142
+ callback2Fired = true;
143
+ };
144
+ const cb3 = (data) => {
145
+ console.log('Callback 3 received (multi test):', data);
146
+ callback3Fired = true;
147
+ };
148
+
117
149
  // Set up multiple subscriptions
118
- const subscription1 = await holosphere.subscribe(testHolon, testLens, mockCallback1);
119
- const subscription2 = await holosphere.subscribe(testHolon, testLens, mockCallback2);
120
- const subscription3 = await holosphere.subscribe(testHolon, 'differentLens', mockCallback3);
121
-
150
+ const subscription1 = await holosphere.subscribe(testHolon, testLens, cb1);
151
+ const subscription2 = await holosphere.subscribe(testHolon, testLens, cb2);
152
+ const subscription3 = await holosphere.subscribe(testHolon, 'differentLens', cb3);
153
+
122
154
  // Verify subscriptions were created
123
155
  expect(Object.keys(holosphere.subscriptions).length).toBe(3);
124
-
125
- // Unsubscribe from one subscription
156
+
157
+ // Unsubscribe from one subscription (subscription2)
126
158
  await subscription2.unsubscribe();
127
-
159
+
128
160
  // Verify only the correct subscription was removed
129
161
  expect(Object.keys(holosphere.subscriptions).length).toBe(2);
130
-
162
+
131
163
  // Create test data
132
164
  const testData = {
133
- id: 'test-item',
134
- value: 'Test value'
165
+ id: 'test-item-multi',
166
+ value: 'Test value for multi sub'
135
167
  };
136
-
168
+
137
169
  // Store data to trigger remaining subscriptions
138
170
  await holosphere.put(testHolon, testLens, testData);
139
-
171
+
140
172
  // Wait a bit for the subscription to trigger
141
173
  await new Promise(resolve => setTimeout(resolve, 1000));
142
-
174
+
143
175
  // Verify only active callbacks were called
144
- expect(mockCallback1).toHaveBeenCalled();
145
- expect(mockCallback2).not.toHaveBeenCalled();
146
- expect(mockCallback3).not.toHaveBeenCalled(); // Different lens
147
-
176
+ expect(callback1Fired).toBe(true);
177
+ expect(dataForCb1).toEqual(expect.objectContaining(testData));
178
+ expect(callback2Fired).toBe(false); // This callback should not have fired
179
+ expect(callback3Fired).toBe(false); // Different lens
180
+
148
181
  // Unsubscribe from all remaining subscriptions
149
182
  await subscription1.unsubscribe();
150
183
  await subscription3.unsubscribe();
151
-
184
+
152
185
  // Verify all subscriptions were removed
153
186
  expect(Object.keys(holosphere.subscriptions).length).toBe(0);
154
187
  });
155
188
 
156
189
  test('should clean up subscriptions when closing HoloSphere instance', async () => {
157
- // Create mock callback
158
- const mockCallback = jest.fn((data) => {
159
- console.log('Callback received:', data);
160
- });
161
-
190
+ // Use a flag - not strictly needed for assertion but good practice
191
+ let callbackFired = false;
192
+ const dataCallback = (data) => {
193
+ console.log('Callback received (close test):', data);
194
+ callbackFired = true;
195
+ };
196
+
162
197
  // Set up subscription
163
- await holosphere.subscribe(testHolon, testLens, mockCallback);
164
-
198
+ await holosphere.subscribe(testHolon, testLens, dataCallback);
199
+
165
200
  // Verify subscription was created
166
201
  expect(Object.keys(holosphere.subscriptions).length).toBe(1);
167
202
 
@@ -303,7 +338,7 @@ describe('Subscription Tests', () => {
303
338
  }
304
339
 
305
340
  // Try to resolve the reference to verify it works correctly
306
- const resolvedData = await holosphere.get(referenceHolon, testLens, originalData.id, null, { resolveReferences: true });
341
+ const resolvedData = await holosphere.get(referenceHolon, testLens, originalData.id, null, { resolveHolograms: true });
307
342
  console.log('Resolved data:', resolvedData);
308
343
 
309
344
  // The resolved data should not be null
package/utils.js ADDED
@@ -0,0 +1,290 @@
1
+ // holo_utils.js
2
+ import * as h3 from 'h3-js';
3
+
4
+ /**
5
+ * Converts latitude and longitude to a holon identifier.
6
+ * @param {number} lat - The latitude.
7
+ * @param {number} lng - The longitude.
8
+ * @param {number} resolution - The resolution level.
9
+ * @returns {Promise<string>} - The resulting holon identifier.
10
+ */
11
+ export async function getHolon(lat, lng, resolution) { // Doesn't need holoInstance
12
+ return h3.latLngToCell(lat, lng, resolution);
13
+ }
14
+
15
+ /**
16
+ * Retrieves all containing holonagons at all scales for given coordinates.
17
+ * @param {number} lat - The latitude.
18
+ * @param {number} lng - The longitude.
19
+ * @returns {Array<string>} - List of holon identifiers.
20
+ */
21
+ export function getScalespace(lat, lng) { // Doesn't need holoInstance
22
+ let list = []
23
+ let cell = h3.latLngToCell(lat, lng, 14);
24
+ list.push(cell)
25
+ for (let i = 13; i >= 0; i--) {
26
+ list.push(h3.cellToParent(cell, i))
27
+ }
28
+ return list
29
+ }
30
+
31
+ /**
32
+ * Retrieves all containing holonagons at all scales for a given holon.
33
+ * @param {string} holon - The holon identifier.
34
+ * @returns {Array<string>} - List of holon identifiers.
35
+ */
36
+ export function getHolonScalespace(holon) { // Doesn't need holoInstance
37
+ let list = []
38
+ let res = h3.getResolution(holon)
39
+ for (let i = res; i >= 0; i--) {
40
+ list.push(h3.cellToParent(holon, i))
41
+ }
42
+ return list
43
+ }
44
+
45
+ /**
46
+ * Subscribes to changes in a specific holon and lens.
47
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
48
+ * @param {string} holon - The holon identifier.
49
+ * @param {string} lens - The lens to subscribe to.
50
+ * @param {function} callback - The callback to execute on changes.
51
+ * @returns {Promise<object>} - Subscription object with unsubscribe method
52
+ */
53
+ export async function subscribe(holoInstance, holon, lens, callback) {
54
+ if (!holon || !lens) {
55
+ throw new Error('subscribe: Missing holon or lens parameters:', holon, lens);
56
+ }
57
+
58
+ if (!callback || typeof callback !== 'function') {
59
+ throw new Error('subscribe: Callback must be a function');
60
+ }
61
+
62
+ const subscriptionId = holoInstance.generateId(); // Use instance's generateId
63
+
64
+ try {
65
+ // Get the Gun chain up to the map()
66
+ const mapChain = holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).map();
67
+
68
+ // Create the subscription by calling .on() on the map chain
69
+ const gunListener = mapChain.on(async (data, key) => { // Renamed variable
70
+ // Check if subscription ID still exists (might have been unsubscribed)
71
+ if (!holoInstance.subscriptions[subscriptionId]) {
72
+ return;
73
+ }
74
+
75
+ if (data) {
76
+ try {
77
+ let parsed = await holoInstance.parse(data);
78
+ if (parsed && holoInstance.isHologram(parsed)) {
79
+ const resolved = await holoInstance.resolveHologram(parsed, { followHolograms: true });
80
+ if (resolved !== parsed) {
81
+ parsed = resolved;
82
+ }
83
+ }
84
+
85
+ // Check again if subscription ID still exists before calling callback
86
+ if (holoInstance.subscriptions[subscriptionId]) {
87
+ callback(parsed, key);
88
+ }
89
+ } catch (error) {
90
+ console.error('Error processing subscribed data:', error);
91
+ }
92
+ }
93
+ });
94
+
95
+ // Store the subscription with its ID on the instance
96
+ holoInstance.subscriptions[subscriptionId] = {
97
+ id: subscriptionId,
98
+ holon,
99
+ lens,
100
+ callback,
101
+ mapChain: mapChain, // Store the map chain
102
+ gunListener: gunListener // Store the listener too (optional, maybe needed for close?)
103
+ };
104
+
105
+ // Return an object with unsubscribe method
106
+ return {
107
+ unsubscribe: async () => {
108
+ const sub = holoInstance.subscriptions[subscriptionId];
109
+ if (!sub) {
110
+ return;
111
+ }
112
+
113
+ try {
114
+ // Turn off the Gun subscription using the stored mapChain reference
115
+ if (sub.mapChain) { // Check if mapChain exists
116
+ sub.mapChain.off(); // Call off() on the chain where .on() was attached
117
+ // Optional: Add delay back? Let's omit for now.
118
+ // await new Promise(res => setTimeout(res, 50));
119
+ } // We might not need to call off() on gunListener explicitly
120
+
121
+ // Remove from subscriptions object AFTER turning off listener
122
+ delete holoInstance.subscriptions[subscriptionId];
123
+ } catch (error) {
124
+ console.error(`Error during unsubscribe logic for ${subscriptionId}:`, error);
125
+ }
126
+ }
127
+ };
128
+ } catch (error) {
129
+ console.error('Error creating subscription:', error);
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Notifies subscribers about data changes
136
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
137
+ * @param {object} data - The data to notify about
138
+ * @private
139
+ */
140
+ export function notifySubscribers(holoInstance, data) {
141
+ if (!data || !data.holon || !data.lens) {
142
+ return;
143
+ }
144
+
145
+ try {
146
+ Object.values(holoInstance.subscriptions).forEach(subscription => {
147
+ if (subscription.holon === data.holon &&
148
+ subscription.lens === data.lens) {
149
+ try {
150
+ if (subscription.callback && typeof subscription.callback === 'function') {
151
+ subscription.callback(data);
152
+ }
153
+ } catch (error) {
154
+ console.warn('Error in subscription callback:', error);
155
+ }
156
+ }
157
+ });
158
+ } catch (error) {
159
+ console.warn('Error notifying subscribers:', error);
160
+ }
161
+ }
162
+
163
+ // Add ID generation method
164
+ export function generateId() { // Doesn't need holoInstance
165
+ return Date.now().toString(10) + Math.random().toString(2);
166
+ }
167
+
168
+ /**
169
+ * Closes the HoloSphere instance and cleans up resources.
170
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
171
+ * @returns {Promise<void>}
172
+ */
173
+ export async function close(holoInstance) {
174
+ try {
175
+ if (holoInstance.gun) {
176
+ // Unsubscribe from all subscriptions
177
+ const subscriptionIds = Object.keys(holoInstance.subscriptions);
178
+ for (const id of subscriptionIds) {
179
+ try {
180
+ const subscription = holoInstance.subscriptions[id];
181
+ if (subscription) {
182
+ // Turn off the Gun subscription using the stored mapChain reference
183
+ if (subscription.mapChain) {
184
+ subscription.mapChain.off();
185
+ } // Also turn off listener directly? Might be redundant.
186
+ // if (subscription.gunListener) {
187
+ // subscription.gunListener.off();
188
+ // }
189
+ }
190
+ } catch (error) {
191
+ console.warn(`Error cleaning up subscription ${id}:`, error);
192
+ }
193
+ }
194
+
195
+ // Clear subscriptions
196
+ holoInstance.subscriptions = {};
197
+
198
+ // Clear schema cache using instance method
199
+ holoInstance.clearSchemaCache();
200
+
201
+ // Close Gun connections
202
+ if (holoInstance.gun.back) {
203
+ try {
204
+ // Clean up mesh connections
205
+ const mesh = holoInstance.gun.back('opt.mesh');
206
+ if (mesh) {
207
+ // Clean up mesh.hear
208
+ if (mesh.hear) {
209
+ try {
210
+ // Safely clear mesh.hear without modifying function properties
211
+ const hearKeys = Object.keys(mesh.hear);
212
+ for (const key of hearKeys) {
213
+ // Check if it's an array before trying to clear it
214
+ if (Array.isArray(mesh.hear[key])) {
215
+ mesh.hear[key] = [];
216
+ }
217
+ }
218
+
219
+ // Create a new empty object for mesh.hear
220
+ // Only if mesh.hear is not a function
221
+ if (typeof mesh.hear !== 'function') {
222
+ mesh.hear = {};
223
+ }
224
+ } catch (meshError) {
225
+ console.warn('Error cleaning up Gun mesh hear:', meshError);
226
+ }
227
+ }
228
+
229
+ // Close any open sockets in the mesh
230
+ if (mesh.way) {
231
+ try {
232
+ Object.values(mesh.way).forEach(connection => {
233
+ if (connection && connection.wire && connection.wire.close) {
234
+ connection.wire.close();
235
+ }
236
+ });
237
+ } catch (sockError) {
238
+ console.warn('Error closing mesh sockets:', sockError);
239
+ }
240
+ }
241
+
242
+ // Clear the peers list
243
+ if (mesh.opt && mesh.opt.peers) {
244
+ mesh.opt.peers = {};
245
+ }
246
+ }
247
+
248
+ // Attempt to clean up any TCP connections
249
+ if (holoInstance.gun.back('opt.web')) {
250
+ try {
251
+ const server = holoInstance.gun.back('opt.web');
252
+ if (server && server.close) {
253
+ server.close();
254
+ }
255
+ } catch (webError) {
256
+ console.warn('Error closing web server:', webError);
257
+ }
258
+ }
259
+ } catch (error) {
260
+ console.warn('Error accessing Gun mesh:', error);
261
+ }
262
+ }
263
+
264
+ // Clear all Gun instance listeners
265
+ try {
266
+ holoInstance.gun.off();
267
+ } catch (error) {
268
+ console.warn('Error turning off Gun listeners:', error);
269
+ }
270
+
271
+ // Wait a moment for cleanup to complete
272
+ await new Promise(resolve => setTimeout(resolve, 100));
273
+ }
274
+
275
+ console.log('HoloSphere instance closed successfully');
276
+ } catch (error) {
277
+ console.error('Error closing HoloSphere instance:', error);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Creates a namespaced username for Gun authentication
283
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
284
+ * @param {string} holonId - The holon ID
285
+ * @returns {string} - Namespaced username
286
+ */
287
+ export function userName(holoInstance, holonId) {
288
+ if (!holonId) return null;
289
+ return `${holoInstance.appname}:${holonId}`;
290
+ }