@geolonia/geonicdb-sdk 0.19.2 → 0.20.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/geonicdb.d.ts CHANGED
@@ -410,15 +410,6 @@ declare class EventEmitter {
410
410
  * Copyright (c) 2026 Geolonia Inc.
411
411
  * Released under the MIT License. See src/sdk/LICENSE for details.
412
412
  */
413
- /**
414
- * Web Push (VAPID) notification helpers (#3014 / #3060).
415
- *
416
- * Pure conversion functions between the browser Push API and the
417
- * NGSIv2 `notification.webpush` / NGSI-LD `endpoint.webpush` shapes
418
- * documented in docs/SUBSCRIPTIONS.md. These do not touch the network —
419
- * see `GeonicDB.getWebPushVapidKey()` / `subscribeWebPush()` /
420
- * `unsubscribeWebPush()` in `./index` for the browser-facing API.
421
- */
422
413
  /** `PushSubscription.toJSON().keys` shape (RFC 8291). */
423
414
  export interface WebPushKeys {
424
415
  p256dh: string;
@@ -445,6 +436,68 @@ export interface WebPushSubscribeOptions {
445
436
  */
446
437
  serviceWorkerRegistration?: ServiceWorkerRegistration;
447
438
  }
439
+ /** Which NGSI protocol `registerWebPushSubscription()` should talk to (#3092). */
440
+ export type WebPushRegisterProtocol = "ngsiv2" | "ngsi-ld";
441
+ /** Entity selector shared by NGSIv2 `subject.entities` and NGSI-LD `entities`. */
442
+ export interface WebPushRegisterEntitySelector {
443
+ id?: string;
444
+ idPattern?: string;
445
+ type?: string;
446
+ }
447
+ /** NGSIv2 subscription `subject` accepted by `registerWebPushSubscription()`. */
448
+ export interface WebPushRegisterNgsiv2Subject {
449
+ entities: WebPushRegisterEntitySelector[];
450
+ condition?: {
451
+ attrs?: string[];
452
+ expression?: string;
453
+ };
454
+ }
455
+ /**
456
+ * Result of `GeonicDB.registerWebPushSubscription()` (#3092).
457
+ * `created` is `true` only when a new GeonicDB subscription was POSTed;
458
+ * reuse (and VAPID-rotation PATCH) returns `created: false`.
459
+ */
460
+ export interface RegisterWebPushSubscriptionResult {
461
+ subscriptionId: string;
462
+ created: boolean;
463
+ }
464
+ /**
465
+ * Options for `GeonicDB.registerWebPushSubscription()` (#3092).
466
+ *
467
+ * `protocol` is required with no default — the SDK already exposes separate
468
+ * NGSIv2 / NGSI-LD converters, and an implicit default would silently register
469
+ * against the wrong API.
470
+ */
471
+ export interface RegisterWebPushSubscriptionOptions extends WebPushSubscribeOptions, WebPushDeliveryOptions {
472
+ protocol: WebPushRegisterProtocol;
473
+ /** Subscription description. Defaults to `'Web Push notification'`. */
474
+ description?: string;
475
+ /** Required when `protocol` is `'ngsiv2'`. */
476
+ subject?: WebPushRegisterNgsiv2Subject;
477
+ /** Required when `protocol` is `'ngsi-ld'`. */
478
+ entities?: WebPushRegisterEntitySelector[];
479
+ /** NGSI-LD `watchedAttributes` (ignored for NGSIv2 — use `subject.condition.attrs`). */
480
+ watchedAttributes?: string[];
481
+ /** NGSI-LD `q` filter (ignored for NGSIv2). */
482
+ q?: string;
483
+ /**
484
+ * Override the localStorage key used for idempotency. Default is
485
+ * `buildWebPushSubscriptionStorageKey(baseUrl, tenant, protocol)` — it does
486
+ * **not** include login principal. On a shared browser (kiosk, shared profile)
487
+ * or when registering multiple distinct filters from the same origin, pass a
488
+ * key that still includes that default scope plus app-managed suffixes
489
+ * (e.g. `` `${buildWebPushSubscriptionStorageKey(...)}:${userId}:${filterId}` ``).
490
+ * See docs/SDK.md → Web Push (#3097).
491
+ */
492
+ storageKey?: string;
493
+ }
494
+ /**
495
+ * Build the default localStorage key for a Web Push GeonicDB subscription id
496
+ * (`baseUrl` + `tenant` + `protocol` only — not principal). Exported so callers
497
+ * can append user/filter suffixes for shared-browser isolation (#3097) or clear
498
+ * the persisted id.
499
+ */
500
+ export declare function buildWebPushSubscriptionStorageKey(baseUrl: string, tenant: string, protocol: WebPushRegisterProtocol): string;
448
501
  /**
449
502
  * Accepted by all conversion functions: either a `PushSubscription.toJSON()`
450
503
  * plain-object shape (`{ endpoint, keys: { p256dh, auth } }`), or a native
@@ -462,6 +515,14 @@ export interface WebPushSubscriptionLike {
462
515
  *
463
516
  * @example
464
517
  * ```ts
518
+ * // Prefer the idempotent high-level helper (#3092):
519
+ * await db.registerWebPushSubscription({
520
+ * protocol: 'ngsiv2',
521
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
522
+ * ttl: 86400,
523
+ * });
524
+ *
525
+ * // Or convert manually:
465
526
  * const subscription = await db.subscribeWebPush();
466
527
  * const webpush = toNgsiv2WebPushNotification(subscription, { ttl: 86400 });
467
528
  * await db.request('POST', '/v2/subscriptions', {
@@ -483,6 +544,13 @@ export declare function toNgsiv2WebPushNotification(subscription: WebPushSubscri
483
544
  *
484
545
  * @example
485
546
  * ```ts
547
+ * await db.registerWebPushSubscription({
548
+ * protocol: 'ngsi-ld',
549
+ * entities: [{ type: 'Sensor' }],
550
+ * watchedAttributes: ['value'],
551
+ * });
552
+ *
553
+ * // Or convert manually:
486
554
  * const subscription = await db.subscribeWebPush();
487
555
  * const { uri, webpush } = toNgsiLdWebPushEndpoint(subscription);
488
556
  * await db.request('POST', '/ngsi-ld/v1/subscriptions', {
@@ -584,6 +652,15 @@ export declare class RateLimitError extends GeonicDBError {
584
652
  readonly retryAfter: number;
585
653
  constructor(message?: string, retryAfter?: number);
586
654
  }
655
+ /**
656
+ * 507 — Storage quota exceeded (subscriptions / entities / registrations /
657
+ * temporal data points beyond the plan's grace ceiling). Callers typically
658
+ * surface a "try again later" toast rather than treating this as a permanent
659
+ * failure (#3092).
660
+ */
661
+ export declare class QuotaExceededError extends GeonicDBError {
662
+ constructor(message?: string);
663
+ }
587
664
  /** Network error (fetch failed, DNS resolution, timeout). */
588
665
  export declare class NetworkError extends GeonicDBError {
589
666
  constructor(message?: string);
@@ -610,6 +687,12 @@ export declare class NetworkError extends GeonicDBError {
610
687
  export declare class GeonicDB extends EventEmitter {
611
688
  private _auth;
612
689
  private _ws;
690
+ /**
691
+ * In-flight `registerWebPushSubscription()` calls keyed by storageKey (#3092).
692
+ * Concurrent callers with the same key share one Promise so a race cannot
693
+ * POST two subscriptions before the first id is persisted.
694
+ */
695
+ private _webPushRegisterInflight;
613
696
  /**
614
697
  * Legacy callback for token refresh events.
615
698
  * Prefer `db.on('tokenRefresh', callback)` for new code.
@@ -812,9 +895,13 @@ export declare class GeonicDB extends EventEmitter {
812
895
  * — otherwise (e.g. after the deployment rotated its VAPID keys) it
813
896
  * unsubscribes the stale one and re-subscribes with the current key.
814
897
  * Fetches the deployment's VAPID public key (unless `applicationServerKey`
815
- * is given) and calls `PushManager.subscribe()`. Convert the result with
816
- * `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()` and create
817
- * the NGSI subscription with `request('POST', ...)`.
898
+ * is given) and calls `PushManager.subscribe()`.
899
+ *
900
+ * This method only covers the browser Push API. To also create (or reuse)
901
+ * the GeonicDB subscription idempotently, prefer
902
+ * `registerWebPushSubscription()` (#3092). For a manual flow, convert the
903
+ * result with `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()`
904
+ * and `request('POST', ...)`.
818
905
  *
819
906
  * Browser-only — throws in environments without Service Worker + Push API
820
907
  * support (e.g. Node.js).
@@ -822,19 +909,54 @@ export declare class GeonicDB extends EventEmitter {
822
909
  * @example
823
910
  * ```ts
824
911
  * await navigator.serviceWorker.register('/sw.js');
825
- * const subscription = await db.subscribeWebPush();
826
- * await db.request('POST', '/v2/subscriptions', {
827
- * description: 'Web Push notification',
912
+ * const { subscriptionId, created } = await db.registerWebPushSubscription({
913
+ * protocol: 'ngsiv2',
828
914
  * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
829
- * notification: { webpush: toNgsiv2WebPushNotification(subscription) },
830
915
  * });
831
916
  * ```
832
917
  */
833
918
  subscribeWebPush(options?: WebPushSubscribeOptions): Promise<PushSubscription>;
919
+ /**
920
+ * Idempotently register a GeonicDB subscription that delivers via Web Push (#3092).
921
+ *
922
+ * 1. Calls `subscribeWebPush()` for the browser PushSubscription.
923
+ * 2. Looks up a previously saved subscription id in `localStorage`.
924
+ * 3. If that id still exists on the broker (`GET` → 200), returns it without
925
+ * `POST`ing. When the push endpoint changed (e.g. VAPID key rotation),
926
+ * `PATCH`es only `notification.webpush` / `endpoint.webpush` so a new
927
+ * subscription is not created (avoids burning the subscription quota).
928
+ * 4. Otherwise `POST`s a new subscription and persists its id.
929
+ *
930
+ * `protocol` is required — there is no default. A 507 from the broker is
931
+ * thrown as `QuotaExceededError`.
932
+ *
933
+ * @example
934
+ * ```ts
935
+ * import { QuotaExceededError } from '@geolonia/geonicdb-sdk';
936
+ *
937
+ * try {
938
+ * const { subscriptionId, created } = await db.registerWebPushSubscription({
939
+ * protocol: 'ngsiv2',
940
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
941
+ * });
942
+ * } catch (err) {
943
+ * if (err instanceof QuotaExceededError) {
944
+ * // plan grace ceiling — delete unused subscriptions or change plan, then retry
945
+ * }
946
+ * throw err;
947
+ * }
948
+ * ```
949
+ */
950
+ registerWebPushSubscription(options: RegisterWebPushSubscriptionOptions): Promise<RegisterWebPushSubscriptionResult>;
951
+ private _registerWebPushSubscriptionBody;
834
952
  /**
835
953
  * Unsubscribe the current browser from Web Push notifications, if
836
954
  * subscribed. Returns `false` (no-op) when there is no active
837
955
  * subscription or the environment has no Service Worker support.
956
+ *
957
+ * Does not delete the GeonicDB subscription document — clear the
958
+ * persisted id (see `buildWebPushSubscriptionStorageKey`) and
959
+ * `DELETE` the subscription separately if needed.
838
960
  */
839
961
  unsubscribeWebPush(registration?: ServiceWorkerRegistration): Promise<boolean>;
840
962
  }
package/geonicdb.iife.js CHANGED
@@ -31,10 +31,12 @@ var GeonicDBModule = (() => {
31
31
  NetworkError: () => NetworkError,
32
32
  NotFoundError: () => NotFoundError,
33
33
  PasswordResetRequiredError: () => PasswordResetRequiredError,
34
+ QuotaExceededError: () => QuotaExceededError,
34
35
  RateLimitError: () => RateLimitError,
35
36
  TemporaryPasswordExpiredError: () => TemporaryPasswordExpiredError,
36
37
  ValidationError: () => ValidationError,
37
38
  buildPathWithParams: () => buildPathWithParams,
39
+ buildWebPushSubscriptionStorageKey: () => buildWebPushSubscriptionStorageKey,
38
40
  default: () => index_default,
39
41
  toNgsiLdWebPushEndpoint: () => toNgsiLdWebPushEndpoint,
40
42
  toNgsiv2WebPushNotification: () => toNgsiv2WebPushNotification,
@@ -95,6 +97,7 @@ var GeonicDBModule = (() => {
95
97
  var SDK_CACHE_IDB_STORE = "responses";
96
98
  var SDK_CACHE_IDB_VERSION = 1;
97
99
  var WEBPUSH_VAPID_KEY_PATH = "/.well-known/webpush-vapid-key";
100
+ var WEBPUSH_SUBSCRIPTION_ID_STORAGE_PREFIX = "geonicdb:webpush:subscriptionId";
98
101
 
99
102
  // src/sdk/dpop.ts
100
103
  var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
@@ -362,6 +365,12 @@ var GeonicDBModule = (() => {
362
365
  this.retryAfter = retryAfter;
363
366
  }
364
367
  };
368
+ var QuotaExceededError = class extends GeonicDBError {
369
+ constructor(message = "Quota exceeded") {
370
+ super(message, 507);
371
+ this.name = "QuotaExceededError";
372
+ }
373
+ };
365
374
  var NetworkError = class extends GeonicDBError {
366
375
  constructor(message = "Network error") {
367
376
  super(message, 0);
@@ -383,6 +392,8 @@ var GeonicDBModule = (() => {
383
392
  return new ValidationError(message);
384
393
  case 429:
385
394
  return new RateLimitError(message);
395
+ case 507:
396
+ return new QuotaExceededError(message);
386
397
  default:
387
398
  return new GeonicDBError(message, status);
388
399
  }
@@ -648,6 +659,10 @@ var GeonicDBModule = (() => {
648
659
  get baseUrl() {
649
660
  return this._baseUrl;
650
661
  }
662
+ /** Fiware-Service / NGSILD-Tenant value configured at construction time. */
663
+ get tenant() {
664
+ return this._tenant;
665
+ }
651
666
  /**
652
667
  * True when running unauthenticated.
653
668
  *
@@ -1737,6 +1752,9 @@ var GeonicDBModule = (() => {
1737
1752
  }
1738
1753
 
1739
1754
  // src/sdk/webpush.ts
1755
+ function buildWebPushSubscriptionStorageKey(baseUrl, tenant, protocol) {
1756
+ return WEBPUSH_SUBSCRIPTION_ID_STORAGE_PREFIX + ":" + encodeURIComponent(baseUrl) + ":" + encodeURIComponent(tenant) + ":" + protocol;
1757
+ }
1740
1758
  function arrayBufferToBase64Url(buf) {
1741
1759
  const bytes = new Uint8Array(buf);
1742
1760
  let binary = "";
@@ -1797,6 +1815,12 @@ var GeonicDBModule = (() => {
1797
1815
  super();
1798
1816
  __publicField(this, "_auth");
1799
1817
  __publicField(this, "_ws");
1818
+ /**
1819
+ * In-flight `registerWebPushSubscription()` calls keyed by storageKey (#3092).
1820
+ * Concurrent callers with the same key share one Promise so a race cannot
1821
+ * POST two subscriptions before the first id is persisted.
1822
+ */
1823
+ __publicField(this, "_webPushRegisterInflight", /* @__PURE__ */ new Map());
1800
1824
  /**
1801
1825
  * Legacy callback for token refresh events.
1802
1826
  * Prefer `db.on('tokenRefresh', callback)` for new code.
@@ -2269,9 +2293,13 @@ var GeonicDBModule = (() => {
2269
2293
  * — otherwise (e.g. after the deployment rotated its VAPID keys) it
2270
2294
  * unsubscribes the stale one and re-subscribes with the current key.
2271
2295
  * Fetches the deployment's VAPID public key (unless `applicationServerKey`
2272
- * is given) and calls `PushManager.subscribe()`. Convert the result with
2273
- * `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()` and create
2274
- * the NGSI subscription with `request('POST', ...)`.
2296
+ * is given) and calls `PushManager.subscribe()`.
2297
+ *
2298
+ * This method only covers the browser Push API. To also create (or reuse)
2299
+ * the GeonicDB subscription idempotently, prefer
2300
+ * `registerWebPushSubscription()` (#3092). For a manual flow, convert the
2301
+ * result with `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()`
2302
+ * and `request('POST', ...)`.
2275
2303
  *
2276
2304
  * Browser-only — throws in environments without Service Worker + Push API
2277
2305
  * support (e.g. Node.js).
@@ -2279,11 +2307,9 @@ var GeonicDBModule = (() => {
2279
2307
  * @example
2280
2308
  * ```ts
2281
2309
  * await navigator.serviceWorker.register('/sw.js');
2282
- * const subscription = await db.subscribeWebPush();
2283
- * await db.request('POST', '/v2/subscriptions', {
2284
- * description: 'Web Push notification',
2310
+ * const { subscriptionId, created } = await db.registerWebPushSubscription({
2311
+ * protocol: 'ngsiv2',
2285
2312
  * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
2286
- * notification: { webpush: toNgsiv2WebPushNotification(subscription) },
2287
2313
  * });
2288
2314
  * ```
2289
2315
  */
@@ -2303,10 +2329,121 @@ var GeonicDBModule = (() => {
2303
2329
  }
2304
2330
  return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
2305
2331
  }
2332
+ /**
2333
+ * Idempotently register a GeonicDB subscription that delivers via Web Push (#3092).
2334
+ *
2335
+ * 1. Calls `subscribeWebPush()` for the browser PushSubscription.
2336
+ * 2. Looks up a previously saved subscription id in `localStorage`.
2337
+ * 3. If that id still exists on the broker (`GET` → 200), returns it without
2338
+ * `POST`ing. When the push endpoint changed (e.g. VAPID key rotation),
2339
+ * `PATCH`es only `notification.webpush` / `endpoint.webpush` so a new
2340
+ * subscription is not created (avoids burning the subscription quota).
2341
+ * 4. Otherwise `POST`s a new subscription and persists its id.
2342
+ *
2343
+ * `protocol` is required — there is no default. A 507 from the broker is
2344
+ * thrown as `QuotaExceededError`.
2345
+ *
2346
+ * @example
2347
+ * ```ts
2348
+ * import { QuotaExceededError } from '@geolonia/geonicdb-sdk';
2349
+ *
2350
+ * try {
2351
+ * const { subscriptionId, created } = await db.registerWebPushSubscription({
2352
+ * protocol: 'ngsiv2',
2353
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
2354
+ * });
2355
+ * } catch (err) {
2356
+ * if (err instanceof QuotaExceededError) {
2357
+ * // plan grace ceiling — delete unused subscriptions or change plan, then retry
2358
+ * }
2359
+ * throw err;
2360
+ * }
2361
+ * ```
2362
+ */
2363
+ async registerWebPushSubscription(options) {
2364
+ if (!options || options.protocol !== "ngsiv2" && options.protocol !== "ngsi-ld") {
2365
+ throw new GeonicDBError(
2366
+ "registerWebPushSubscription() requires protocol: 'ngsiv2' | 'ngsi-ld'"
2367
+ );
2368
+ }
2369
+ if (options.protocol === "ngsiv2" && !options.subject?.entities?.length) {
2370
+ throw new GeonicDBError(
2371
+ "registerWebPushSubscription({ protocol: 'ngsiv2' }) requires subject.entities"
2372
+ );
2373
+ }
2374
+ if (options.protocol === "ngsi-ld" && !options.entities?.length) {
2375
+ throw new GeonicDBError(
2376
+ "registerWebPushSubscription({ protocol: 'ngsi-ld' }) requires entities"
2377
+ );
2378
+ }
2379
+ const storageKey = options.storageKey ?? buildWebPushSubscriptionStorageKey(
2380
+ this._auth.baseUrl,
2381
+ this._auth.tenant,
2382
+ options.protocol
2383
+ );
2384
+ assertWebPushSubscriptionStorageWritable(storageKey);
2385
+ const inflight = this._webPushRegisterInflight.get(storageKey);
2386
+ if (inflight) return inflight;
2387
+ const run = this._registerWebPushSubscriptionBody(options, storageKey).finally(() => {
2388
+ if (this._webPushRegisterInflight.get(storageKey) === run) {
2389
+ this._webPushRegisterInflight.delete(storageKey);
2390
+ }
2391
+ });
2392
+ this._webPushRegisterInflight.set(storageKey, run);
2393
+ return run;
2394
+ }
2395
+ async _registerWebPushSubscriptionBody(options, storageKey) {
2396
+ const pushSubscription = await this.subscribeWebPush({
2397
+ applicationServerKey: options.applicationServerKey,
2398
+ serviceWorkerRegistration: options.serviceWorkerRegistration
2399
+ });
2400
+ const delivery = {
2401
+ ...options.ttl !== void 0 && { ttl: options.ttl },
2402
+ ...options.urgency !== void 0 && { urgency: options.urgency },
2403
+ ...options.topic !== void 0 && { topic: options.topic }
2404
+ };
2405
+ const savedId = readWebPushSubscriptionId(storageKey);
2406
+ if (savedId) {
2407
+ const collectionPath = webPushSubscriptionCollectionPath(options.protocol);
2408
+ const itemPath = `${collectionPath}/${encodeURIComponent(savedId)}`;
2409
+ try {
2410
+ const existing = await this.request("GET", itemPath);
2411
+ const storedEndpoint = readStoredWebPushEndpoint(options.protocol, existing);
2412
+ if (storedEndpoint && storedEndpoint !== pushSubscription.endpoint) {
2413
+ await this.request(
2414
+ "PATCH",
2415
+ itemPath,
2416
+ buildWebPushPatchBody(options.protocol, pushSubscription, delivery)
2417
+ );
2418
+ }
2419
+ return { subscriptionId: savedId, created: false };
2420
+ } catch (err) {
2421
+ if (!(err instanceof NotFoundError)) throw err;
2422
+ clearWebPushSubscriptionId(storageKey);
2423
+ }
2424
+ }
2425
+ const createPath = webPushSubscriptionCollectionPath(options.protocol);
2426
+ const body = buildWebPushCreateBody(options, pushSubscription, delivery);
2427
+ const res = await this.requestRaw("POST", createPath, body);
2428
+ if (!res.ok) {
2429
+ const e = await res.json().catch(() => ({}));
2430
+ throw createErrorFromResponse(res.status, e, "Request failed: " + res.status);
2431
+ }
2432
+ const subscriptionId = subscriptionIdFromLocation(
2433
+ res.headers.get("Location"),
2434
+ options.protocol
2435
+ );
2436
+ writeWebPushSubscriptionId(storageKey, subscriptionId);
2437
+ return { subscriptionId, created: true };
2438
+ }
2306
2439
  /**
2307
2440
  * Unsubscribe the current browser from Web Push notifications, if
2308
2441
  * subscribed. Returns `false` (no-op) when there is no active
2309
2442
  * subscription or the environment has no Service Worker support.
2443
+ *
2444
+ * Does not delete the GeonicDB subscription document — clear the
2445
+ * persisted id (see `buildWebPushSubscriptionStorageKey`) and
2446
+ * `DELETE` the subscription separately if needed.
2310
2447
  */
2311
2448
  async unsubscribeWebPush(registration) {
2312
2449
  if (typeof navigator === "undefined" || !navigator.serviceWorker) return false;
@@ -2345,6 +2482,126 @@ var GeonicDBModule = (() => {
2345
2482
  const qs = sp.toString();
2346
2483
  return qs ? `${path}?${qs}` : path;
2347
2484
  }
2485
+ function webPushSubscriptionCollectionPath(protocol) {
2486
+ return protocol === "ngsiv2" ? "/v2/subscriptions" : "/ngsi-ld/v1/subscriptions";
2487
+ }
2488
+ function subscriptionIdFromLocation(location, protocol) {
2489
+ if (!location) {
2490
+ throw new GeonicDBError("Missing Location header after Web Push subscription create");
2491
+ }
2492
+ const marker = protocol === "ngsiv2" ? "/v2/subscriptions/" : "/ngsi-ld/v1/subscriptions/";
2493
+ const idx = location.indexOf(marker);
2494
+ const raw = idx >= 0 ? location.slice(idx + marker.length) : location.split("/").pop() || "";
2495
+ const id = decodeURIComponent(raw.split("?")[0].split("#")[0]);
2496
+ if (!id) {
2497
+ throw new GeonicDBError("Could not parse subscription id from Location: " + location);
2498
+ }
2499
+ return id;
2500
+ }
2501
+ function readStoredWebPushEndpoint(protocol, body) {
2502
+ const notification = body.notification;
2503
+ if (!notification) return void 0;
2504
+ if (protocol === "ngsiv2") {
2505
+ const webpush = notification.webpush;
2506
+ return webpush?.url;
2507
+ }
2508
+ const endpoint = notification.endpoint;
2509
+ return endpoint?.uri;
2510
+ }
2511
+ function buildWebPushCreateBody(options, pushSubscription, delivery) {
2512
+ const description = options.description ?? "Web Push notification";
2513
+ if (options.protocol === "ngsiv2") {
2514
+ return {
2515
+ description,
2516
+ subject: options.subject,
2517
+ notification: {
2518
+ webpush: toNgsiv2WebPushNotification(pushSubscription, delivery)
2519
+ }
2520
+ };
2521
+ }
2522
+ const { uri, webpush } = toNgsiLdWebPushEndpoint(pushSubscription, delivery);
2523
+ return {
2524
+ type: "Subscription",
2525
+ description,
2526
+ entities: options.entities,
2527
+ ...options.watchedAttributes && { watchedAttributes: options.watchedAttributes },
2528
+ ...options.q && { q: options.q },
2529
+ notification: {
2530
+ endpoint: { uri, accept: "application/json", protocol: "webpush", webpush }
2531
+ }
2532
+ };
2533
+ }
2534
+ function buildWebPushPatchBody(protocol, pushSubscription, delivery) {
2535
+ if (protocol === "ngsiv2") {
2536
+ return {
2537
+ notification: {
2538
+ webpush: toNgsiv2WebPushNotification(pushSubscription, delivery)
2539
+ }
2540
+ };
2541
+ }
2542
+ const { uri, webpush } = toNgsiLdWebPushEndpoint(pushSubscription, delivery);
2543
+ return {
2544
+ notification: {
2545
+ endpoint: { uri, accept: "application/json", protocol: "webpush", webpush }
2546
+ }
2547
+ };
2548
+ }
2549
+ function readWebPushSubscriptionId(storageKey) {
2550
+ try {
2551
+ if (typeof localStorage === "undefined") return null;
2552
+ return localStorage.getItem(storageKey);
2553
+ } catch {
2554
+ return null;
2555
+ }
2556
+ }
2557
+ function writeWebPushSubscriptionId(storageKey, subscriptionId) {
2558
+ try {
2559
+ if (typeof localStorage === "undefined") {
2560
+ throw new GeonicDBError(
2561
+ "registerWebPushSubscription() requires localStorage for idempotent registration"
2562
+ );
2563
+ }
2564
+ localStorage.setItem(storageKey, subscriptionId);
2565
+ } catch (err) {
2566
+ if (err instanceof GeonicDBError) throw err;
2567
+ throw new GeonicDBError(
2568
+ "registerWebPushSubscription() could not persist subscription id to localStorage"
2569
+ );
2570
+ }
2571
+ }
2572
+ function assertWebPushSubscriptionStorageWritable(storageKey) {
2573
+ const probeKey = storageKey + "::__writable__";
2574
+ try {
2575
+ if (typeof localStorage === "undefined") {
2576
+ throw new GeonicDBError(
2577
+ "registerWebPushSubscription() requires localStorage for idempotent registration"
2578
+ );
2579
+ }
2580
+ localStorage.setItem(probeKey, "1");
2581
+ if (localStorage.getItem(probeKey) !== "1") {
2582
+ throw new GeonicDBError(
2583
+ "registerWebPushSubscription() could not persist subscription id to localStorage"
2584
+ );
2585
+ }
2586
+ localStorage.removeItem(probeKey);
2587
+ } catch (err) {
2588
+ try {
2589
+ if (typeof localStorage !== "undefined") localStorage.removeItem(probeKey);
2590
+ } catch {
2591
+ }
2592
+ if (err instanceof GeonicDBError) throw err;
2593
+ throw new GeonicDBError(
2594
+ "registerWebPushSubscription() could not persist subscription id to localStorage"
2595
+ );
2596
+ }
2597
+ }
2598
+ function clearWebPushSubscriptionId(storageKey) {
2599
+ try {
2600
+ if (typeof localStorage === "undefined") return;
2601
+ localStorage.removeItem(storageKey);
2602
+ } catch {
2603
+ }
2604
+ }
2348
2605
  return __toCommonJS(index_exports);
2349
2606
  })();
2350
2607
  if(typeof window!=="undefined"){window.GeonicDB=GeonicDBModule.GeonicDB||GeonicDBModule.default}