@geolonia/geonicdb-sdk 0.3.0 → 0.4.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 +82 -0
- package/geonicdb.cjs +323 -18
- package/geonicdb.cjs.map +4 -4
- package/geonicdb.iife.js +323 -18
- package/geonicdb.iife.js.map +4 -4
- package/geonicdb.mjs +323 -18
- package/geonicdb.mjs.map +4 -4
- package/package.json +1 -1
package/geonicdb.mjs
CHANGED
|
@@ -147,6 +147,8 @@ var RECONNECT_MAX_ATTEMPTS = 10;
|
|
|
147
147
|
var RECONNECT_BASE_MS = 1e3;
|
|
148
148
|
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
149
149
|
var SUB_PROTOCOL = "access_token";
|
|
150
|
+
var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
|
|
151
|
+
var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
|
|
150
152
|
|
|
151
153
|
// src/sdk/errors.ts
|
|
152
154
|
var GeonicDBError = class extends Error {
|
|
@@ -223,6 +225,93 @@ function createErrorFromResponse(status, body, fallbackMessage) {
|
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
// src/sdk/cache.ts
|
|
229
|
+
var SdkCache = class {
|
|
230
|
+
constructor(maxEntries = SDK_CACHE_MAX_ENTRIES_DEFAULT) {
|
|
231
|
+
__publicField(this, "_store", /* @__PURE__ */ new Map());
|
|
232
|
+
__publicField(this, "_maxEntries");
|
|
233
|
+
if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
|
|
234
|
+
throw new Error(`SdkCache maxEntries must be a positive integer, got ${String(maxEntries)}`);
|
|
235
|
+
}
|
|
236
|
+
this._maxEntries = maxEntries;
|
|
237
|
+
}
|
|
238
|
+
/** Build a stable cache key. Method is upper-cased so casing differences do not produce duplicate entries. */
|
|
239
|
+
static keyFor(method, path) {
|
|
240
|
+
return `${method.toUpperCase()}:${path}`;
|
|
241
|
+
}
|
|
242
|
+
/** Returns the entry and bumps it to most-recent position, or `undefined`. */
|
|
243
|
+
get(key) {
|
|
244
|
+
const entry = this._store.get(key);
|
|
245
|
+
if (!entry) return void 0;
|
|
246
|
+
this._store.delete(key);
|
|
247
|
+
this._store.set(key, entry);
|
|
248
|
+
return entry;
|
|
249
|
+
}
|
|
250
|
+
/** Insert or replace an entry. Evicts the LRU entry when over capacity. */
|
|
251
|
+
set(key, entry) {
|
|
252
|
+
if (this._store.has(key)) {
|
|
253
|
+
this._store.delete(key);
|
|
254
|
+
}
|
|
255
|
+
this._store.set(key, entry);
|
|
256
|
+
while (this._store.size > this._maxEntries) {
|
|
257
|
+
const oldest = this._store.keys().next().value;
|
|
258
|
+
if (oldest === void 0) break;
|
|
259
|
+
this._store.delete(oldest);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Delete a specific entry. Returns whether anything was removed. */
|
|
263
|
+
delete(key) {
|
|
264
|
+
return this._store.delete(key);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Delete every entry whose key matches a predicate. Returns the array of
|
|
268
|
+
* removed keys so callers can emit `cacheInvalidated` events.
|
|
269
|
+
*/
|
|
270
|
+
deleteWhere(predicate) {
|
|
271
|
+
const removed = [];
|
|
272
|
+
for (const [key, entry] of this._store) {
|
|
273
|
+
if (predicate(key, entry)) {
|
|
274
|
+
this._store.delete(key);
|
|
275
|
+
removed.push(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return removed;
|
|
279
|
+
}
|
|
280
|
+
/** Drop everything. */
|
|
281
|
+
clear() {
|
|
282
|
+
this._store.clear();
|
|
283
|
+
}
|
|
284
|
+
/** Current entry count. Useful for tests / metrics. */
|
|
285
|
+
size() {
|
|
286
|
+
return this._store.size;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var HEADERS_TO_PERSIST = [
|
|
290
|
+
"content-type",
|
|
291
|
+
"etag",
|
|
292
|
+
"last-modified",
|
|
293
|
+
"cache-control",
|
|
294
|
+
"vary",
|
|
295
|
+
"link",
|
|
296
|
+
"ngsild-results-count",
|
|
297
|
+
"fiware-total-count",
|
|
298
|
+
"x-total-count",
|
|
299
|
+
"ngsild-next",
|
|
300
|
+
"fiware-next-token"
|
|
301
|
+
];
|
|
302
|
+
function snapshotHeaders(headers) {
|
|
303
|
+
const out = {};
|
|
304
|
+
for (const name of HEADERS_TO_PERSIST) {
|
|
305
|
+
const value = headers.get(name);
|
|
306
|
+
if (value !== null) out[name] = value;
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
function isCacheableMethod(method) {
|
|
311
|
+
const upper = method.toUpperCase();
|
|
312
|
+
return upper === "GET" || upper === "HEAD";
|
|
313
|
+
}
|
|
314
|
+
|
|
226
315
|
// src/sdk/auth.ts
|
|
227
316
|
var _AuthManager = class _AuthManager {
|
|
228
317
|
constructor(baseUrl, apiKey, tenant, debug = false) {
|
|
@@ -240,6 +329,16 @@ var _AuthManager = class _AuthManager {
|
|
|
240
329
|
__publicField(this, "_tokenPromise", null);
|
|
241
330
|
/** Callback to emit tokenRefresh events. Set by GeonicDB class. */
|
|
242
331
|
__publicField(this, "onTokenRefresh", null);
|
|
332
|
+
/**
|
|
333
|
+
* SDK-level cache (#991 Phase A). When set, cacheable GET requests are
|
|
334
|
+
* served via ETag/304 with automatic If-None-Match negotiation, and
|
|
335
|
+
* concurrent requests to the same path are deduplicated.
|
|
336
|
+
*/
|
|
337
|
+
__publicField(this, "_cache", null);
|
|
338
|
+
/** Emitter for cacheHit / cacheMiss / cacheInvalidated events. */
|
|
339
|
+
__publicField(this, "_emitCacheEvent", null);
|
|
340
|
+
/** In-flight request map keyed by `${METHOD}:${path}` for request deduplication. */
|
|
341
|
+
__publicField(this, "_inFlight", /* @__PURE__ */ new Map());
|
|
243
342
|
this._baseUrl = baseUrl;
|
|
244
343
|
this._apiKey = apiKey;
|
|
245
344
|
this._tenant = tenant;
|
|
@@ -255,6 +354,22 @@ var _AuthManager = class _AuthManager {
|
|
|
255
354
|
_log(...args) {
|
|
256
355
|
if (this._debug) console.log("[GeonicDB]", ...args);
|
|
257
356
|
}
|
|
357
|
+
/** Wire an SdkCache instance (called by GeonicDB constructor when caching is enabled). */
|
|
358
|
+
setCache(cache) {
|
|
359
|
+
this._cache = cache;
|
|
360
|
+
}
|
|
361
|
+
/** Provide the cache event emitter (forwarded to the GeonicDB EventEmitter). */
|
|
362
|
+
setCacheEventEmitter(emitter) {
|
|
363
|
+
this._emitCacheEvent = emitter;
|
|
364
|
+
}
|
|
365
|
+
/** Expose the cache for invalidation (e.g. WebSocket entity events). */
|
|
366
|
+
getCache() {
|
|
367
|
+
return this._cache;
|
|
368
|
+
}
|
|
369
|
+
/** Emit a cache event if a listener is wired. */
|
|
370
|
+
emitCacheEvent(name, payload) {
|
|
371
|
+
this._emitCacheEvent?.(name, payload);
|
|
372
|
+
}
|
|
258
373
|
/** Login with email and password (Bearer JWT). */
|
|
259
374
|
async login(email, password) {
|
|
260
375
|
this._log("login", email);
|
|
@@ -278,6 +393,7 @@ var _AuthManager = class _AuthManager {
|
|
|
278
393
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
279
394
|
this._tokenType = "Bearer";
|
|
280
395
|
this._refreshToken = data.refreshToken;
|
|
396
|
+
this._invalidateAuthScopedState();
|
|
281
397
|
return data;
|
|
282
398
|
}
|
|
283
399
|
/**
|
|
@@ -292,6 +408,7 @@ var _AuthManager = class _AuthManager {
|
|
|
292
408
|
this._tokenExpiry = opts.expiresIn != null ? Date.now() + (opts.expiresIn - 60) * 1e3 : Date.now() + DEFAULT_TOKEN_TTL_SEC * 1e3;
|
|
293
409
|
this._refreshToken = opts.refreshToken || null;
|
|
294
410
|
this._tokenPromise = null;
|
|
411
|
+
this._invalidateAuthScopedState();
|
|
295
412
|
}
|
|
296
413
|
/** Clear all credentials. */
|
|
297
414
|
logout() {
|
|
@@ -299,6 +416,17 @@ var _AuthManager = class _AuthManager {
|
|
|
299
416
|
this._tokenExpiry = 0;
|
|
300
417
|
this._refreshToken = null;
|
|
301
418
|
this._tokenPromise = null;
|
|
419
|
+
this._invalidateAuthScopedState();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Drop everything that may have been associated with the previous auth
|
|
423
|
+
* context. Called on `login()` (after token assignment), `setCredentials()`,
|
|
424
|
+
* and `logout()`. Without this, a cached body or in-flight Response from
|
|
425
|
+
* user A could be returned to user B after a credentials swap.
|
|
426
|
+
*/
|
|
427
|
+
_invalidateAuthScopedState() {
|
|
428
|
+
this._cache?.clear();
|
|
429
|
+
this._inFlight.clear();
|
|
302
430
|
}
|
|
303
431
|
/** Ensure a valid token is available, refreshing or acquiring as needed. */
|
|
304
432
|
async ensureToken() {
|
|
@@ -418,15 +546,89 @@ var _AuthManager = class _AuthManager {
|
|
|
418
546
|
}
|
|
419
547
|
/**
|
|
420
548
|
* Make an authenticated HTTP request with automatic token refresh and DPoP.
|
|
549
|
+
*
|
|
550
|
+
* When the SDK cache is enabled and the request is cacheable
|
|
551
|
+
* (GET / HEAD without a body), this method:
|
|
552
|
+
* - Sends `If-None-Match` / `If-Modified-Since` derived from a previously
|
|
553
|
+
* cached entry, if any.
|
|
554
|
+
* - Returns the cached body wrapped in a synthesized `200` Response when
|
|
555
|
+
* the server replies `304 Not Modified`.
|
|
556
|
+
* - Persists fresh `200` responses (with their ETag/Last-Modified) into
|
|
557
|
+
* the cache.
|
|
558
|
+
* - Deduplicates concurrent in-flight requests to the same path.
|
|
421
559
|
*/
|
|
422
560
|
async request(method, path, body) {
|
|
423
561
|
this._log(method, path);
|
|
562
|
+
if (!this._cache || !isCacheableMethod(method) || body !== void 0) {
|
|
563
|
+
const token = await this.ensureToken();
|
|
564
|
+
const res = await this._doAuthenticatedRequest(method, path, body, token);
|
|
565
|
+
this._log(method, path, "\u2192", res.status);
|
|
566
|
+
return res;
|
|
567
|
+
}
|
|
568
|
+
const key = SdkCache.keyFor(method, path);
|
|
569
|
+
const inFlight = this._inFlight.get(key);
|
|
570
|
+
if (inFlight) {
|
|
571
|
+
return inFlight.then((res) => res.clone());
|
|
572
|
+
}
|
|
573
|
+
const promise = this._cachedRequest(method, path, key);
|
|
574
|
+
this._inFlight.set(key, promise);
|
|
575
|
+
try {
|
|
576
|
+
return await promise;
|
|
577
|
+
} finally {
|
|
578
|
+
this._inFlight.delete(key);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async _cachedRequest(method, path, key) {
|
|
582
|
+
const cache = this._cache;
|
|
583
|
+
if (!cache) {
|
|
584
|
+
const token2 = await this.ensureToken();
|
|
585
|
+
return this._doAuthenticatedRequest(method, path, void 0, token2);
|
|
586
|
+
}
|
|
587
|
+
const cached = cache.get(key);
|
|
588
|
+
const conditional = {};
|
|
589
|
+
if (cached?.etag) conditional["If-None-Match"] = cached.etag;
|
|
590
|
+
if (cached?.lastModified) conditional["If-Modified-Since"] = cached.lastModified;
|
|
424
591
|
const token = await this.ensureToken();
|
|
425
|
-
const res = await this._doAuthenticatedRequest(method, path,
|
|
592
|
+
const res = await this._doAuthenticatedRequest(method, path, void 0, token, 0, conditional);
|
|
426
593
|
this._log(method, path, "\u2192", res.status);
|
|
594
|
+
if (res.status === 304 && cached) {
|
|
595
|
+
const refreshedHeaders = { ...cached.headers, ...snapshotHeaders(res.headers) };
|
|
596
|
+
const refreshedEtag = res.headers.get("etag") ?? cached.etag;
|
|
597
|
+
const refreshedLastModified = res.headers.get("last-modified") ?? cached.lastModified;
|
|
598
|
+
cache.set(key, {
|
|
599
|
+
...cached,
|
|
600
|
+
etag: refreshedEtag,
|
|
601
|
+
lastModified: refreshedLastModified,
|
|
602
|
+
headers: refreshedHeaders,
|
|
603
|
+
cachedAt: Date.now()
|
|
604
|
+
});
|
|
605
|
+
this.emitCacheEvent("cacheHit", { key, path });
|
|
606
|
+
const body = typeof cached.data === "string" ? cached.data : JSON.stringify(cached.data);
|
|
607
|
+
return new Response(body, { status: 200, headers: refreshedHeaders });
|
|
608
|
+
}
|
|
609
|
+
if (res.status === 200) {
|
|
610
|
+
const etag = res.headers.get("etag") ?? void 0;
|
|
611
|
+
const lastModified = res.headers.get("last-modified") ?? void 0;
|
|
612
|
+
if (etag || lastModified) {
|
|
613
|
+
const text = await res.clone().text();
|
|
614
|
+
let data = text;
|
|
615
|
+
try {
|
|
616
|
+
data = JSON.parse(text);
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
cache.set(key, {
|
|
620
|
+
etag,
|
|
621
|
+
lastModified,
|
|
622
|
+
data,
|
|
623
|
+
headers: snapshotHeaders(res.headers),
|
|
624
|
+
cachedAt: Date.now()
|
|
625
|
+
});
|
|
626
|
+
this.emitCacheEvent("cacheMiss", { key, path });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
427
629
|
return res;
|
|
428
630
|
}
|
|
429
|
-
async _doAuthenticatedRequest(method, path, body, token, retryCount = 0) {
|
|
631
|
+
async _doAuthenticatedRequest(method, path, body, token, retryCount = 0, extraHeaders = {}) {
|
|
430
632
|
const url = this._baseUrl + path;
|
|
431
633
|
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
|
|
432
634
|
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
@@ -442,7 +644,8 @@ var _AuthManager = class _AuthManager {
|
|
|
442
644
|
const headers = {
|
|
443
645
|
Authorization: (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken,
|
|
444
646
|
"Content-Type": "application/ld+json",
|
|
445
|
-
Accept: "application/ld+json"
|
|
647
|
+
Accept: "application/ld+json",
|
|
648
|
+
...extraHeaders
|
|
446
649
|
};
|
|
447
650
|
if (dpopProof) headers["DPoP"] = dpopProof;
|
|
448
651
|
if (this._tenant) headers["Fiware-Service"] = this._tenant;
|
|
@@ -478,7 +681,7 @@ var _AuthManager = class _AuthManager {
|
|
|
478
681
|
if (retryNonce) this._dpopNonce = retryNonce;
|
|
479
682
|
const delay = parseInt(res.headers.get("Retry-After") || "1", 10) * 1e3;
|
|
480
683
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
481
|
-
return this._doAuthenticatedRequest(method, path, body, currentToken, retryCount + 1);
|
|
684
|
+
return this._doAuthenticatedRequest(method, path, body, currentToken, retryCount + 1, extraHeaders);
|
|
482
685
|
}
|
|
483
686
|
return res;
|
|
484
687
|
}
|
|
@@ -754,14 +957,50 @@ var GeonicDB = class extends EventEmitter {
|
|
|
754
957
|
this.onTokenRefresh?.(creds);
|
|
755
958
|
this.emit("tokenRefresh", creds);
|
|
756
959
|
};
|
|
960
|
+
if (opts.cache !== false) {
|
|
961
|
+
const cache = new SdkCache(opts.cacheMaxEntries ?? SDK_CACHE_MAX_ENTRIES_DEFAULT);
|
|
962
|
+
this._auth.setCache(cache);
|
|
963
|
+
this._auth.setCacheEventEmitter((name, payload) => this.emit(name, payload));
|
|
964
|
+
}
|
|
757
965
|
this._ws = new WebSocketManager(
|
|
758
966
|
this._auth,
|
|
759
967
|
baseUrl,
|
|
760
968
|
tenant,
|
|
761
|
-
(event, data) =>
|
|
969
|
+
(event, data) => {
|
|
970
|
+
this.emit(event, data);
|
|
971
|
+
if (event === "entityCreated" || event === "entityUpdated" || event === "entityDeleted") {
|
|
972
|
+
this._invalidateCacheForEntityEvent(data);
|
|
973
|
+
}
|
|
974
|
+
},
|
|
762
975
|
opts.wsEndpoint
|
|
763
976
|
);
|
|
764
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* Drop cache entries that may have been affected by an entity change.
|
|
980
|
+
*
|
|
981
|
+
* The SDK does not know which lists exactly contain the entity, so the
|
|
982
|
+
* conservative strategy is to drop all NGSI-LD / NGSIv2 entity-related
|
|
983
|
+
* paths. ETag re-validation on the next read brings the data back in one
|
|
984
|
+
* round trip with `If-None-Match`, so this remains efficient even when
|
|
985
|
+
* over-invalidating.
|
|
986
|
+
*/
|
|
987
|
+
_invalidateCacheForEntityEvent(event) {
|
|
988
|
+
const cache = this._auth.getCache();
|
|
989
|
+
if (!cache) return;
|
|
990
|
+
const entityId = event?.entityId;
|
|
991
|
+
const removed = cache.deleteWhere((key) => {
|
|
992
|
+
if (key.includes("/ngsi-ld/v1/entities") || key.includes("/v2/entities")) return true;
|
|
993
|
+
if (entityId && key.includes(entityId)) return true;
|
|
994
|
+
return false;
|
|
995
|
+
});
|
|
996
|
+
for (const key of removed) {
|
|
997
|
+
this._auth.emitCacheEvent("cacheInvalidated", { key, path: key.split(":").slice(1).join(":") });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/** Drop every cached response. Useful in tests and on manual auth changes. */
|
|
1001
|
+
clearCache() {
|
|
1002
|
+
this._auth.getCache()?.clear();
|
|
1003
|
+
}
|
|
765
1004
|
// --- Authentication ---
|
|
766
1005
|
/** Login with email and password (Bearer JWT). */
|
|
767
1006
|
async login(email, password) {
|
|
@@ -804,25 +1043,82 @@ var GeonicDB = class extends EventEmitter {
|
|
|
804
1043
|
}
|
|
805
1044
|
/** Query entities with optional filters. */
|
|
806
1045
|
async getEntities(params) {
|
|
807
|
-
|
|
808
|
-
if (params) {
|
|
809
|
-
const parts = [];
|
|
810
|
-
if (params.type) parts.push("type=" + encodeURIComponent(params.type));
|
|
811
|
-
if (params.limit != null) parts.push("limit=" + params.limit);
|
|
812
|
-
if (params.offset != null) parts.push("offset=" + params.offset);
|
|
813
|
-
if (params.q) parts.push("q=" + encodeURIComponent(params.q));
|
|
814
|
-
if (parts.length) qs = "?" + parts.join("&");
|
|
815
|
-
}
|
|
816
|
-
const res = await this._auth.request(
|
|
817
|
-
"GET",
|
|
818
|
-
"/ngsi-ld/v1/entities" + qs
|
|
819
|
-
);
|
|
1046
|
+
const res = await this._auth.request("GET", buildEntitiesPath(params));
|
|
820
1047
|
if (!res.ok) {
|
|
821
1048
|
const e = await res.json().catch(() => ({}));
|
|
822
1049
|
throw createErrorFromResponse(res.status, e, "Query failed");
|
|
823
1050
|
}
|
|
824
1051
|
return await res.json();
|
|
825
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Poll for entity list changes using ETag-based revalidation (#991 Phase A).
|
|
1055
|
+
*
|
|
1056
|
+
* The handle's `stop()` ends the loop. Each tick performs a normal
|
|
1057
|
+
* `getEntities()` call; the SDK's cache layer issues `If-None-Match`
|
|
1058
|
+
* automatically. The poll detects "no change" by comparing the previous
|
|
1059
|
+
* ETag with the response's ETag (the cache exposes the cached ETag on 304
|
|
1060
|
+
* replays as well, so the comparison stays valid in both 200 and 304
|
|
1061
|
+
* paths).
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```typescript
|
|
1065
|
+
* const handle = db.poll({ type: 'Room' }, {
|
|
1066
|
+
* interval: 5000,
|
|
1067
|
+
* onData: (rooms) => render(rooms),
|
|
1068
|
+
* onNoChange: () => {},
|
|
1069
|
+
* });
|
|
1070
|
+
* // ...later
|
|
1071
|
+
* handle.stop();
|
|
1072
|
+
* ```
|
|
1073
|
+
*/
|
|
1074
|
+
poll(params, options) {
|
|
1075
|
+
const interval = options.interval ?? SDK_POLL_INTERVAL_MS_DEFAULT;
|
|
1076
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
1077
|
+
throw new Error(`poll interval must be a positive number (got ${String(interval)})`);
|
|
1078
|
+
}
|
|
1079
|
+
let stopped = false;
|
|
1080
|
+
let timer = null;
|
|
1081
|
+
let prevEtag;
|
|
1082
|
+
let prevPath;
|
|
1083
|
+
const buildPath = () => buildEntitiesPath(params);
|
|
1084
|
+
const tick = async () => {
|
|
1085
|
+
if (stopped) return;
|
|
1086
|
+
try {
|
|
1087
|
+
const path = prevPath ?? (prevPath = buildPath());
|
|
1088
|
+
const res = await this._auth.request("GET", path);
|
|
1089
|
+
if (stopped) return;
|
|
1090
|
+
const etag = res.headers.get("etag") ?? void 0;
|
|
1091
|
+
if (prevEtag !== void 0 && etag !== void 0 && prevEtag === etag) {
|
|
1092
|
+
options.onNoChange?.();
|
|
1093
|
+
} else {
|
|
1094
|
+
if (!res.ok) {
|
|
1095
|
+
throw createErrorFromResponse(res.status, {}, "Poll request failed");
|
|
1096
|
+
}
|
|
1097
|
+
const data = await res.json();
|
|
1098
|
+
prevEtag = etag;
|
|
1099
|
+
options.onData?.(data);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
options.onError?.(err);
|
|
1103
|
+
} finally {
|
|
1104
|
+
if (!stopped) {
|
|
1105
|
+
timer = setTimeout(() => {
|
|
1106
|
+
void tick();
|
|
1107
|
+
}, interval);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
void tick();
|
|
1112
|
+
return {
|
|
1113
|
+
stop() {
|
|
1114
|
+
stopped = true;
|
|
1115
|
+
if (timer !== null) {
|
|
1116
|
+
clearTimeout(timer);
|
|
1117
|
+
timer = null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
826
1122
|
/** Count entities matching the given filters. */
|
|
827
1123
|
async count(params) {
|
|
828
1124
|
const parts = ["count=true", "limit=0"];
|
|
@@ -1021,6 +1317,15 @@ var index_default = GeonicDB;
|
|
|
1021
1317
|
if (typeof window !== "undefined") {
|
|
1022
1318
|
window.GeonicDB = GeonicDB;
|
|
1023
1319
|
}
|
|
1320
|
+
function buildEntitiesPath(params) {
|
|
1321
|
+
if (!params) return "/ngsi-ld/v1/entities";
|
|
1322
|
+
const parts = [];
|
|
1323
|
+
if (params.type) parts.push("type=" + encodeURIComponent(params.type));
|
|
1324
|
+
if (params.limit != null) parts.push("limit=" + params.limit);
|
|
1325
|
+
if (params.offset != null) parts.push("offset=" + params.offset);
|
|
1326
|
+
if (params.q) parts.push("q=" + encodeURIComponent(params.q));
|
|
1327
|
+
return parts.length > 0 ? "/ngsi-ld/v1/entities?" + parts.join("&") : "/ngsi-ld/v1/entities";
|
|
1328
|
+
}
|
|
1024
1329
|
export {
|
|
1025
1330
|
AuthenticationError,
|
|
1026
1331
|
AuthorizationError,
|