holosphere 2.0.0-alpha2 → 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 (98) 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 +1 -1
  14. package/dist/esm/holosphere.js +50 -53
  15. package/dist/index-BB_vVJgv.cjs +5 -0
  16. package/dist/index-BB_vVJgv.cjs.map +1 -0
  17. package/dist/index-CBitK71M.cjs +12 -0
  18. package/dist/index-CBitK71M.cjs.map +1 -0
  19. package/dist/index-CV0eOogK.js +37423 -0
  20. package/dist/index-CV0eOogK.js.map +1 -0
  21. package/dist/index-Cz-PLCUR.js +15104 -0
  22. package/dist/index-Cz-PLCUR.js.map +1 -0
  23. package/dist/indexeddb-storage-CRsZyB2f.cjs +2 -0
  24. package/dist/indexeddb-storage-CRsZyB2f.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-CMW4qRQS.js → indexeddb-storage-DZaGlY_a.js} +49 -13
  26. package/dist/indexeddb-storage-DZaGlY_a.js.map +1 -0
  27. package/dist/{memory-storage-DQzcAZlf.js → memory-storage-BkUi6sZG.js} +6 -2
  28. package/dist/memory-storage-BkUi6sZG.js.map +1 -0
  29. package/dist/{memory-storage-DmePEP2q.cjs → memory-storage-C0DuUsdY.cjs} +2 -2
  30. package/dist/memory-storage-C0DuUsdY.cjs.map +1 -0
  31. package/dist/secp256k1-0kPdAVkK.cjs +12 -0
  32. package/dist/secp256k1-0kPdAVkK.cjs.map +1 -0
  33. package/dist/{secp256k1-vOXp40Fx.js → secp256k1-DN4FVXcv.js} +2 -393
  34. package/dist/secp256k1-DN4FVXcv.js.map +1 -0
  35. package/docs/CONTRACTS.md +797 -0
  36. package/examples/demo.html +47 -0
  37. package/package.json +10 -5
  38. package/src/contracts/abis/Appreciative.json +1280 -0
  39. package/src/contracts/abis/AppreciativeFactory.json +101 -0
  40. package/src/contracts/abis/Bundle.json +1435 -0
  41. package/src/contracts/abis/BundleFactory.json +106 -0
  42. package/src/contracts/abis/Holon.json +881 -0
  43. package/src/contracts/abis/Holons.json +330 -0
  44. package/src/contracts/abis/Managed.json +1262 -0
  45. package/src/contracts/abis/ManagedFactory.json +149 -0
  46. package/src/contracts/abis/Membrane.json +261 -0
  47. package/src/contracts/abis/Splitter.json +1624 -0
  48. package/src/contracts/abis/SplitterFactory.json +220 -0
  49. package/src/contracts/abis/TestToken.json +321 -0
  50. package/src/contracts/abis/Zoned.json +1461 -0
  51. package/src/contracts/abis/ZonedFactory.json +154 -0
  52. package/src/contracts/chain-manager.js +375 -0
  53. package/src/contracts/deployer.js +443 -0
  54. package/src/contracts/event-listener.js +507 -0
  55. package/src/contracts/holon-contracts.js +344 -0
  56. package/src/contracts/index.js +83 -0
  57. package/src/contracts/networks.js +224 -0
  58. package/src/contracts/operations.js +670 -0
  59. package/src/contracts/queries.js +589 -0
  60. package/src/core/holosphere.js +453 -1
  61. package/src/crypto/nostr-utils.js +263 -0
  62. package/src/federation/handshake.js +455 -0
  63. package/src/federation/hologram.js +1 -1
  64. package/src/hierarchical/upcast.js +6 -5
  65. package/src/index.js +463 -1939
  66. package/src/lib/ai-methods.js +308 -0
  67. package/src/lib/contract-methods.js +293 -0
  68. package/src/lib/errors.js +23 -0
  69. package/src/lib/federation-methods.js +238 -0
  70. package/src/lib/index.js +26 -0
  71. package/src/spatial/h3-operations.js +2 -2
  72. package/src/storage/backends/gundb-backend.js +377 -46
  73. package/src/storage/global-tables.js +28 -1
  74. package/src/storage/gun-auth.js +303 -0
  75. package/src/storage/gun-federation.js +776 -0
  76. package/src/storage/gun-references.js +198 -0
  77. package/src/storage/gun-schema.js +291 -0
  78. package/src/storage/gun-wrapper.js +347 -31
  79. package/src/storage/indexeddb-storage.js +49 -11
  80. package/src/storage/memory-storage.js +5 -0
  81. package/src/storage/nostr-async.js +45 -23
  82. package/src/storage/nostr-client.js +11 -5
  83. package/src/storage/persistent-storage.js +6 -1
  84. package/src/storage/unified-storage.js +119 -0
  85. package/src/subscriptions/manager.js +1 -1
  86. package/types/index.d.ts +133 -0
  87. package/dist/index-CDfIuXew.js +0 -15974
  88. package/dist/index-CDfIuXew.js.map +0 -1
  89. package/dist/index-ifOgtDvd.cjs +0 -3
  90. package/dist/index-ifOgtDvd.cjs.map +0 -1
  91. package/dist/indexeddb-storage-CMW4qRQS.js.map +0 -1
  92. package/dist/indexeddb-storage-DLZOgetM.cjs +0 -2
  93. package/dist/indexeddb-storage-DLZOgetM.cjs.map +0 -1
  94. package/dist/memory-storage-DQzcAZlf.js.map +0 -1
  95. package/dist/memory-storage-DmePEP2q.cjs.map +0 -1
  96. package/dist/secp256k1-CP0ZkpAx.cjs +0 -13
  97. package/dist/secp256k1-CP0ZkpAx.cjs.map +0 -1
  98. package/dist/secp256k1-vOXp40Fx.js.map +0 -1
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * GunDB Storage Wrapper with radisk persistence
3
3
  * Handles path construction and CRUD operations
4
+ * Note: GunDB doesn't handle nested objects well, so we store data as JSON strings
4
5
  */
5
6
 
6
7
  import { gunPromise, gunPut, gunCollect } from './gun-async.js';
7
8
 
9
+ // ============================================================================
10
+ // PATH BUILDERS
11
+ // ============================================================================
12
+
8
13
  /**
9
14
  * Build Gun path from components
10
15
  * @param {string} appname - Application namespace
@@ -25,6 +30,22 @@ export function buildPath(appname, holon, lens, key = null) {
25
30
  return `${appname}/${encodedHolon}/${encodedLens}`;
26
31
  }
27
32
 
33
+ /**
34
+ * Build Gun path for global tables (app-wide data not tied to holons)
35
+ * @param {string} appname - Application namespace
36
+ * @param {string} tableName - Global table name (e.g., 'schemas', 'federation')
37
+ * @param {string} key - Data key (optional)
38
+ * @returns {string} Gun path
39
+ */
40
+ export function buildGlobalPath(appname, tableName, key = null) {
41
+ const encodedTable = encodePathComponent(tableName);
42
+ if (key) {
43
+ const encodedKey = encodePathComponent(key);
44
+ return `${appname}/${encodedTable}/${encodedKey}`;
45
+ }
46
+ return `${appname}/${encodedTable}`;
47
+ }
48
+
28
49
  /**
29
50
  * Encode path component to handle special characters
30
51
  * @private
@@ -33,6 +54,94 @@ function encodePathComponent(component) {
33
54
  return encodeURIComponent(component).replace(/%2F/g, '/');
34
55
  }
35
56
 
57
+ /**
58
+ * Serialize data for GunDB storage
59
+ * Wraps data in { _json: string } format for Gun compatibility
60
+ * Gun requires an object at graph roots, so we can't store raw JSON strings
61
+ * The deserialization handles both this format and raw JSON strings for reading old data
62
+ * @private
63
+ */
64
+ function serializeForGun(data) {
65
+ return { _json: JSON.stringify(data) };
66
+ }
67
+
68
+ /**
69
+ * Deserialize data from GunDB storage
70
+ * Handles multiple formats:
71
+ * - Direct JSON string (holosphere original - now default)
72
+ * - _json wrapped format (holosphere2 legacy)
73
+ * - Gun internal references (_["#"])
74
+ * - Gun node data (_[">"])
75
+ * - Plain objects
76
+ * @private
77
+ */
78
+ function deserializeFromGun(data) {
79
+ if (!data) {
80
+ return null;
81
+ }
82
+
83
+ try {
84
+ // Format 1: String data (holosphere original stores JSON as string directly)
85
+ if (typeof data === 'string') {
86
+ try {
87
+ return JSON.parse(data);
88
+ } catch (e) {
89
+ // Not JSON, return as-is
90
+ return data;
91
+ }
92
+ }
93
+
94
+ // Format 2: _json wrapped format (holosphere2 default)
95
+ if (data._json && typeof data._json === 'string') {
96
+ try {
97
+ return JSON.parse(data._json);
98
+ } catch (e) {
99
+ console.warn('Failed to parse _json field:', e);
100
+ return null;
101
+ }
102
+ }
103
+
104
+ // Format 3: Gun internal reference (_["#"]) - this indicates a Gun reference
105
+ // The actual data retrieval should happen via resolveReference, we just identify it here
106
+ if (data._ && data._['#']) {
107
+ // This is a Gun reference - return as-is for reference resolution
108
+ // The caller should check isReference() and handle appropriately
109
+ return data;
110
+ }
111
+
112
+ // Format 4: Gun node data with timestamps (_[">"])
113
+ // Gun stores metadata in _.> - find the actual value
114
+ if (data._ && data._['>']) {
115
+ const nodeValue = Object.entries(data).find(([k, v]) => k !== '_' && typeof v === 'string');
116
+ if (nodeValue) {
117
+ try {
118
+ return JSON.parse(nodeValue[1]);
119
+ } catch (e) {
120
+ return nodeValue[1];
121
+ }
122
+ }
123
+ }
124
+
125
+ // Format 5: Plain object - clean Gun metadata and return
126
+ if (typeof data === 'object' && data !== null) {
127
+ const cleaned = { ...data };
128
+ delete cleaned['_'];
129
+
130
+ // Check if any remaining keys - if empty after removing _, return null
131
+ if (Object.keys(cleaned).length === 0) {
132
+ return null;
133
+ }
134
+
135
+ return cleaned;
136
+ }
137
+
138
+ return data;
139
+ } catch (error) {
140
+ console.warn('Error deserializing Gun data:', error);
141
+ return data; // Return raw data as fallback
142
+ }
143
+ }
144
+
36
145
  /**
37
146
  * Write data to Gun with radisk persistence
38
147
  * @param {Object} gun - Gun instance
@@ -42,7 +151,8 @@ function encodePathComponent(component) {
42
151
  */
43
152
  export async function write(gun, path, data) {
44
153
  try {
45
- await gunPut(gun.get(path), data, 2000);
154
+ const serialized = serializeForGun(data);
155
+ await gunPut(gun.get(path), serialized, 2000);
46
156
  // Delay to allow Gun to propagate the write (50ms for better reliability)
47
157
  await new Promise(resolve => setTimeout(resolve, 50));
48
158
  return true;
@@ -58,7 +168,13 @@ export async function write(gun, path, data) {
58
168
  * @returns {Promise<Object|null>} Data or null if not found
59
169
  */
60
170
  export async function read(gun, path) {
61
- const data = await gunPromise(gun.get(path), 2000);
171
+ const rawData = await gunPromise(gun.get(path), 2000);
172
+
173
+ if (!rawData) {
174
+ return null;
175
+ }
176
+
177
+ const data = deserializeFromGun(rawData);
62
178
 
63
179
  // Return null if deleted or not found
64
180
  if (!data || data._deleted) {
@@ -70,22 +186,114 @@ export async function read(gun, path) {
70
186
 
71
187
  /**
72
188
  * Read all data under a path (lens query)
189
+ * Uses Gun's .open() which waits for nested data to load
190
+ * Falls back to .once() with iteration
73
191
  * @param {Object} gun - Gun instance
74
192
  * @param {string} path - Gun path
75
193
  * @returns {Promise<Object[]>} Array of data objects
76
194
  */
77
195
  export async function readAll(gun, path) {
78
- const results = await gunCollect(gun.get(path), 1000);
196
+ return new Promise((resolve) => {
197
+ const output = new Map();
198
+ let settled = false;
79
199
 
80
- // Filter out deleted items and Gun metadata
81
- return results
82
- .filter(({ data, key }) => {
83
- if (!data || typeof data !== 'object') return false;
84
- if (key.startsWith('_')) return false;
85
- if (data._deleted) return false;
86
- return true;
87
- })
88
- .map(({ data }) => data);
200
+ const ref = gun.get(path);
201
+
202
+ // Use .once() on parent to get all keys, then process
203
+ ref.once((parentData) => {
204
+ if (settled) return;
205
+
206
+ if (!parentData) {
207
+ settled = true;
208
+ resolve([]);
209
+ return;
210
+ }
211
+
212
+ // Get all keys except Gun metadata
213
+ const keys = Object.keys(parentData).filter(k => k !== '_');
214
+
215
+ if (keys.length === 0) {
216
+ settled = true;
217
+ resolve([]);
218
+ return;
219
+ }
220
+
221
+ // Process inline data directly from parentData
222
+ // Gun often includes the data inline as references or direct values
223
+ for (const key of keys) {
224
+ const rawItem = parentData[key];
225
+ if (!rawItem) continue;
226
+
227
+ let item = null;
228
+
229
+ // Check if it's a Gun reference (soul)
230
+ if (typeof rawItem === 'object' && rawItem['#']) {
231
+ // Skip references for now, map().once will handle them
232
+ continue;
233
+ }
234
+
235
+ // Try to parse as _json wrapper
236
+ if (rawItem._json && typeof rawItem._json === 'string') {
237
+ try {
238
+ item = JSON.parse(rawItem._json);
239
+ } catch (e) {}
240
+ }
241
+ // Try as direct JSON string
242
+ else if (typeof rawItem === 'string') {
243
+ try {
244
+ item = JSON.parse(rawItem);
245
+ } catch (e) {}
246
+ }
247
+
248
+ if (item && item.id && !item._deleted) {
249
+ output.set(item.id, item);
250
+ }
251
+ }
252
+
253
+ // If we got all data inline, resolve now
254
+ if (output.size > 0) {
255
+ settled = true;
256
+ resolve(Array.from(output.values()));
257
+ return;
258
+ }
259
+
260
+ // Otherwise use map().once() to resolve references
261
+ ref.map().once((data, key) => {
262
+ if (settled || !data || key === '_') return;
263
+
264
+ let item = null;
265
+ if (data._json && typeof data._json === 'string') {
266
+ try {
267
+ item = JSON.parse(data._json);
268
+ } catch (e) {}
269
+ } else if (typeof data === 'string') {
270
+ try {
271
+ item = JSON.parse(data);
272
+ } catch (e) {}
273
+ }
274
+
275
+ if (item && item.id && !item._deleted) {
276
+ output.set(item.id, item);
277
+ }
278
+ });
279
+
280
+ // Give map().once() time to complete
281
+ setTimeout(() => {
282
+ if (!settled) {
283
+ settled = true;
284
+ resolve(Array.from(output.values()));
285
+ }
286
+ }, 500);
287
+ });
288
+
289
+ // Overall timeout
290
+ setTimeout(() => {
291
+ if (!settled) {
292
+ settled = true;
293
+ resolve(Array.from(output.values()));
294
+ }
295
+ }, 2000);
296
+ });
89
297
  }
90
298
 
91
299
  /**
@@ -96,21 +304,24 @@ export async function readAll(gun, path) {
96
304
  * @returns {Promise<boolean>} Success indicator
97
305
  */
98
306
  export async function update(gun, path, updates) {
99
- const existing = await gunPromise(gun.get(path));
307
+ const rawData = await gunPromise(gun.get(path));
100
308
 
101
- if (!existing || !existing.id || existing._deleted) {
102
- return false; // Not found or deleted
309
+ if (!rawData) {
310
+ return false; // Not found
103
311
  }
104
312
 
105
- // Remove Gun metadata before merging
106
- const cleanExisting = { ...existing };
107
- delete cleanExisting['_'];
313
+ // Deserialize existing data
314
+ const existing = deserializeFromGun(rawData);
315
+ if (!existing || !existing.id || existing._deleted) {
316
+ return false;
317
+ }
108
318
 
109
319
  // Merge updates
110
- const merged = { ...cleanExisting, ...updates };
320
+ const merged = { ...existing, ...updates };
111
321
 
112
322
  try {
113
- await gunPut(gun.get(path), merged);
323
+ const serialized = serializeForGun(merged);
324
+ await gunPut(gun.get(path), serialized);
114
325
  return true;
115
326
  } catch (error) {
116
327
  throw error;
@@ -125,21 +336,22 @@ export async function update(gun, path, updates) {
125
336
  */
126
337
  export async function deleteData(gun, path) {
127
338
  try {
128
- // Gun requires tombstone to be an object with _deleted flag
129
- // First read existing data to preserve metadata
130
- const existing = await gunPromise(gun.get(path));
131
- if (!existing) {
339
+ // First read existing data to get the id
340
+ const rawData = await gunPromise(gun.get(path));
341
+ if (!rawData) {
132
342
  return true; // Already deleted/doesn't exist
133
343
  }
134
344
 
135
- // Create tombstone object
345
+ const existing = deserializeFromGun(rawData);
346
+
347
+ // Create tombstone object and serialize it
136
348
  const tombstone = {
137
- id: existing.id,
349
+ id: existing?.id,
138
350
  _deleted: true,
139
351
  _deletedAt: Date.now()
140
352
  };
141
353
 
142
- await gunPut(gun.get(path), tombstone);
354
+ await gunPut(gun.get(path), serializeForGun(tombstone));
143
355
  return true;
144
356
  } catch (error) {
145
357
  throw error;
@@ -186,8 +398,11 @@ export function subscribe(gun, path, callback, options = {}) {
186
398
  const ref = gun.get(path);
187
399
 
188
400
  ref.map().on((data, key) => {
189
- if (data && !key.startsWith('_') && !data._deleted) {
190
- callback(data, key);
401
+ if (data && !key.startsWith('_')) {
402
+ const deserialized = deserializeFromGun(data);
403
+ if (deserialized && !deserialized._deleted) {
404
+ callback(deserialized, key);
405
+ }
191
406
  }
192
407
  });
193
408
 
@@ -203,8 +418,11 @@ export function subscribe(gun, path, callback, options = {}) {
203
418
  } else {
204
419
  // Subscribe to single item
205
420
  const listener = gun.get(path).on((data, key) => {
206
- if (data && !data._deleted) {
207
- callback(data, key);
421
+ if (data) {
422
+ const deserialized = deserializeFromGun(data);
423
+ if (deserialized && !deserialized._deleted) {
424
+ callback(deserialized, key);
425
+ }
208
426
  }
209
427
  });
210
428
 
@@ -219,3 +437,101 @@ export function subscribe(gun, path, callback, options = {}) {
219
437
  };
220
438
  }
221
439
  }
440
+
441
+ // ============================================================================
442
+ // GLOBAL TABLE OPERATIONS
443
+ // ============================================================================
444
+
445
+ /**
446
+ * Write data to a global table
447
+ * Global tables are app-wide data not tied to specific holons (e.g., schemas, federation)
448
+ * @param {Object} gun - Gun instance
449
+ * @param {string} appname - Application namespace
450
+ * @param {string} tableName - Global table name
451
+ * @param {Object} data - Data to write (must have 'id' field)
452
+ * @returns {Promise<boolean>} Success indicator
453
+ */
454
+ export async function writeGlobal(gun, appname, tableName, data) {
455
+ if (!data || !data.id) {
456
+ throw new Error('writeGlobal: data must have an id field');
457
+ }
458
+ const path = buildGlobalPath(appname, tableName, data.id);
459
+ return write(gun, path, data);
460
+ }
461
+
462
+ /**
463
+ * Read data from a global table
464
+ * @param {Object} gun - Gun instance
465
+ * @param {string} appname - Application namespace
466
+ * @param {string} tableName - Global table name
467
+ * @param {string} key - Data key
468
+ * @returns {Promise<Object|null>} Data or null if not found
469
+ */
470
+ export async function readGlobal(gun, appname, tableName, key) {
471
+ const path = buildGlobalPath(appname, tableName, key);
472
+ return read(gun, path);
473
+ }
474
+
475
+ /**
476
+ * Read all data from a global table
477
+ * Uses same approach as readAll
478
+ * @param {Object} gun - Gun instance
479
+ * @param {string} appname - Application namespace
480
+ * @param {string} tableName - Global table name
481
+ * @param {number} timeout - Timeout in ms (default: 2000)
482
+ * @returns {Promise<Object[]>} Array of data objects
483
+ */
484
+ export async function readAllGlobal(gun, appname, tableName, timeout = 2000) {
485
+ const path = buildGlobalPath(appname, tableName);
486
+ return readAll(gun, path);
487
+ }
488
+
489
+ /**
490
+ * Delete data from a global table
491
+ * @param {Object} gun - Gun instance
492
+ * @param {string} appname - Application namespace
493
+ * @param {string} tableName - Global table name
494
+ * @param {string} key - Data key
495
+ * @returns {Promise<boolean>} Success indicator
496
+ */
497
+ export async function deleteGlobal(gun, appname, tableName, key) {
498
+ const path = buildGlobalPath(appname, tableName, key);
499
+ return deleteData(gun, path);
500
+ }
501
+
502
+ /**
503
+ * Delete all data from a global table
504
+ * @param {Object} gun - Gun instance
505
+ * @param {string} appname - Application namespace
506
+ * @param {string} tableName - Global table name
507
+ * @returns {Promise<Object>} Deletion results { success: boolean, count: number }
508
+ */
509
+ export async function deleteAllGlobal(gun, appname, tableName) {
510
+ const path = buildGlobalPath(appname, tableName);
511
+ return deleteAll(gun, path);
512
+ }
513
+
514
+ // ============================================================================
515
+ // DATA PARSING UTILITIES
516
+ // ============================================================================
517
+
518
+ /**
519
+ * Parse data from Gun storage, handling various formats
520
+ * - JSON string in _json field
521
+ * - Legacy object format
522
+ * - Gun references
523
+ * @param {*} rawData - Raw data from Gun
524
+ * @returns {Object|null} Parsed data or null
525
+ */
526
+ export function parse(rawData) {
527
+ return deserializeFromGun(rawData);
528
+ }
529
+
530
+ /**
531
+ * Serialize data for Gun storage
532
+ * @param {Object} data - Data to serialize
533
+ * @returns {string} JSON string
534
+ */
535
+ export function serialize(data) {
536
+ return serializeForGun(data);
537
+ }
@@ -8,8 +8,10 @@ import { PersistentStorage } from './persistent-storage.js';
8
8
  export class IndexedDBStorage extends PersistentStorage {
9
9
  constructor() {
10
10
  super();
11
+ /** @type {IDBDatabase|null} */
11
12
  this.db = null;
12
- this.dbName = null;
13
+ /** @type {string} */
14
+ this.dbName = '';
13
15
  this.storeName = 'events';
14
16
  }
15
17
 
@@ -22,11 +24,12 @@ export class IndexedDBStorage extends PersistentStorage {
22
24
  request.onerror = () => reject(request.error);
23
25
  request.onsuccess = () => {
24
26
  this.db = request.result;
25
- resolve();
27
+ resolve(undefined);
26
28
  };
27
29
 
28
30
  request.onupgradeneeded = (event) => {
29
- const db = event.target.result;
31
+ const target = /** @type {IDBOpenDBRequest} */ (event.target);
32
+ const db = target.result;
30
33
 
31
34
  // Create object store if it doesn't exist
32
35
  if (!db.objectStoreNames.contains(this.storeName)) {
@@ -40,18 +43,26 @@ export class IndexedDBStorage extends PersistentStorage {
40
43
 
41
44
  async put(key, event) {
42
45
  return new Promise((resolve, reject) => {
46
+ if (!this.db) {
47
+ reject(new Error('Database not initialized'));
48
+ return;
49
+ }
43
50
  const transaction = this.db.transaction([this.storeName], 'readwrite');
44
51
  const objectStore = transaction.objectStore(this.storeName);
45
52
 
46
53
  const request = objectStore.put({ key, event, timestamp: Date.now() });
47
54
 
48
- request.onsuccess = () => resolve();
55
+ request.onsuccess = () => resolve(undefined);
49
56
  request.onerror = () => reject(request.error);
50
57
  });
51
58
  }
52
59
 
53
60
  async get(key) {
54
61
  return new Promise((resolve, reject) => {
62
+ if (!this.db) {
63
+ reject(new Error('Database not initialized'));
64
+ return;
65
+ }
55
66
  const transaction = this.db.transaction([this.storeName], 'readonly');
56
67
  const objectStore = transaction.objectStore(this.storeName);
57
68
 
@@ -65,20 +76,39 @@ export class IndexedDBStorage extends PersistentStorage {
65
76
  });
66
77
  }
67
78
 
79
+ /**
80
+ * @param {string} prefix
81
+ * @returns {Promise<any[]>}
82
+ */
68
83
  async getAll(prefix) {
69
84
  return new Promise((resolve, reject) => {
85
+ if (!this.db) {
86
+ reject(new Error('Database not initialized'));
87
+ return;
88
+ }
70
89
  const transaction = this.db.transaction([this.storeName], 'readonly');
71
90
  const objectStore = transaction.objectStore(this.storeName);
72
91
 
92
+ /** @type {any[]} */
73
93
  const results = [];
74
- const request = objectStore.openCursor();
94
+
95
+ // Use IDBKeyRange for efficient prefix query instead of full table scan
96
+ // This creates a range from "prefix" to "prefix\uffff" (highest unicode char)
97
+ // which efficiently uses the B-tree index
98
+ let request;
99
+ if (prefix) {
100
+ const range = IDBKeyRange.bound(prefix, prefix + '\uffff', false, false);
101
+ request = objectStore.openCursor(range);
102
+ } else {
103
+ // No prefix = get all
104
+ request = objectStore.openCursor();
105
+ }
75
106
 
76
107
  request.onsuccess = (event) => {
77
- const cursor = event.target.result;
108
+ const target = /** @type {IDBRequest} */ (event.target);
109
+ const cursor = target.result;
78
110
  if (cursor) {
79
- if (cursor.value.key.startsWith(prefix)) {
80
- results.push(cursor.value.event);
81
- }
111
+ results.push(cursor.value.event);
82
112
  cursor.continue();
83
113
  } else {
84
114
  resolve(results);
@@ -91,24 +121,32 @@ export class IndexedDBStorage extends PersistentStorage {
91
121
 
92
122
  async delete(key) {
93
123
  return new Promise((resolve, reject) => {
124
+ if (!this.db) {
125
+ reject(new Error('Database not initialized'));
126
+ return;
127
+ }
94
128
  const transaction = this.db.transaction([this.storeName], 'readwrite');
95
129
  const objectStore = transaction.objectStore(this.storeName);
96
130
 
97
131
  const request = objectStore.delete(key);
98
132
 
99
- request.onsuccess = () => resolve();
133
+ request.onsuccess = () => resolve(undefined);
100
134
  request.onerror = () => reject(request.error);
101
135
  });
102
136
  }
103
137
 
104
138
  async clear() {
105
139
  return new Promise((resolve, reject) => {
140
+ if (!this.db) {
141
+ reject(new Error('Database not initialized'));
142
+ return;
143
+ }
106
144
  const transaction = this.db.transaction([this.storeName], 'readwrite');
107
145
  const objectStore = transaction.objectStore(this.storeName);
108
146
 
109
147
  const request = objectStore.clear();
110
148
 
111
- request.onsuccess = () => resolve();
149
+ request.onsuccess = () => resolve(undefined);
112
150
  request.onerror = () => reject(request.error);
113
151
  });
114
152
  }
@@ -32,7 +32,12 @@ export class MemoryStorage extends PersistentStorage {
32
32
  return data ? JSON.parse(JSON.stringify(data)) : null; // Deep clone
33
33
  }
34
34
 
35
+ /**
36
+ * @param {string} prefix
37
+ * @returns {Promise<any[]>}
38
+ */
35
39
  async getAll(prefix) {
40
+ /** @type {any[]} */
36
41
  const results = [];
37
42
  for (const [key, value] of this.data.entries()) {
38
43
  if (key.startsWith(prefix)) {