holosphere 1.3.0-alpha3 → 1.3.0-alpha5

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.d.ts CHANGED
@@ -20,6 +20,17 @@ interface PutOptions {
20
20
  interface GetOptions {
21
21
  resolveHolograms?: boolean;
22
22
  validationOptions?: object;
23
+ /** Per-`.once()` deadline in ms; cold paths resolve `null` after this. Default 8000. Pass `0` to disable. */
24
+ timeout?: number;
25
+ /** Return `_deleted: true` soft-tombstoned records instead of treating them as not-found. Default false. */
26
+ includeDeleted?: boolean;
27
+ }
28
+
29
+ interface GetAllOptions {
30
+ /** Per-`.once()` deadline in ms; cold paths resolve `[]` after this. Default 8000. Pass `0` to disable. */
31
+ timeout?: number;
32
+ /** Include `_deleted: true` soft-tombstoned records in the response. Default false. */
33
+ includeDeleted?: boolean;
23
34
  }
24
35
 
25
36
  interface ResolveHologramOptions {
@@ -42,22 +53,69 @@ interface Hologram {
42
53
  *
43
54
  * On success: `isHologram === true` and source* fields point at the origin.
44
55
  * On failure: `isHologram === false` and `error` describes why resolution failed.
56
+ *
57
+ * **Exported.** Domain types in consumers should declare
58
+ * `_hologram?: ResolvedHologramMeta` instead of inlining the shape.
45
59
  */
46
- interface ResolvedHologramMeta {
60
+ export interface ResolvedHologramMeta {
47
61
  isHologram: boolean;
48
62
  soul: string;
49
63
  sourceHolon?: string | null;
50
64
  sourceLens?: string | null;
51
65
  sourceKey?: string | null;
66
+ /** Display name of the source holon — stamped by `resolveHologram` when known. */
67
+ sourceHolonName?: string;
52
68
  resolvedAt: number;
53
69
  error?: string;
54
70
  }
55
71
 
56
- interface ResolvedHologramData {
72
+ export interface ResolvedHologramData {
57
73
  _hologram: ResolvedHologramMeta;
58
74
  [key: string]: any;
59
75
  }
60
76
 
77
+ /**
78
+ * Federation provenance envelope stamped on records that arrived via a
79
+ * federated partner. Set by `getFederated` and by `propagate` when writing
80
+ * to outbound partners.
81
+ *
82
+ * **Exported.** Domain types should declare `_federation?: FederationMeta`
83
+ * instead of inlining the shape.
84
+ */
85
+ export interface FederationMeta {
86
+ /** The holon the record was propagated FROM. */
87
+ origin: string;
88
+ /** The lens it was published under at the origin. */
89
+ sourceLens: string;
90
+ /** Origin holon's display name (best-effort; absent if the source has no name). */
91
+ originName?: string;
92
+ /** Source-side id of the record, useful when local-side keys differ. */
93
+ originalId?: string;
94
+ /** Wall-clock ms when the propagation was emitted. */
95
+ propagatedAt?: number;
96
+ }
97
+
98
+ /**
99
+ * Soft-tombstone marker recognised by `get`/`getAll`. A record with
100
+ * `_deleted: true` is treated as not-found in the default response and
101
+ * surfaced only when `{ includeDeleted: true }` is passed.
102
+ *
103
+ * **Exported.** Domain types that want to allow tombstoned records on the
104
+ * wire should declare `_deleted?: boolean`.
105
+ */
106
+ export type DeletedMarker = boolean;
107
+
108
+ /**
109
+ * Convenience mixin: the three envelope fields the library stamps onto
110
+ * records. Domain types can extend or intersect with this to avoid
111
+ * redeclaring the shapes.
112
+ */
113
+ export interface HolosphereEnvelope {
114
+ _hologram?: ResolvedHologramMeta;
115
+ _federation?: FederationMeta;
116
+ _deleted?: DeletedMarker;
117
+ }
118
+
61
119
  /**
62
120
  * Per-partner directional lens config. Directions are from the holding
63
121
  * space's perspective: `inbound` lenses are received from the partner,
@@ -168,7 +226,7 @@ declare class HoloSphere {
168
226
  put(holon: string, lens: string, data: object, password?: string | null, options?: PutOptions): Promise<PutResult>;
169
227
  get(holon: string, lens: string): Promise<any | null>;
170
228
  get(holon: string, lens: string, key: string, password?: string | null, options?: GetOptions): Promise<any | null>;
171
- getAll(holon: string, lens: string, password?: string | null): Promise<Array<any>>;
229
+ getAll(holon: string, lens: string, password?: string | null, options?: GetAllOptions): Promise<Array<any>>;
172
230
  parse(rawData: any): Promise<object | null>;
173
231
  delete(holon: string, lens: string, key: string, password?: string | null): Promise<boolean>;
174
232
  deleteAll(holon: string, lens: string, password?: string | null): Promise<boolean>;
@@ -187,8 +245,8 @@ declare class HoloSphere {
187
245
  getAllGlobal(tableName: string, password?: string | null): Promise<Array<any>>;
188
246
  deleteGlobal(tableName: string, key: string, password?: string | null): Promise<boolean>;
189
247
  deleteAllGlobal(tableName: string, password?: string | null): Promise<boolean>;
190
- subscribeGlobal(lens: string, key: string | null, callback: (data: any, key?: string) => void, options?: { realtimeOnly?: boolean }): Promise<{ unsubscribe: () => void }>;
191
- subscribeGlobal(lens: string, callback: (data: any, key?: string) => void): Promise<{ unsubscribe: () => void }>;
248
+ subscribeGlobal(lens: string, key: string | null, callback: (data: any, key?: string) => void, options?: { realtimeOnly?: boolean }): { unsubscribe: () => void };
249
+ subscribeGlobal(lens: string, callback: (data: any, key?: string) => void): { unsubscribe: () => void };
192
250
 
193
251
  // Hologram
194
252
  createHologram(holon: string, lens: string, data: { id: string, [key: string]: any }): Hologram;
@@ -210,8 +268,10 @@ declare class HoloSphere {
210
268
  getScalespace(lat: number, lng: number): string[];
211
269
  getHolonScalespace(holon: string): string[];
212
270
 
213
- // Subscription
214
- subscribe(holon: string, lens: string, callback: (data: any, key?: string) => void): Promise<{ unsubscribe: () => void }>;
271
+ // Subscription. Returns synchronously — `await` on the return value
272
+ // still works (await on a non-Promise resolves to the value), so both
273
+ // call styles produce the same `{ unsubscribe }` shape.
274
+ subscribe(holon: string, lens: string, callback: (data: any, key?: string) => void): { unsubscribe: () => void };
215
275
 
216
276
  // Federation - v1 style
217
277
  federate(holonId1: string, holonId2: string, password1?: string | null, password2?: string | null, bidirectional?: boolean, lensConfig?: { inbound?: string[], outbound?: string[] }): Promise<boolean>;
package/holosphere.js CHANGED
@@ -125,6 +125,10 @@ class HoloSphere {
125
125
  // Initialize schema cache
126
126
  this.schemaCache = new Map();
127
127
 
128
+ // Holon-name cache so resolveHologram + getFederated don't refetch
129
+ // `settings/<holon>` for every hologram from the same source.
130
+ this._holonNameCache = new Map();
131
+
128
132
  // Initialize allowed authors set (for canWrite)
129
133
  this._allowedAuthors = new Set();
130
134
  }
@@ -192,8 +196,8 @@ class HoloSphere {
192
196
  return ContentOps.get(this, holon, lens, key, password, options);
193
197
  }
194
198
 
195
- async getAll(holon, lens, password = null) {
196
- return ContentOps.getAll(this, holon, lens, password);
199
+ async getAll(holon, lens, password = null, options = {}) {
200
+ return ContentOps.getAll(this, holon, lens, password, options);
197
201
  }
198
202
 
199
203
  async parse(rawData) {
@@ -262,8 +266,10 @@ class HoloSphere {
262
266
  /**
263
267
  * Subscribe to real-time changes in a global table.
264
268
  * v2-compatible: subscribeGlobal(lens, key, callback, options)
269
+ *
270
+ * Returns synchronously — see {@link subscribe}.
265
271
  */
266
- async subscribeGlobal(lens, keyOrCallback, callbackOrOptions, options = {}) {
272
+ subscribeGlobal(lens, keyOrCallback, callbackOrOptions, options = {}) {
267
273
  let key, callback;
268
274
  if (typeof keyOrCallback === 'function') {
269
275
  callback = keyOrCallback;
@@ -336,7 +342,14 @@ class HoloSphere {
336
342
  return Utils.getHolonScalespace(holon);
337
343
  }
338
344
 
339
- async subscribe(holon, lens, callback) {
345
+ /**
346
+ * Subscribe to real-time changes for a holon/lens.
347
+ *
348
+ * Synchronous return: `{ unsubscribe: () => void }`. Callers do not
349
+ * need to `await` — both `const s = holosphere.subscribe(...)` and
350
+ * `const s = await holosphere.subscribe(...)` yield the same shape.
351
+ */
352
+ subscribe(holon, lens, callback) {
340
353
  return Utils.subscribe(this, holon, lens, callback);
341
354
  }
342
355
 
@@ -344,6 +357,38 @@ class HoloSphere {
344
357
  return Utils.notifySubscribers(this, data);
345
358
  }
346
359
 
360
+ /**
361
+ * Resolve a holon's display name from its `settings/<holon>` record.
362
+ * Cached per-instance so repeated lookups (e.g. one per hologram in a
363
+ * federated batch) only fetch once. Returns `null` when no name is set.
364
+ *
365
+ * Used internally by `resolveHologram` to stamp
366
+ * `_hologram.sourceHolonName` so every consumer of a resolved hologram
367
+ * already has the display name without a second round-trip.
368
+ */
369
+ async getHolonName(holonId) {
370
+ if (!holonId) return null;
371
+ const key = String(holonId);
372
+ if (this._holonNameCache.has(key)) return this._holonNameCache.get(key);
373
+ try {
374
+ const settings = await this.get(key, 'settings', key);
375
+ let name = null;
376
+ if (settings) {
377
+ if (Array.isArray(settings)) {
378
+ const found = settings.find(s => s && typeof s.name === 'string' && s.name.trim() !== '');
379
+ name = found ? found.name : null;
380
+ } else if (typeof settings.name === 'string' && settings.name.trim() !== '') {
381
+ name = settings.name;
382
+ }
383
+ }
384
+ this._holonNameCache.set(key, name);
385
+ return name;
386
+ } catch {
387
+ this._holonNameCache.set(key, null);
388
+ return null;
389
+ }
390
+ }
391
+
347
392
  generateId() {
348
393
  return Utils.generateId();
349
394
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holosphere",
3
- "version": "1.3.0-alpha3",
3
+ "version": "1.3.0-alpha5",
4
4
  "description": "Holonic Geospatial Communication Infrastructure",
5
5
  "main": "holosphere.js",
6
6
  "module": "holosphere.js",
package/utils.js CHANGED
@@ -44,13 +44,21 @@ export function getHolonScalespace(holon) { // Doesn't need holoInstance
44
44
 
45
45
  /**
46
46
  * Subscribes to changes in a specific holon and lens.
47
+ *
48
+ * Returns **synchronously** — the call has no internal awaits, so callers
49
+ * never need to `await` the result. (`await` on the return value still
50
+ * works as before; it just resolves through the value unchanged.) This
51
+ * means `const sub = holosphere.subscribe(...); sub.unsubscribe();` is the
52
+ * canonical pattern and there is no `Promise<{ unsubscribe }>` shape to
53
+ * disambiguate against the resolved object.
54
+ *
47
55
  * @param {HoloSphere} holoInstance - The HoloSphere instance.
48
56
  * @param {string} holon - The holon identifier.
49
57
  * @param {string} lens - The lens to subscribe to.
50
58
  * @param {function} callback - The callback to execute on changes.
51
- * @returns {Promise<object>} - Subscription object with unsubscribe method
59
+ * @returns {{ unsubscribe: () => void }} - Subscription with unsubscribe method.
52
60
  */
53
- export async function subscribe(holoInstance, holon, lens, callback) {
61
+ export function subscribe(holoInstance, holon, lens, callback) {
54
62
  if (!holon || !lens) {
55
63
  throw new Error('subscribe: Missing holon or lens parameters:', holon, lens);
56
64
  }
@@ -82,6 +90,17 @@ export async function subscribe(holoInstance, holon, lens, callback) {
82
90
  }
83
91
  }
84
92
 
93
+ // Subscribers expect `object | null`. `parse()` can return
94
+ // a string/number/boolean when a legacy or corrupted leaf
95
+ // happened to be a JSON-encoded primitive (e.g.
96
+ // `'"hello"'` parses to `'hello'`). Drop those so every
97
+ // consumer can stop guarding with
98
+ // `typeof x === 'string' ? JSON.parse(x) : x`.
99
+ if (parsed !== null && (typeof parsed !== 'object' || Array.isArray(parsed))) {
100
+ console.warn(`[holosphere.subscribe] dropping non-object payload at ${holon}/${lens}/${key}:`, typeof parsed);
101
+ return;
102
+ }
103
+
85
104
  // Check again if subscription ID still exists before calling callback
86
105
  if (holoInstance.subscriptions[subscriptionId]) {
87
106
  callback(parsed, key);
@@ -102,9 +121,14 @@ export async function subscribe(holoInstance, holon, lens, callback) {
102
121
  gunListener: gunListener // Store the listener too (optional, maybe needed for close?)
103
122
  };
104
123
 
105
- // Return an object with unsubscribe method
124
+ // Return an object with unsubscribe method.
125
+ // `unsubscribe` is sync — nothing it does (`mapChain.off()`,
126
+ // `delete subscriptions[id]`) blocks. Keeping it sync means the
127
+ // returned shape is `{ unsubscribe: () => void }` rather than
128
+ // `() => Promise<void>`, matching what callers expect when they
129
+ // store it in a `() => void` cleanup slot.
106
130
  return {
107
- unsubscribe: async () => {
131
+ unsubscribe: () => {
108
132
  const sub = holoInstance.subscriptions[subscriptionId];
109
133
  if (!sub) {
110
134
  return;
@@ -114,9 +138,7 @@ export async function subscribe(holoInstance, holon, lens, callback) {
114
138
  // Turn off the Gun subscription using the stored mapChain reference
115
139
  if (sub.mapChain) { // Check if mapChain exists
116
140
  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
141
+ }
120
142
 
121
143
  // Remove from subscriptions object AFTER turning off listener
122
144
  delete holoInstance.subscriptions[subscriptionId];