holosphere 1.3.0-alpha4 → 1.3.0-alpha7

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
@@ -15,11 +15,39 @@ interface PutOptions {
15
15
  disableHologramRedirection?: boolean;
16
16
  actingAs?: string;
17
17
  password?: string | null;
18
+ /**
19
+ * Per-put deadline (ms) for Gun's ack callback. When the deadline fires,
20
+ * the returned promise resolves with `{ success: true, queued: true, ... }`
21
+ * so an offline/partitioned mesh can't hang the caller — Gun keeps the
22
+ * write locally and replays it on reconnect. Default 5000. Pass `0` to
23
+ * disable and wait for ack indefinitely.
24
+ */
25
+ timeout?: number;
26
+ }
27
+
28
+ interface PutGlobalOptions {
29
+ /**
30
+ * Per-put deadline (ms) for Gun's ack callback. The promise still
31
+ * resolves to `undefined` (its public contract); on timeout a warning
32
+ * is logged. Default 5000. Pass `0` to disable.
33
+ */
34
+ timeout?: number;
18
35
  }
19
36
 
20
37
  interface GetOptions {
21
38
  resolveHolograms?: boolean;
22
39
  validationOptions?: object;
40
+ /** Per-`.once()` deadline in ms; cold paths resolve `null` after this. Default 8000. Pass `0` to disable. */
41
+ timeout?: number;
42
+ /** Return `_deleted: true` soft-tombstoned records instead of treating them as not-found. Default false. */
43
+ includeDeleted?: boolean;
44
+ }
45
+
46
+ interface GetAllOptions {
47
+ /** Per-`.once()` deadline in ms; cold paths resolve `[]` after this. Default 8000. Pass `0` to disable. */
48
+ timeout?: number;
49
+ /** Include `_deleted: true` soft-tombstoned records in the response. Default false. */
50
+ includeDeleted?: boolean;
23
51
  }
24
52
 
25
53
  interface ResolveHologramOptions {
@@ -42,22 +70,69 @@ interface Hologram {
42
70
  *
43
71
  * On success: `isHologram === true` and source* fields point at the origin.
44
72
  * On failure: `isHologram === false` and `error` describes why resolution failed.
73
+ *
74
+ * **Exported.** Domain types in consumers should declare
75
+ * `_hologram?: ResolvedHologramMeta` instead of inlining the shape.
45
76
  */
46
- interface ResolvedHologramMeta {
77
+ export interface ResolvedHologramMeta {
47
78
  isHologram: boolean;
48
79
  soul: string;
49
80
  sourceHolon?: string | null;
50
81
  sourceLens?: string | null;
51
82
  sourceKey?: string | null;
83
+ /** Display name of the source holon — stamped by `resolveHologram` when known. */
84
+ sourceHolonName?: string;
52
85
  resolvedAt: number;
53
86
  error?: string;
54
87
  }
55
88
 
56
- interface ResolvedHologramData {
89
+ export interface ResolvedHologramData {
57
90
  _hologram: ResolvedHologramMeta;
58
91
  [key: string]: any;
59
92
  }
60
93
 
94
+ /**
95
+ * Federation provenance envelope stamped on records that arrived via a
96
+ * federated partner. Set by `getFederated` and by `propagate` when writing
97
+ * to outbound partners.
98
+ *
99
+ * **Exported.** Domain types should declare `_federation?: FederationMeta`
100
+ * instead of inlining the shape.
101
+ */
102
+ export interface FederationMeta {
103
+ /** The holon the record was propagated FROM. */
104
+ origin: string;
105
+ /** The lens it was published under at the origin. */
106
+ sourceLens: string;
107
+ /** Origin holon's display name (best-effort; absent if the source has no name). */
108
+ originName?: string;
109
+ /** Source-side id of the record, useful when local-side keys differ. */
110
+ originalId?: string;
111
+ /** Wall-clock ms when the propagation was emitted. */
112
+ propagatedAt?: number;
113
+ }
114
+
115
+ /**
116
+ * Soft-tombstone marker recognised by `get`/`getAll`. A record with
117
+ * `_deleted: true` is treated as not-found in the default response and
118
+ * surfaced only when `{ includeDeleted: true }` is passed.
119
+ *
120
+ * **Exported.** Domain types that want to allow tombstoned records on the
121
+ * wire should declare `_deleted?: boolean`.
122
+ */
123
+ export type DeletedMarker = boolean;
124
+
125
+ /**
126
+ * Convenience mixin: the three envelope fields the library stamps onto
127
+ * records. Domain types can extend or intersect with this to avoid
128
+ * redeclaring the shapes.
129
+ */
130
+ export interface HolosphereEnvelope {
131
+ _hologram?: ResolvedHologramMeta;
132
+ _federation?: FederationMeta;
133
+ _deleted?: DeletedMarker;
134
+ }
135
+
61
136
  /**
62
137
  * Per-partner directional lens config. Directions are from the holding
63
138
  * space's perspective: `inbound` lenses are received from the partner,
@@ -123,6 +198,16 @@ interface PutResult {
123
198
  pathKey: string;
124
199
  propagationResult?: PropagationResult | null;
125
200
  error?: string;
201
+ /**
202
+ * `true` when the ack deadline fired before Gun confirmed the write
203
+ * (offline/partitioned mesh). The write is still committed locally
204
+ * via radisk and Gun replays it whenever a peer reappears; subscriber
205
+ * notification, hologram cascade, and federation propagation run at
206
+ * that point. Absent or `false` when the put was acknowledged.
207
+ */
208
+ queued?: boolean;
209
+ /** Holograms whose `updated` timestamp was bumped by this put (empty when the put timed out). */
210
+ updatedHolograms?: Array<{ soul: string; holon: string; lens: string; key: string }>;
126
211
  }
127
212
 
128
213
  interface CanWriteResult {
@@ -168,7 +253,7 @@ declare class HoloSphere {
168
253
  put(holon: string, lens: string, data: object, password?: string | null, options?: PutOptions): Promise<PutResult>;
169
254
  get(holon: string, lens: string): Promise<any | null>;
170
255
  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>>;
256
+ getAll(holon: string, lens: string, password?: string | null, options?: GetAllOptions): Promise<Array<any>>;
172
257
  parse(rawData: any): Promise<object | null>;
173
258
  delete(holon: string, lens: string, key: string, password?: string | null): Promise<boolean>;
174
259
  deleteAll(holon: string, lens: string, password?: string | null): Promise<boolean>;
@@ -181,14 +266,14 @@ declare class HoloSphere {
181
266
  deleteNode(holon: string, lens: string, key: string): Promise<boolean>;
182
267
 
183
268
  // Global
184
- putGlobal(tableName: string, data: object, password?: string | null): Promise<void>;
185
- writeGlobal(tableName: string, data: object): Promise<void>;
269
+ putGlobal(tableName: string, data: object, password?: string | null, options?: PutGlobalOptions): Promise<void>;
270
+ writeGlobal(tableName: string, data: object, options?: PutGlobalOptions): Promise<void>;
186
271
  getGlobal(tableName: string, key: string, password?: string | null): Promise<any | null>;
187
272
  getAllGlobal(tableName: string, password?: string | null): Promise<Array<any>>;
188
273
  deleteGlobal(tableName: string, key: string, password?: string | null): Promise<boolean>;
189
274
  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 }>;
275
+ subscribeGlobal(lens: string, key: string | null, callback: (data: any, key?: string) => void, options?: { realtimeOnly?: boolean }): { unsubscribe: () => void };
276
+ subscribeGlobal(lens: string, callback: (data: any, key?: string) => void): { unsubscribe: () => void };
192
277
 
193
278
  // Hologram
194
279
  createHologram(holon: string, lens: string, data: { id: string, [key: string]: any }): Hologram;
@@ -210,8 +295,10 @@ declare class HoloSphere {
210
295
  getScalespace(lat: number, lng: number): string[];
211
296
  getHolonScalespace(holon: string): string[];
212
297
 
213
- // Subscription
214
- subscribe(holon: string, lens: string, callback: (data: any, key?: string) => void): Promise<{ unsubscribe: () => void }>;
298
+ // Subscription. Returns synchronously — `await` on the return value
299
+ // still works (await on a non-Promise resolves to the value), so both
300
+ // call styles produce the same `{ unsubscribe }` shape.
301
+ subscribe(holon: string, lens: string, callback: (data: any, key?: string) => void): { unsubscribe: () => void };
215
302
 
216
303
  // Federation - v1 style
217
304
  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) {
@@ -232,15 +236,15 @@ class HoloSphere {
232
236
 
233
237
  // ================================ GLOBAL FUNCTIONS ================================
234
238
 
235
- async putGlobal(tableName, data, password = null) {
236
- return GlobalOps.putGlobal(this, tableName, data, password);
239
+ async putGlobal(tableName, data, password = null, options = {}) {
240
+ return GlobalOps.putGlobal(this, tableName, data, password, options);
237
241
  }
238
242
 
239
243
  /**
240
244
  * v2-compatible alias for putGlobal (no password param)
241
245
  */
242
- async writeGlobal(tableName, data) {
243
- return GlobalOps.putGlobal(this, tableName, data, null);
246
+ async writeGlobal(tableName, data, options = {}) {
247
+ return GlobalOps.putGlobal(this, tableName, data, null, options);
244
248
  }
245
249
 
246
250
  async getGlobal(tableName, key, password = null) {
@@ -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-alpha4",
3
+ "version": "1.3.0-alpha7",
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];