@geolonia/geonicdb-sdk 0.2.1 → 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 CHANGED
@@ -153,7 +153,8 @@ const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');
153
153
  db.subscribe({ entityTypes: ['Room'] });
154
154
 
155
155
  db.on('entityUpdated', (event) => {
156
- console.log(`${event.entityId} updated:`, event.data);
156
+ // event.entity contains the complete NGSI-LD entity ({ id, type, ...attributes })
157
+ console.log(`${event.entityId} updated:`, event.entity);
157
158
  });
158
159
 
159
160
  db.on('error', (err) => console.error(err));
@@ -175,6 +176,57 @@ await db.connect();
175
176
  | `reconnecting` | `{ attempt, delay }` | Auto-reconnect starting |
176
177
  | `error` | `Error` | Error occurred |
177
178
  | `tokenRefresh` | `RefreshedCredentials` | Bearer token was refreshed |
179
+ | `cacheHit` | `CacheEvent` | Request served from in-memory cache (304 received) |
180
+ | `cacheMiss` | `CacheEvent` | Cache miss — fresh response fetched from origin |
181
+ | `cacheInvalidated` | `CacheEvent` | Cache entry dropped (e.g. after a WebSocket entity event) |
182
+
183
+ ## Client-side Cache & Polling
184
+
185
+ The SDK includes an in-memory cache that handles `ETag` / `If-None-Match`
186
+ negotiation transparently. Subsequent reads of an unchanged resource are
187
+ served as `304 Not Modified` — the SDK presents them to your code as a normal
188
+ `200` response with the cached body.
189
+
190
+ ```typescript
191
+ // First call: fresh fetch, ETag stored
192
+ const rooms1 = await db.getEntities({ type: 'Room' });
193
+
194
+ // Second call: server returns 304, SDK returns the cached body
195
+ const rooms2 = await db.getEntities({ type: 'Room' });
196
+
197
+ // Concurrent calls to the same path are deduplicated:
198
+ const [a, b, c] = await Promise.all([
199
+ db.getEntities({ type: 'Room' }),
200
+ db.getEntities({ type: 'Room' }),
201
+ db.getEntities({ type: 'Room' }),
202
+ ]); // → only one HTTP request hits the network
203
+ ```
204
+
205
+ Cache is enabled by default; pass `cache: false` to opt out.
206
+
207
+ ### Polling
208
+
209
+ `db.poll(params, options)` repeats `getEntities()` at an interval and reports
210
+ back whether the data actually changed. Internally it leverages the cache's
211
+ ETag negotiation, so unchanged ticks transfer almost no bytes.
212
+
213
+ ```typescript
214
+ const handle = db.poll({ type: 'Room' }, {
215
+ interval: 5000,
216
+ onData: (rooms) => render(rooms),
217
+ onNoChange: () => {}, // server returned 304
218
+ onError: (err) => console.error(err),
219
+ });
220
+
221
+ // Stop later
222
+ handle.stop();
223
+ ```
224
+
225
+ ### WebSocket-driven cache invalidation
226
+
227
+ When the SDK receives `entityCreated` / `entityUpdated` / `entityDeleted`
228
+ WebSocket events, it drops affected cache entries automatically. The next
229
+ read re-validates with the server.
178
230
 
179
231
  ## API Reference
180
232
 
@@ -192,6 +244,9 @@ await db.connect();
192
244
  | `tenant` | `string` | No | Tenant name (or use `data-tenant` attribute on script tag) |
193
245
  | `baseUrl` | `string` | No | API base URL (auto-detected from script `src` if omitted) |
194
246
  | `wsEndpoint` | `string` | No | WebSocket endpoint URL (auto-detected from `baseUrl` if omitted) |
247
+ | `debug` | `boolean` | No | Enable debug logging to console (default: `false`) |
248
+ | `cache` | `boolean` | No | Enable in-memory cache with ETag/304 + request dedup (default: `true`) |
249
+ | `cacheMaxEntries` | `number` | No | LRU cache capacity when `cache` is enabled (default: `1000`) |
195
250
 
196
251
  ### Authentication
197
252
 
@@ -276,6 +331,35 @@ await db.connect();
276
331
  | `request(method, path, body?)` | `string, string, unknown` | `Promise<unknown>` | Authenticated request with automatic JSON parsing |
277
332
  | `requestRaw(method, path, body?)` | `string, string, unknown` | `Promise<Response>` | Authenticated request returning raw `Response` (for accessing headers) |
278
333
 
334
+ ### Cache & Polling
335
+
336
+ | Method | Parameters | Returns | Description |
337
+ |--------|-----------|---------|-------------|
338
+ | `poll(params, options)` | `GetEntitiesParams \| undefined, PollOptions<T>` | `PollHandle` | ETag-based polling — fires `onData` only when the data actually changes. `options` is **required** (`onData` must be specified to receive data) |
339
+ | `clearCache()` | — | `void` | Drop all cached responses |
340
+
341
+ #### `PollOptions<T>`
342
+
343
+ | Property | Type | Description |
344
+ |----------|------|-------------|
345
+ | `interval` | `number` | Polling interval in milliseconds (default: `5000`) |
346
+ | `onData` | `(data: T) => void` | Called whenever the server returns fresh data (200) |
347
+ | `onNoChange` | `() => void` | Called when the server returns 304 Not Modified |
348
+ | `onError` | `(err: Error) => void` | Called on any fetch/parse failure |
349
+
350
+ #### `PollHandle`
351
+
352
+ | Method | Parameters | Returns | Description |
353
+ |--------|-----------|---------|-------------|
354
+ | `stop()` | — | `void` | Stop the polling timer |
355
+
356
+ #### `CacheEvent`
357
+
358
+ | Property | Type | Description |
359
+ |----------|------|-------------|
360
+ | `key` | `string` | Cache key (`METHOD:path`) |
361
+ | `path` | `string` | Request path |
362
+
279
363
  ### WebSocket
280
364
 
281
365
  | Method | Parameters | Returns | Description |
@@ -300,6 +384,39 @@ await db.connect();
300
384
  | `on(event, listener)` | `string, Function` | `this` | Register an event listener |
301
385
  | `off(event, listener)` | `string, Function` | `this` | Remove an event listener |
302
386
 
387
+ ## Error Handling
388
+
389
+ All async methods throw typed errors extending `GeonicDBError`. Use `instanceof` to distinguish error types.
390
+
391
+ ```typescript
392
+ import { GeonicDBError, AuthenticationError, NotFoundError } from '@geolonia/geonicdb-sdk';
393
+
394
+ try {
395
+ await db.getEntity('urn:ngsi-ld:Room:404');
396
+ } catch (err) {
397
+ if (err instanceof AuthenticationError) {
398
+ // 401 — re-login needed
399
+ } else if (err instanceof NotFoundError) {
400
+ // 404 — entity not found
401
+ } else if (err instanceof GeonicDBError) {
402
+ console.error(err.message, err.statusCode);
403
+ } else {
404
+ console.error(err);
405
+ }
406
+ }
407
+ ```
408
+
409
+ | Class | Status | Description |
410
+ |-------|--------|-------------|
411
+ | `GeonicDBError` | — | Base class (`statusCode` property) |
412
+ | `AuthenticationError` | 401 | Invalid credentials or expired token |
413
+ | `AuthorizationError` | 403 | Insufficient permissions |
414
+ | `NotFoundError` | 404 | Resource not found |
415
+ | `ConflictError` | 409 | Entity already exists |
416
+ | `ValidationError` | 422 | Bad request payload |
417
+ | `RateLimitError` | 429 | Rate limited (`retryAfter` property) |
418
+ | `NetworkError` | — | Fetch failure, DNS error, timeout |
419
+
303
420
  ## License
304
421
 
305
422
  MIT