@geolonia/geonicdb-sdk 0.11.1 → 0.13.0
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/README.md +81 -0
- package/geonicdb.cjs +302 -2
- package/geonicdb.cjs.map +3 -3
- package/geonicdb.iife.js +302 -2
- package/geonicdb.iife.js.map +3 -3
- package/geonicdb.mjs +302 -2
- package/geonicdb.mjs.map +3 -3
- package/ngsi-v2/index.cjs +92 -0
- package/ngsi-v2/index.cjs.map +7 -0
- package/ngsi-v2/index.d.ts +58 -0
- package/ngsi-v2/index.mjs +73 -0
- package/ngsi-v2/index.mjs.map +7 -0
- package/package.json +36 -1
- package/react/index.cjs +266 -0
- package/react/index.cjs.map +7 -0
- package/react/index.d.ts +551 -0
- package/react/index.mjs +233 -0
- package/react/index.mjs.map +7 -0
package/README.md
CHANGED
|
@@ -326,6 +326,38 @@ const [a, b, c] = await Promise.all([
|
|
|
326
326
|
|
|
327
327
|
Cache is enabled by default; pass `cache: false` to opt out.
|
|
328
328
|
|
|
329
|
+
### Persistence across reloads
|
|
330
|
+
|
|
331
|
+
In the browser the cache (validators + bodies) is **persisted to IndexedDB** so
|
|
332
|
+
it survives a page reload. A reloaded SPA can therefore send `If-None-Match` on
|
|
333
|
+
its very first request and get a `304` instead of always cold-starting into a
|
|
334
|
+
full `200` body.
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// First load: fetched in full and saved to IndexedDB.
|
|
338
|
+
await db.getEntities({ type: 'AedLocation' });
|
|
339
|
+
|
|
340
|
+
// After a page reload, restore the session — the cache rehydrates with it:
|
|
341
|
+
if (await db.restoreSession()) {
|
|
342
|
+
await db.getEntities({ type: 'AedLocation' });
|
|
343
|
+
// → If-None-Match sent; unchanged data comes back as 304 (near-zero bytes).
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- Persistence is tied to `rememberSession`: it is active only for sessions that
|
|
348
|
+
themselves survive a reload (`login({ rememberSession: true })` →
|
|
349
|
+
`restoreSession()`), sharing the exact lifecycle of the persisted DPoP session.
|
|
350
|
+
- Entries are namespaced by tenant **and authenticated principal**, so a
|
|
351
|
+
different subject sharing the same origin/tenant (another tab, another SDK
|
|
352
|
+
instance, a later login) never reads entries written by a previous principal.
|
|
353
|
+
They are dropped on every credential change (`login()`, `setCredentials()`,
|
|
354
|
+
`logout()`) and only read back by the same principal that wrote them, via
|
|
355
|
+
`restoreSession()`.
|
|
356
|
+
- Capacity / LRU follow `cacheMaxEntries`, the same as the in-memory cache.
|
|
357
|
+
- In non-browser runtimes (Node, CLI) without IndexedDB the cache stays
|
|
358
|
+
in-memory only — fully backward compatible.
|
|
359
|
+
- `clearCache()` flushes the persisted store as well as the in-memory cache.
|
|
360
|
+
|
|
329
361
|
### Cache lifetime under WebSocket activity
|
|
330
362
|
|
|
331
363
|
WebSocket entity events (`entityCreated` / `entityUpdated` / `entityDeleted`)
|
|
@@ -558,6 +590,55 @@ try {
|
|
|
558
590
|
| `RateLimitError` | 429 | Rate limited (`retryAfter` property) |
|
|
559
591
|
| `NetworkError` | — | Fetch failure, DNS error, timeout |
|
|
560
592
|
|
|
593
|
+
## React Hooks (`@geolonia/geonicdb-sdk/react`)
|
|
594
|
+
|
|
595
|
+
Optional React integration, shipped as a separate subpath so the core SDK stays
|
|
596
|
+
framework-agnostic. Requires React 18 or 19 (declared as an **optional peer
|
|
597
|
+
dependency** — core consumers are unaffected).
|
|
598
|
+
|
|
599
|
+
```tsx
|
|
600
|
+
import GeonicDB from '@geolonia/geonicdb-sdk';
|
|
601
|
+
import { useLdEntities, useTemporalData, GeonicDbMap } from '@geolonia/geonicdb-sdk/react';
|
|
602
|
+
|
|
603
|
+
const db = new GeonicDB({ baseUrl, tenant });
|
|
604
|
+
|
|
605
|
+
function Rooms() {
|
|
606
|
+
const { entities, loading, error, refetch } = useLdEntities(db, { type: 'Room', limit: 100 });
|
|
607
|
+
if (loading) return <p>Loading…</p>;
|
|
608
|
+
if (error) return <p>{error.message}</p>;
|
|
609
|
+
return <GeonicDbMap entities={entities} apiKey="YOUR-GEOLONIA-KEY" />;
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
| Export | Type | Description |
|
|
614
|
+
|--------|------|-------------|
|
|
615
|
+
| `useLdEntities(client, options?)` | hook | NGSI-LD エンティティ取得。`{ entities, loading, error, refetch }` を返す。`options`: `type` / `limit` / `attrs` / `q` / `scopeQ` |
|
|
616
|
+
| `useTemporalData(client, options)` | hook | Temporal API 取得。`{ data, loading, error, refetch }` を返す。`options.id` 指定時は単一エンティティ、`options.enabled === false` で取得スキップ |
|
|
617
|
+
| `GeonicDbMap` | component | Geolonia 地図にエンティティをマーカー描画。`@geolonia/embed`(optional peer)を動的 import |
|
|
618
|
+
|
|
619
|
+
両 hook は in-flight リクエストを世代管理しており、`options` 変更やアンマウント時に古いレスポンスが新しい結果を上書きしない。`GeonicDbMap` のポップアップは `entity.id` / `entity.type` を HTML エスケープして描画する。
|
|
620
|
+
|
|
621
|
+
## NGSIv2 Client (`@geolonia/geonicdb-sdk/ngsi-v2`)
|
|
622
|
+
|
|
623
|
+
NGSIv2 (`/v2/entities`) を叩く軽量クライアント。NGSI-LD を使うコア SDK とは独立した
|
|
624
|
+
サブパスで、NGSIv2 のみを使う利用者向け。
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
import { createNgsiV2Client } from '@geolonia/geonicdb-sdk/ngsi-v2';
|
|
628
|
+
|
|
629
|
+
const client = createNgsiV2Client({ baseUrl: 'https://geonicdb.example.com', apiKey: 'KEY', service: 'mytenant' });
|
|
630
|
+
const rooms = await client.getEntities({ type: 'Room', limit: 20 });
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
| Method | Description |
|
|
634
|
+
|--------|-------------|
|
|
635
|
+
| `createNgsiV2Client(config)` | `NgsiV2Client` を生成。`config`: `baseUrl`(必須)/ `apiKey` / `service` |
|
|
636
|
+
| `getEntities(options?)` | エンティティ一覧(`type` / `q` / `limit` / `offset` / `attrs` / `options`) |
|
|
637
|
+
| `getEntity(id, options?)` | 単一エンティティ取得 |
|
|
638
|
+
| `createEntity(entity)` | エンティティ作成 |
|
|
639
|
+
| `updateEntityAttributes(id, attrs)` | 属性の部分更新 |
|
|
640
|
+
| `deleteEntity(id)` | エンティティ削除 |
|
|
641
|
+
|
|
561
642
|
## License
|
|
562
643
|
|
|
563
644
|
MIT
|
package/geonicdb.cjs
CHANGED
|
@@ -85,6 +85,9 @@ var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
|
|
|
85
85
|
var DPOP_IDB_NAME = "geonicdb-sdk";
|
|
86
86
|
var DPOP_IDB_STORE = "dpop-sessions";
|
|
87
87
|
var DPOP_IDB_VERSION = 1;
|
|
88
|
+
var SDK_CACHE_IDB_NAME = "geonicdb-sdk-cache";
|
|
89
|
+
var SDK_CACHE_IDB_STORE = "responses";
|
|
90
|
+
var SDK_CACHE_IDB_VERSION = 1;
|
|
88
91
|
|
|
89
92
|
// src/sdk/dpop.ts
|
|
90
93
|
var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
|
|
@@ -364,11 +367,97 @@ var SdkCache = class {
|
|
|
364
367
|
constructor(maxEntries = SDK_CACHE_MAX_ENTRIES_DEFAULT) {
|
|
365
368
|
__publicField(this, "_store", /* @__PURE__ */ new Map());
|
|
366
369
|
__publicField(this, "_maxEntries");
|
|
370
|
+
/**
|
|
371
|
+
* Optional persistent backing store (IndexedDB) so cached validators survive
|
|
372
|
+
* a page reload (#1262). `null` in non-browser runtimes — the cache then
|
|
373
|
+
* behaves as a pure in-memory LRU.
|
|
374
|
+
*/
|
|
375
|
+
__publicField(this, "_persistence", null);
|
|
376
|
+
/**
|
|
377
|
+
* Auth-scope namespace for persisted entries. The auth layer sets this to a
|
|
378
|
+
* tenant + authenticated-principal identifier so that entries written by one
|
|
379
|
+
* principal are never read back by another sharing the same origin/tenant
|
|
380
|
+
* (different tabs / SDK instances each scope to their own principal).
|
|
381
|
+
*/
|
|
382
|
+
__publicField(this, "_scope", "");
|
|
383
|
+
/**
|
|
384
|
+
* Whether write-through persistence is currently active. Driven by the auth
|
|
385
|
+
* layer: enabled only for remembered sessions, so persisted entries belong to
|
|
386
|
+
* exactly one principal and are dropped on every credential change.
|
|
387
|
+
*/
|
|
388
|
+
__publicField(this, "_persistEnabled", false);
|
|
389
|
+
/**
|
|
390
|
+
* Serialises persistence writes so they complete in call order (#1262
|
|
391
|
+
* review). `set`/`delete`/`clearPersisted` are otherwise independent
|
|
392
|
+
* fire-and-forget ops; if a late `put()` resolved after a `remove()` /
|
|
393
|
+
* `clearScope()`, an evicted/deleted/logged-out entry could reappear in
|
|
394
|
+
* IndexedDB. Chaining them keeps the persisted store consistent with the
|
|
395
|
+
* in-memory state and with the auth-boundary flush.
|
|
396
|
+
*/
|
|
397
|
+
__publicField(this, "_persistQueue", Promise.resolve());
|
|
367
398
|
if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
|
|
368
399
|
throw new Error(`SdkCache maxEntries must be a positive integer, got ${String(maxEntries)}`);
|
|
369
400
|
}
|
|
370
401
|
this._maxEntries = maxEntries;
|
|
371
402
|
}
|
|
403
|
+
/** Wire a persistence backend (called once at construction when available). */
|
|
404
|
+
attachPersistence(persistence) {
|
|
405
|
+
this._persistence = persistence;
|
|
406
|
+
}
|
|
407
|
+
/** Whether a persistence backend is attached. */
|
|
408
|
+
hasPersistence() {
|
|
409
|
+
return this._persistence !== null;
|
|
410
|
+
}
|
|
411
|
+
/** Set the auth-scope namespace used for persisted reads/writes. */
|
|
412
|
+
setScope(scope) {
|
|
413
|
+
this._scope = scope;
|
|
414
|
+
}
|
|
415
|
+
/** Enable or disable write-through persistence (driven by `rememberSession`). */
|
|
416
|
+
setPersistEnabled(enabled) {
|
|
417
|
+
this._persistEnabled = enabled && this._persistence !== null;
|
|
418
|
+
}
|
|
419
|
+
/** True when set/delete should propagate to the persistent store. */
|
|
420
|
+
get _persisting() {
|
|
421
|
+
return this._persistEnabled && this._persistence !== null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Append a persistence operation to the serialised queue. The backend and
|
|
425
|
+
* scope are captured at enqueue time so a later scope change cannot retarget
|
|
426
|
+
* an already-queued op. A failing op is swallowed (persistence is
|
|
427
|
+
* best-effort) and does not stall the chain. Returns the tail so callers that
|
|
428
|
+
* need ordering (`clearPersisted`) can await it.
|
|
429
|
+
*/
|
|
430
|
+
_enqueuePersist(op) {
|
|
431
|
+
const persistence = this._persistence;
|
|
432
|
+
if (!persistence) return this._persistQueue;
|
|
433
|
+
const scope = this._scope;
|
|
434
|
+
this._persistQueue = this._persistQueue.catch(() => void 0).then(() => op(persistence, scope)).catch(() => void 0);
|
|
435
|
+
return this._persistQueue;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Load persisted entries for the current scope into memory, oldest first so
|
|
439
|
+
* the in-memory LRU recency order is preserved. Best-effort and idempotent —
|
|
440
|
+
* a no-op when no backend is attached. Called after a remembered session is
|
|
441
|
+
* restored so the very first request after a reload can send `If-None-Match`.
|
|
442
|
+
*/
|
|
443
|
+
async hydrate() {
|
|
444
|
+
if (!this._persistence) return;
|
|
445
|
+
const entries = await this._persistence.loadScope(this._scope, this._maxEntries);
|
|
446
|
+
for (const { key, entry } of entries) {
|
|
447
|
+
if (this._store.has(key)) this._store.delete(key);
|
|
448
|
+
this._store.set(key, entry);
|
|
449
|
+
}
|
|
450
|
+
while (this._store.size > this._maxEntries) {
|
|
451
|
+
const oldest = this._store.keys().next().value;
|
|
452
|
+
if (oldest === void 0) break;
|
|
453
|
+
this._store.delete(oldest);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** Drop every persisted entry for the current scope (auth change / flush). */
|
|
457
|
+
async clearPersisted() {
|
|
458
|
+
if (!this._persistence) return;
|
|
459
|
+
await this._enqueuePersist((persistence, scope) => persistence.clearScope(scope));
|
|
460
|
+
}
|
|
372
461
|
/** Build a stable cache key. Method is upper-cased so casing differences do not produce duplicate entries. */
|
|
373
462
|
static keyFor(method, path) {
|
|
374
463
|
return `${method.toUpperCase()}:${path}`;
|
|
@@ -387,15 +476,27 @@ var SdkCache = class {
|
|
|
387
476
|
this._store.delete(key);
|
|
388
477
|
}
|
|
389
478
|
this._store.set(key, entry);
|
|
479
|
+
const evicted = [];
|
|
390
480
|
while (this._store.size > this._maxEntries) {
|
|
391
481
|
const oldest = this._store.keys().next().value;
|
|
392
482
|
if (oldest === void 0) break;
|
|
393
483
|
this._store.delete(oldest);
|
|
484
|
+
evicted.push(oldest);
|
|
485
|
+
}
|
|
486
|
+
if (this._persisting) {
|
|
487
|
+
void this._enqueuePersist(async (persistence, scope) => {
|
|
488
|
+
await persistence.put(scope, key, entry);
|
|
489
|
+
if (evicted.length > 0) await persistence.remove(scope, evicted);
|
|
490
|
+
});
|
|
394
491
|
}
|
|
395
492
|
}
|
|
396
493
|
/** Delete a specific entry. Returns whether anything was removed. */
|
|
397
494
|
delete(key) {
|
|
398
|
-
|
|
495
|
+
const removed = this._store.delete(key);
|
|
496
|
+
if (removed && this._persisting) {
|
|
497
|
+
void this._enqueuePersist((persistence, scope) => persistence.remove(scope, [key]));
|
|
498
|
+
}
|
|
499
|
+
return removed;
|
|
399
500
|
}
|
|
400
501
|
/**
|
|
401
502
|
* Delete every entry whose key matches a predicate. Returns the array of
|
|
@@ -533,6 +634,49 @@ var _AuthManager = class _AuthManager {
|
|
|
533
634
|
/** Wire an SdkCache instance (called by GeonicDB constructor when caching is enabled). */
|
|
534
635
|
setCache(cache) {
|
|
535
636
|
this._cache = cache;
|
|
637
|
+
cache?.setScope(this._cacheScope());
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* #1262: persistence scope for the SDK cache — tenant + authenticated
|
|
641
|
+
* principal (email when available). Keying on the principal, not just the
|
|
642
|
+
* tenant, prevents a different subject sharing the same origin/tenant (another
|
|
643
|
+
* tab, another SDK instance, a later login) from reading entries written by a
|
|
644
|
+
* previous principal. The principal segment is empty for sessions without a
|
|
645
|
+
* known email; such sessions never persist (persistence is gated on a
|
|
646
|
+
* remembered DPoP session that always carries an email), so the empty segment
|
|
647
|
+
* is not reachable as a write scope in practice.
|
|
648
|
+
*/
|
|
649
|
+
_cacheScope() {
|
|
650
|
+
return `${this._tenant}${this._email ?? ""}`;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* #1262: enable write-through persistence for the SDK cache so it survives a
|
|
654
|
+
* reload. Always re-scopes to the current tenant + principal first.
|
|
655
|
+
*
|
|
656
|
+
* `clearFirst` drops any persisted entries left by a previous session of the
|
|
657
|
+
* same principal — used on a fresh `login()`. It is `false` on
|
|
658
|
+
* `restoreSession()`, where the persisted entries belong to the very session
|
|
659
|
+
* being restored and are about to be rehydrated.
|
|
660
|
+
*/
|
|
661
|
+
async _enablePersistentCache(clearFirst) {
|
|
662
|
+
if (!this._cache) return;
|
|
663
|
+
this._cache.setScope(this._cacheScope());
|
|
664
|
+
if (clearFirst) await this._cache.clearPersisted();
|
|
665
|
+
this._cache.setPersistEnabled(true);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* #1262: disable write-through persistence and drop the persisted cache for
|
|
669
|
+
* the current principal. Called on credential changes that do not persist a
|
|
670
|
+
* session (`setCredentials()`, non-remembered `login()`, `logout()`),
|
|
671
|
+
* mirroring the in-memory invalidation so a stale body can never outlive its
|
|
672
|
+
* principal. Callers that null out `_email` must invoke this **before** doing
|
|
673
|
+
* so, otherwise the scope no longer resolves to the principal being dropped.
|
|
674
|
+
*/
|
|
675
|
+
_disablePersistentCache() {
|
|
676
|
+
if (!this._cache) return;
|
|
677
|
+
this._cache.setScope(this._cacheScope());
|
|
678
|
+
this._cache.setPersistEnabled(false);
|
|
679
|
+
void this._cache.clearPersisted();
|
|
536
680
|
}
|
|
537
681
|
/** Provide the cache event emitter (forwarded to the GeonicDB EventEmitter). */
|
|
538
682
|
setCacheEventEmitter(emitter) {
|
|
@@ -587,6 +731,7 @@ var _AuthManager = class _AuthManager {
|
|
|
587
731
|
this._email = email;
|
|
588
732
|
this._rememberSession = wantsRemember;
|
|
589
733
|
this._invalidateAuthScopedState();
|
|
734
|
+
this._cache?.setPersistEnabled(false);
|
|
590
735
|
if (!wantsRemember && this._tenant) {
|
|
591
736
|
void clearDPoPSession(this._tenant);
|
|
592
737
|
}
|
|
@@ -596,6 +741,9 @@ var _AuthManager = class _AuthManager {
|
|
|
596
741
|
const bound = await this._bindDpopSession();
|
|
597
742
|
if (this._rememberSession) {
|
|
598
743
|
await this._persistDpopSession();
|
|
744
|
+
await this._enablePersistentCache(true);
|
|
745
|
+
} else {
|
|
746
|
+
this._disablePersistentCache();
|
|
599
747
|
}
|
|
600
748
|
return bound;
|
|
601
749
|
} catch (err) {
|
|
@@ -604,12 +752,18 @@ var _AuthManager = class _AuthManager {
|
|
|
604
752
|
this._tokenType = "Bearer";
|
|
605
753
|
this._refreshToken = null;
|
|
606
754
|
this._tokenPromise = null;
|
|
755
|
+
this._invalidateAuthScopedState();
|
|
756
|
+
this._disablePersistentCache();
|
|
607
757
|
this._rememberSession = false;
|
|
608
758
|
this._email = null;
|
|
609
|
-
this._invalidateAuthScopedState();
|
|
610
759
|
throw err;
|
|
611
760
|
}
|
|
612
761
|
}
|
|
762
|
+
this._rememberSession = false;
|
|
763
|
+
if (this._tenant) {
|
|
764
|
+
void clearDPoPSession(this._tenant);
|
|
765
|
+
}
|
|
766
|
+
this._disablePersistentCache();
|
|
613
767
|
return data;
|
|
614
768
|
}
|
|
615
769
|
/**
|
|
@@ -694,6 +848,12 @@ var _AuthManager = class _AuthManager {
|
|
|
694
848
|
this._refreshToken = opts.refreshToken || null;
|
|
695
849
|
this._tokenPromise = null;
|
|
696
850
|
this._invalidateAuthScopedState();
|
|
851
|
+
this._disablePersistentCache();
|
|
852
|
+
this._rememberSession = false;
|
|
853
|
+
this._email = null;
|
|
854
|
+
if (this._tenant) {
|
|
855
|
+
void clearDPoPSession(this._tenant);
|
|
856
|
+
}
|
|
697
857
|
}
|
|
698
858
|
/** Clear all credentials. */
|
|
699
859
|
logout() {
|
|
@@ -701,6 +861,7 @@ var _AuthManager = class _AuthManager {
|
|
|
701
861
|
this._tokenExpiry = 0;
|
|
702
862
|
this._refreshToken = null;
|
|
703
863
|
this._tokenPromise = null;
|
|
864
|
+
this._disablePersistentCache();
|
|
704
865
|
this._email = null;
|
|
705
866
|
if (this._tenant) {
|
|
706
867
|
void clearDPoPSession(this._tenant);
|
|
@@ -746,6 +907,8 @@ var _AuthManager = class _AuthManager {
|
|
|
746
907
|
this._email = persisted.email ?? null;
|
|
747
908
|
this._tokenPromise = null;
|
|
748
909
|
this._invalidateAuthScopedState();
|
|
910
|
+
await this._enablePersistentCache(false);
|
|
911
|
+
await this._cache?.hydrate();
|
|
749
912
|
return true;
|
|
750
913
|
}
|
|
751
914
|
/**
|
|
@@ -1368,6 +1531,140 @@ var WebSocketManager = class {
|
|
|
1368
1531
|
}
|
|
1369
1532
|
};
|
|
1370
1533
|
|
|
1534
|
+
// src/sdk/cache-persistence.ts
|
|
1535
|
+
var cachePersistenceSupported = typeof globalThis.indexedDB !== "undefined" && typeof structuredClone !== "undefined";
|
|
1536
|
+
var SCOPE_SEP = "\0";
|
|
1537
|
+
var SCOPE_RANGE_END = "\uFFFF";
|
|
1538
|
+
function openCacheDB() {
|
|
1539
|
+
return new Promise((resolve, reject) => {
|
|
1540
|
+
const req = globalThis.indexedDB.open(
|
|
1541
|
+
SDK_CACHE_IDB_NAME,
|
|
1542
|
+
SDK_CACHE_IDB_VERSION
|
|
1543
|
+
);
|
|
1544
|
+
req.onupgradeneeded = () => {
|
|
1545
|
+
const db = req.result;
|
|
1546
|
+
if (!db.objectStoreNames.contains(SDK_CACHE_IDB_STORE)) {
|
|
1547
|
+
db.createObjectStore(SDK_CACHE_IDB_STORE, { keyPath: "id" });
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
req.onsuccess = () => {
|
|
1551
|
+
resolve(req.result);
|
|
1552
|
+
};
|
|
1553
|
+
req.onerror = () => {
|
|
1554
|
+
reject(req.error ?? new Error("IndexedDB open failed"));
|
|
1555
|
+
};
|
|
1556
|
+
req.onblocked = () => {
|
|
1557
|
+
reject(req.error ?? new Error("IndexedDB open blocked"));
|
|
1558
|
+
};
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
function recordId(scope, key) {
|
|
1562
|
+
return scope + SCOPE_SEP + key;
|
|
1563
|
+
}
|
|
1564
|
+
var IdbCachePersistence = class {
|
|
1565
|
+
async loadScope(scope, limit) {
|
|
1566
|
+
let db = null;
|
|
1567
|
+
try {
|
|
1568
|
+
db = await openCacheDB();
|
|
1569
|
+
const records = await new Promise((resolve, reject) => {
|
|
1570
|
+
const tx = db.transaction(SDK_CACHE_IDB_STORE, "readonly");
|
|
1571
|
+
const range = globalThis.IDBKeyRange.bound(
|
|
1572
|
+
scope + SCOPE_SEP,
|
|
1573
|
+
scope + SCOPE_SEP + SCOPE_RANGE_END
|
|
1574
|
+
);
|
|
1575
|
+
const req = tx.objectStore(SDK_CACHE_IDB_STORE).getAll(range);
|
|
1576
|
+
req.onsuccess = () => {
|
|
1577
|
+
resolve(req.result ?? []);
|
|
1578
|
+
};
|
|
1579
|
+
req.onerror = () => {
|
|
1580
|
+
reject(req.error ?? new Error("IndexedDB getAll failed"));
|
|
1581
|
+
};
|
|
1582
|
+
});
|
|
1583
|
+
return records.filter((r) => r && r.scope === scope && typeof r.key === "string" && r.entry).sort((a, b) => (a.entry.cachedAt ?? 0) - (b.entry.cachedAt ?? 0)).slice(-limit).map((r) => ({ key: r.key, entry: r.entry }));
|
|
1584
|
+
} catch {
|
|
1585
|
+
return [];
|
|
1586
|
+
} finally {
|
|
1587
|
+
db?.close();
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async put(scope, key, entry) {
|
|
1591
|
+
let db = null;
|
|
1592
|
+
try {
|
|
1593
|
+
db = await openCacheDB();
|
|
1594
|
+
await new Promise((resolve, reject) => {
|
|
1595
|
+
const tx = db.transaction(SDK_CACHE_IDB_STORE, "readwrite");
|
|
1596
|
+
const record = { id: recordId(scope, key), scope, key, entry };
|
|
1597
|
+
tx.objectStore(SDK_CACHE_IDB_STORE).put(record);
|
|
1598
|
+
tx.oncomplete = () => {
|
|
1599
|
+
resolve();
|
|
1600
|
+
};
|
|
1601
|
+
tx.onerror = () => {
|
|
1602
|
+
reject(tx.error ?? new Error("IndexedDB put failed"));
|
|
1603
|
+
};
|
|
1604
|
+
tx.onabort = () => {
|
|
1605
|
+
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
} catch {
|
|
1609
|
+
} finally {
|
|
1610
|
+
db?.close();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async remove(scope, keys) {
|
|
1614
|
+
if (keys.length === 0) return;
|
|
1615
|
+
let db = null;
|
|
1616
|
+
try {
|
|
1617
|
+
db = await openCacheDB();
|
|
1618
|
+
await new Promise((resolve, reject) => {
|
|
1619
|
+
const tx = db.transaction(SDK_CACHE_IDB_STORE, "readwrite");
|
|
1620
|
+
const store = tx.objectStore(SDK_CACHE_IDB_STORE);
|
|
1621
|
+
for (const key of keys) store.delete(recordId(scope, key));
|
|
1622
|
+
tx.oncomplete = () => {
|
|
1623
|
+
resolve();
|
|
1624
|
+
};
|
|
1625
|
+
tx.onerror = () => {
|
|
1626
|
+
reject(tx.error ?? new Error("IndexedDB delete failed"));
|
|
1627
|
+
};
|
|
1628
|
+
tx.onabort = () => {
|
|
1629
|
+
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
1630
|
+
};
|
|
1631
|
+
});
|
|
1632
|
+
} catch {
|
|
1633
|
+
} finally {
|
|
1634
|
+
db?.close();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async clearScope(scope) {
|
|
1638
|
+
let db = null;
|
|
1639
|
+
try {
|
|
1640
|
+
db = await openCacheDB();
|
|
1641
|
+
await new Promise((resolve, reject) => {
|
|
1642
|
+
const tx = db.transaction(SDK_CACHE_IDB_STORE, "readwrite");
|
|
1643
|
+
const range = globalThis.IDBKeyRange.bound(
|
|
1644
|
+
scope + SCOPE_SEP,
|
|
1645
|
+
scope + SCOPE_SEP + SCOPE_RANGE_END
|
|
1646
|
+
);
|
|
1647
|
+
tx.objectStore(SDK_CACHE_IDB_STORE).delete(range);
|
|
1648
|
+
tx.oncomplete = () => {
|
|
1649
|
+
resolve();
|
|
1650
|
+
};
|
|
1651
|
+
tx.onerror = () => {
|
|
1652
|
+
reject(tx.error ?? new Error("IndexedDB clear failed"));
|
|
1653
|
+
};
|
|
1654
|
+
tx.onabort = () => {
|
|
1655
|
+
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
1656
|
+
};
|
|
1657
|
+
});
|
|
1658
|
+
} catch {
|
|
1659
|
+
} finally {
|
|
1660
|
+
db?.close();
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
function createCachePersistence() {
|
|
1665
|
+
return cachePersistenceSupported ? new IdbCachePersistence() : null;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1371
1668
|
// src/sdk/index.ts
|
|
1372
1669
|
var GeonicDB = class extends EventEmitter {
|
|
1373
1670
|
constructor(options) {
|
|
@@ -1397,6 +1694,8 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1397
1694
|
};
|
|
1398
1695
|
if (opts.cache !== false) {
|
|
1399
1696
|
const cache = new SdkCache(opts.cacheMaxEntries ?? SDK_CACHE_MAX_ENTRIES_DEFAULT);
|
|
1697
|
+
const persistence = createCachePersistence();
|
|
1698
|
+
if (persistence) cache.attachPersistence(persistence);
|
|
1400
1699
|
this._auth.setCache(cache);
|
|
1401
1700
|
this._auth.setCacheEventEmitter((name, payload) => this.emit(name, payload));
|
|
1402
1701
|
}
|
|
@@ -1425,6 +1724,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1425
1724
|
for (const key of removed) {
|
|
1426
1725
|
this._auth.emitCacheEvent("cacheInvalidated", { key, path: key.split(":").slice(1).join(":") });
|
|
1427
1726
|
}
|
|
1727
|
+
void cache.clearPersisted();
|
|
1428
1728
|
}
|
|
1429
1729
|
// --- Authentication ---
|
|
1430
1730
|
/**
|