angular-odata 0.163.0 → 0.164.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.
@@ -1,6 +1,6 @@
1
1
  import { HttpHeaders, HttpParams, HttpResponse, HttpErrorResponse, HttpEventType, provideHttpClient, HttpClient } from '@angular/common/http';
2
- import { Subject, map as map$1, EMPTY, throwError, Observable, forkJoin, of, NEVER, firstValueFrom } from 'rxjs';
3
- import { map, expand, reduce, finalize, defaultIfEmpty, switchMap, catchError, startWith, tap } from 'rxjs/operators';
2
+ import { Subject, defer, of, from, ReplaySubject, Subscription, race, EMPTY, throwError, Observable, forkJoin, NEVER, firstValueFrom, map as map$1 } from 'rxjs';
3
+ import { map, tap, catchError, switchMap, finalize, expand, reduce, defaultIfEmpty, startWith } from 'rxjs/operators';
4
4
  import * as i0 from '@angular/core';
5
5
  import { EventEmitter, Injectable, InjectionToken, provideAppInitializer, inject, makeEnvironmentProviders, NgModule } from '@angular/core';
6
6
  import { CommonModule } from '@angular/common';
@@ -195,6 +195,7 @@ const ODATA_ALIAS_PREFIX = '@';
195
195
  const NEWLINE = '\r\n';
196
196
  const NEWLINE_REGEXP = /\r?\n/;
197
197
  const CACHE_KEY_SEPARATOR = ':';
198
+ const CACHE_REPRESENTATION_HEADERS = ['accept', 'accept-language', 'prefer'];
198
199
  // Models
199
200
  const CID_FIELD_NAME = '_cid';
200
201
  const EVENT_SPLITTER = /\s+/;
@@ -5928,6 +5929,8 @@ class ODataRequest {
5928
5929
  withCredentials;
5929
5930
  bodyQueryOptions;
5930
5931
  fetchPolicy;
5932
+ ignoreCacheControl;
5933
+ cacheInvalidation;
5931
5934
  maxAge;
5932
5935
  resource;
5933
5936
  _responseType;
@@ -5954,6 +5957,8 @@ class ODataRequest {
5954
5957
  this._body = this.resource.serialize(this._body, parserOptions);
5955
5958
  this.withCredentials = init.withCredentials ?? apiOptions.withCredentials;
5956
5959
  this.fetchPolicy = init.fetchPolicy ?? apiOptions.fetchPolicy;
5960
+ this.ignoreCacheControl = init.ignoreCacheControl ?? apiOptions.ignoreCacheControl;
5961
+ this.cacheInvalidation = init.cacheInvalidation ?? apiOptions.cacheInvalidation;
5957
5962
  this.maxAge = init.maxAge;
5958
5963
  this.bodyQueryOptions = [
5959
5964
  ...(apiOptions.bodyQueryOptions ?? []),
@@ -6052,6 +6057,8 @@ class ODataRequest {
6052
6057
  reportProgress: options.reportProgress,
6053
6058
  responseType: options.responseType,
6054
6059
  fetchPolicy: options.fetchPolicy,
6060
+ ignoreCacheControl: options.ignoreCacheControl,
6061
+ cacheInvalidation: options.cacheInvalidation,
6055
6062
  maxAge: options.maxAge,
6056
6063
  parserOptions: options.parserOptions,
6057
6064
  withCredentials: options.withCredentials,
@@ -6096,8 +6103,29 @@ class ODataRequest {
6096
6103
  get urlWithParams() {
6097
6104
  return `${this.api.serviceRootUrl}${this.pathWithParams}`;
6098
6105
  }
6106
+ /** A versioned response identity that includes the API and transport representation. */
6099
6107
  get cacheKey() {
6100
- return this._params.keys().length > 0 ? `${this._path}?${this._params}` : this._path;
6108
+ return [
6109
+ 'v2',
6110
+ this.api.serviceRootUrl,
6111
+ JSON.stringify(this.api.name ?? null),
6112
+ this._path,
6113
+ this._params.toString(),
6114
+ this.responseType ?? 'json',
6115
+ String(this.withCredentials ?? false),
6116
+ String(this.ignoreCacheControl),
6117
+ ...CACHE_REPRESENTATION_HEADERS.map((name) => JSON.stringify(this.headers.getAll(name))),
6118
+ ]
6119
+ .map((part) => part.replace(/%/g, '%25').replace(/\|/g, '%7C'))
6120
+ .join('|');
6121
+ }
6122
+ /** The versioned response-cache partition belonging to this API. */
6123
+ get cacheScope() {
6124
+ return [
6125
+ 'request',
6126
+ 'v2',
6127
+ encodeURIComponent(JSON.stringify([this.api.serviceRootUrl, this.api.name ?? null])),
6128
+ ];
6101
6129
  }
6102
6130
  isQueryBody() {
6103
6131
  return (this._method === 'GET' &&
@@ -6557,16 +6585,6 @@ class ODataActionResource extends ODataResource {
6557
6585
  }
6558
6586
  }
6559
6587
 
6560
- const Arrays = {
6561
- // Zip arrays
6562
- // Example
6563
- // Arrays.zip([1, 2, 3, 4, 5, 6], ['a', 'b', 'c', 'd', 'e', 'f'])
6564
- // => [[1, 'a'], [2, 'b'], [3, 'c'], [4, 'd'], [5, 'e'], [6, 'f']]
6565
- zip: (...arrays) => {
6566
- return arrays[0].map((_, i) => arrays.map((array) => array[i]));
6567
- },
6568
- };
6569
-
6570
6588
  class ODataBatchRequest extends Subject {
6571
6589
  request;
6572
6590
  id;
@@ -6664,103 +6682,144 @@ class ODataBatchResource extends ODataResource {
6664
6682
  return batch;
6665
6683
  }
6666
6684
  //#endregion
6667
- storeRequester() {
6668
- const current = this.api.requester;
6669
- // Switch to the batch requester
6670
- this.api.requester = (req) => {
6671
- if (req.api !== this.api)
6672
- throw new Error('Batch Request are for the same api.');
6673
- if (req.observe === 'events')
6674
- throw new Error("Batch Request does not allows observe == 'events'.");
6675
- this._requests.push(new ODataBatchRequest(req));
6676
- return this._requests[this._requests.length - 1];
6677
- };
6678
- return current;
6679
- }
6680
- restoreRequester(handler) {
6681
- this.api.requester = handler;
6682
- }
6683
6685
  /**
6684
6686
  * Add to batch request
6685
6687
  * @param ctx The context for the request
6686
6688
  * @returns The result of execute the context
6687
6689
  */
6688
6690
  add(ctx) {
6689
- // Store original requester
6690
- var handler = this.storeRequester();
6691
- // Execute the context
6692
- const result = ctx(this);
6693
- // Restore original requester
6694
- this.restoreRequester(handler);
6695
- return result;
6691
+ return this.api.captureRequests((req) => {
6692
+ if (req.api !== this.api)
6693
+ throw new Error('Batch Request are for the same api.');
6694
+ if (req.observe === 'events')
6695
+ throw new Error("Batch Request does not allows observe == 'events'.");
6696
+ const request = new ODataBatchRequest(req);
6697
+ this._requests.push(request);
6698
+ return request;
6699
+ }, () => ctx(this));
6696
6700
  }
6701
+ /** Send collected requests, evaluating their cache policies when subscribed. */
6697
6702
  send(options) {
6698
- if (this.api.options.jsonBatchFormat) {
6699
- return this.sendJson(options);
6700
- }
6701
- else {
6702
- return this.sendLegacy(options);
6703
+ return defer(() => {
6704
+ const readsCache = this._requests.some(({ request }) => request.isFetch() &&
6705
+ request.fetchPolicy !== 'network-only' &&
6706
+ request.fetchPolicy !== 'no-cache');
6707
+ let readiness = defer(() => {
6708
+ const ready = readsCache ? this.api.cache?.ready?.() : undefined;
6709
+ return ready === undefined ? of(undefined) : from(ready);
6710
+ }).pipe(tap({
6711
+ error: (error) => this._requests.forEach((request) => request.error(error)),
6712
+ }));
6713
+ if (this.api.errorHandler !== undefined) {
6714
+ readiness = readiness.pipe(catchError(this.api.errorHandler));
6715
+ }
6716
+ return readiness.pipe(switchMap(() => this.sendRequests(options)));
6717
+ });
6718
+ }
6719
+ sendRequests(options) {
6720
+ const outgoing = [];
6721
+ const responses = new Map();
6722
+ const responseErrors = new Set();
6723
+ const cacheErrors = new ReplaySubject(1);
6724
+ let dispatchingResponses = false;
6725
+ let pendingCacheError;
6726
+ const subscriptions = new Subscription();
6727
+ for (const request of this._requests) {
6728
+ subscriptions.add(this.api
6729
+ .executeRequest(request.request, () => {
6730
+ const response = new Subject();
6731
+ responses.set(request, response);
6732
+ outgoing.push(request);
6733
+ return response;
6734
+ }, true)
6735
+ .subscribe({
6736
+ next: (response) => request.next(response),
6737
+ error: (error) => {
6738
+ request.error(error);
6739
+ if (responseErrors.has(error) || this.api.isCacheMiss(error))
6740
+ return;
6741
+ if (dispatchingResponses)
6742
+ pendingCacheError ??= { error };
6743
+ else
6744
+ cacheErrors.error(error);
6745
+ },
6746
+ complete: () => request.complete(),
6747
+ }));
6703
6748
  }
6749
+ const result = defer(() => this.api.options.jsonBatchFormat
6750
+ ? this.sendJson(outgoing, options).pipe(map((response) => ({
6751
+ response,
6752
+ parsed: ODataBatchResource.parseJsonResponse(outgoing, response),
6753
+ })))
6754
+ : this.sendLegacy(outgoing, options).pipe(map((response) => ({
6755
+ response,
6756
+ parsed: ODataBatchResource.parseLegacyResponse(outgoing, response),
6757
+ }))));
6758
+ const resultWithResponses = result.pipe(map(({ response, parsed }) => {
6759
+ this._responses = [...(this._responses ?? []), ...parsed];
6760
+ dispatchingResponses = true;
6761
+ outgoing.forEach((request, index) => {
6762
+ const result = parsed[index];
6763
+ const stream = responses.get(request);
6764
+ if (result === undefined) {
6765
+ const error = new Error('Missing batch response');
6766
+ responseErrors.add(error);
6767
+ stream.error(error);
6768
+ }
6769
+ else if (result.ok) {
6770
+ stream.next(result);
6771
+ stream.complete();
6772
+ }
6773
+ else {
6774
+ responseErrors.add(result);
6775
+ stream.error(result);
6776
+ }
6777
+ });
6778
+ dispatchingResponses = false;
6779
+ if (pendingCacheError !== undefined)
6780
+ cacheErrors.error(pendingCacheError.error);
6781
+ return response;
6782
+ }));
6783
+ const failures = this.api.errorHandler === undefined
6784
+ ? cacheErrors
6785
+ : cacheErrors.pipe(catchError(this.api.errorHandler));
6786
+ return race(failures, resultWithResponses).pipe(tap({
6787
+ error: (error) => {
6788
+ responseErrors.add(error);
6789
+ responses.forEach((response) => response.error(error));
6790
+ this._requests.forEach((request) => request.error(error));
6791
+ },
6792
+ complete: () => this._requests.forEach((request) => request.complete()),
6793
+ }), finalize(() => subscriptions.unsubscribe()));
6704
6794
  }
6705
- sendJson(options) {
6795
+ sendJson(requests, options) {
6706
6796
  const headers = Http.mergeHttpHeaders((options && options.headers) || {}, {
6707
6797
  [ODATA_VERSION]: VERSION_4_0,
6708
6798
  });
6709
- return this.api
6710
- .request('POST', this, {
6711
- body: ODataBatchResource.buildJsonBody(this._requests, this.api.options),
6799
+ return this.api.request('POST', this, {
6800
+ body: ODataBatchResource.buildJsonBody(requests, this.api.options),
6712
6801
  responseType: 'json',
6713
6802
  observe: 'response',
6714
6803
  headers: headers,
6715
6804
  params: options ? options.params : undefined,
6716
6805
  withCredentials: options ? options.withCredentials : undefined,
6717
- })
6718
- .pipe(map$1((response) => {
6719
- if (this._responses == null) {
6720
- this._responses = [];
6721
- }
6722
- this._responses = [
6723
- ...this._responses,
6724
- ...ODataBatchResource.parseJsonResponse(this._requests, response),
6725
- ];
6726
- //HACK: tuple[1] === undefined
6727
- Arrays.zip(this._requests, this._responses).forEach((tuple) => {
6728
- if (!tuple[0].isStopped && tuple[1])
6729
- tuple[0].onLoad(tuple[1]);
6730
- });
6731
- return response;
6732
- }));
6806
+ });
6733
6807
  }
6734
- sendLegacy(options) {
6808
+ sendLegacy(requests, options) {
6735
6809
  const bound = Strings.uniqueId({ prefix: BATCH_PREFIX });
6736
6810
  const headers = Http.mergeHttpHeaders((options && options.headers) || {}, {
6737
6811
  [ODATA_VERSION]: VERSION_4_0,
6738
6812
  [CONTENT_TYPE]: MULTIPART_MIXED_BOUNDARY + bound,
6739
6813
  [ACCEPT]: MULTIPART_MIXED,
6740
6814
  });
6741
- return this.api
6742
- .request('POST', this, {
6743
- body: ODataBatchResource.buildLegacyBody(bound, this._requests, this.api.options),
6815
+ return this.api.request('POST', this, {
6816
+ body: ODataBatchResource.buildLegacyBody(bound, requests, this.api.options),
6744
6817
  responseType: 'text',
6745
6818
  observe: 'response',
6746
6819
  headers: headers,
6747
6820
  params: options ? options.params : undefined,
6748
6821
  withCredentials: options ? options.withCredentials : undefined,
6749
- })
6750
- .pipe(map$1((response) => {
6751
- if (this._responses == null) {
6752
- this._responses = [];
6753
- }
6754
- this._responses = [
6755
- ...this._responses,
6756
- ...ODataBatchResource.parseLegacyResponse(this._requests, response),
6757
- ];
6758
- Arrays.zip(this._requests, this._responses).forEach((tuple) => {
6759
- if (!tuple[0].isStopped && tuple[1])
6760
- tuple[0].onLoad(tuple[1]);
6761
- });
6762
- return response;
6763
- }));
6822
+ });
6764
6823
  }
6765
6824
  /**
6766
6825
  * Execute the batch request
@@ -6770,7 +6829,7 @@ class ODataBatchResource extends ODataResource {
6770
6829
  */
6771
6830
  exec(ctx, options) {
6772
6831
  let result = this.add(ctx);
6773
- return this.send(options).pipe(map$1((response) => [result, response]));
6832
+ return this.send(options).pipe(map((response) => [result, response]));
6774
6833
  }
6775
6834
  body() {
6776
6835
  return ODataBatchResource.buildLegacyBody(Strings.uniqueId({ prefix: BATCH_PREFIX }), this._requests, this.api.options);
@@ -6940,21 +6999,29 @@ class ODataBatchResource extends ODataResource {
6940
6999
  });
6941
7000
  }
6942
7001
  static parseJsonResponse(requests, response) {
6943
- const responses = (response.body ? response.body : {})['responses'] ?? [];
6944
- return responses.map((response, index) => {
6945
- let request = requests[index].request;
6946
- let code = response['status'];
6947
- let headers = new HttpHeaders(response['headers']);
6948
- let body = response['body'];
7002
+ const responses = response.body?.['responses'] ?? [];
7003
+ return requests.map(({ request, id }, index) => {
7004
+ const response = responses.find((response) => response.id === id) ??
7005
+ (responses[index]?.id ? undefined : responses[index]);
7006
+ if (response === undefined) {
7007
+ return new HttpErrorResponse({
7008
+ error: new Error('Missing batch response'),
7009
+ status: 0,
7010
+ url: request.urlWithParams,
7011
+ });
7012
+ }
7013
+ let code = response.status;
7014
+ const headers = new HttpHeaders(response.headers);
7015
+ let body = response.body;
6949
7016
  if (code === 0) {
6950
7017
  code = !!body ? 200 : 0;
6951
7018
  }
6952
7019
  let ok = code >= 200 && code < 300;
6953
7020
  if (request.responseType === 'json' && typeof body === 'string') {
6954
7021
  const originalBody = body;
6955
- body = body.replace(XSSI_PREFIX, '');
7022
+ const text = body.replace(XSSI_PREFIX, '');
6956
7023
  try {
6957
- body = body !== '' ? JSON.parse(body) : null;
7024
+ body = text !== '' ? JSON.parse(text) : null;
6958
7025
  }
6959
7026
  catch (error) {
6960
7027
  body = originalBody;
@@ -10183,16 +10250,30 @@ class ODataResponseOptions {
10183
10250
  });
10184
10251
  }
10185
10252
  setCache(cacheControl) {
10186
- cacheControl.split(',').forEach((directive) => {
10187
- if (directive.startsWith(MAX_AGE)) {
10188
- let maxAge = Number(directive.split('=')[1]);
10189
- if (!Number.isNaN(maxAge))
10190
- this.maxAge = maxAge;
10191
- }
10192
- if (['public', 'private', 'no-cache', 'no-store'].indexOf(directive) !== -1) {
10193
- this.cacheability = directive;
10253
+ const precedence = ['public', 'private', 'no-cache', 'no-store'];
10254
+ this.cacheability = undefined;
10255
+ this.maxAge = undefined;
10256
+ for (const directive of cacheControl.split(',')) {
10257
+ const separator = directive.indexOf('=');
10258
+ const name = (separator < 0 ? directive : directive.slice(0, separator)).trim().toLowerCase();
10259
+ const value = separator < 0
10260
+ ? ''
10261
+ : directive
10262
+ .slice(separator + 1)
10263
+ .trim()
10264
+ .replace(/^"(.*)"$/, '$1');
10265
+ if (name === MAX_AGE) {
10266
+ const maxAge = /^\d+$/.test(value) ? Number(value) : NaN;
10267
+ const seconds = Number.isFinite(maxAge * 1000) ? maxAge : 0;
10268
+ this.maxAge = Math.min(this.maxAge ?? Infinity, seconds);
10269
+ }
10270
+ const cacheability = precedence.find((candidate) => candidate === name);
10271
+ if (cacheability !== undefined &&
10272
+ (this.cacheability === undefined ||
10273
+ precedence.indexOf(cacheability) > precedence.indexOf(this.cacheability))) {
10274
+ this.cacheability = cacheability;
10194
10275
  }
10195
- });
10276
+ }
10196
10277
  }
10197
10278
  }
10198
10279
 
@@ -10241,6 +10322,17 @@ class ODataResponse extends HttpResponse {
10241
10322
  url: this.url,
10242
10323
  };
10243
10324
  }
10325
+ /** Whether the response can be reused without server validation. */
10326
+ isCacheable(ignoreCacheControl = false) {
10327
+ const vary = (this.headers.getAll('Vary') ?? [])
10328
+ .flatMap((value) => value.split(','))
10329
+ .map((name) => name.trim().toLowerCase())
10330
+ .filter((name) => name.length > 0);
10331
+ return (this.ok &&
10332
+ (ignoreCacheControl ||
10333
+ (this.options.cacheability !== 'no-store' && this.options.cacheability !== 'no-cache')) &&
10334
+ vary.every((name) => CACHE_REPRESENTATION_HEADERS.includes(name)));
10335
+ }
10244
10336
  _options;
10245
10337
  get options() {
10246
10338
  if (this._options === undefined) {
@@ -12916,6 +13008,10 @@ class ODataApiOptions {
12916
13008
  * Cache fetch policy
12917
13009
  */
12918
13010
  fetchPolicy;
13011
+ /** Ignore response Cache-Control directives. Defaults to false. */
13012
+ ignoreCacheControl;
13013
+ /** Scope invalidated by successful writes. Defaults to entity-set. */
13014
+ cacheInvalidation;
12919
13015
  /**
12920
13016
  * Extra params to be sent in the request
12921
13017
  */
@@ -12947,6 +13043,8 @@ class ODataApiOptions {
12947
13043
  this.withCredentials = config.withCredentials;
12948
13044
  this.stripMetadata = config.stripMetadata || DEFAULT_STRIP_METADATA;
12949
13045
  this.fetchPolicy = config.fetchPolicy || DEFAULT_FETCH_POLICY;
13046
+ this.ignoreCacheControl = config.ignoreCacheControl ?? false;
13047
+ this.cacheInvalidation = config.cacheInvalidation ?? 'entity-set';
12950
13048
  this.bodyQueryOptions = config.bodyQueryOptions || [];
12951
13049
  this.accept = config.accept;
12952
13050
  Object.assign(this.etag, config.etag || {});
@@ -13016,11 +13114,15 @@ class ODataReference extends ODataAnnotatable {
13016
13114
  }
13017
13115
 
13018
13116
  const RESERVED_FIELD_NAMES = Object.getOwnPropertyNames(ODataModel.prototype);
13117
+ // Share invalidation generations across API instances using the same cache.
13118
+ const CACHE_GENERATIONS = new WeakMap();
13119
+ const CACHE_MISSES = new WeakSet();
13019
13120
  /**
13020
13121
  * Api abstraction for consuming OData services.
13021
13122
  */
13022
13123
  class ODataApi {
13023
13124
  requester;
13125
+ _requestCapture;
13024
13126
  serviceRootUrl;
13025
13127
  metadataUrl;
13026
13128
  name;
@@ -13212,6 +13314,8 @@ class ODataApi {
13212
13314
  bodyQueryOptions: options.bodyQueryOptions,
13213
13315
  reportProgress: options.reportProgress,
13214
13316
  fetchPolicy: options.fetchPolicy,
13317
+ ignoreCacheControl: options.ignoreCacheControl,
13318
+ cacheInvalidation: options.cacheInvalidation,
13215
13319
  maxAge: options.maxAge,
13216
13320
  parserOptions: options.parserOptions,
13217
13321
  withCredentials: options.withCredentials,
@@ -13251,53 +13355,100 @@ class ODataApi {
13251
13355
  * @returns
13252
13356
  */
13253
13357
  handleRequest(req) {
13254
- return this.cache !== undefined && req.isFetch()
13255
- ? this.handleCacheFetch(req)
13256
- : this.cache !== undefined && req.isMutate()
13257
- ? this.handleCacheMutate(req)
13258
- : this.handleRequester(req);
13259
- }
13260
- handleRequester(req) {
13261
- return (this.requester !== undefined ? this.requester(req) : NEVER).pipe(map((res) => res.type === HttpEventType.Response ? ODataResponse.fromHttpResponse(req, res) : res));
13262
- }
13263
- handleCacheFetch(req) {
13358
+ return this._requestCapture !== undefined
13359
+ ? this.handleRequester(req, this._requestCapture)
13360
+ : this.executeRequest(req);
13361
+ }
13362
+ /** @internal Collect batch requests synchronously without executing their cache effects. */
13363
+ captureRequests(capture, callback) {
13364
+ const previous = this._requestCapture;
13365
+ this._requestCapture = capture;
13366
+ try {
13367
+ return callback();
13368
+ }
13369
+ finally {
13370
+ this._requestCapture = previous;
13371
+ }
13372
+ }
13373
+ /** @internal Execute a collected or ordinary request with subscription-time cache semantics. */
13374
+ executeRequest(req, requester = this.requester, cacheReady = false) {
13375
+ return defer(() => {
13376
+ const cache = this.cache;
13377
+ const network = () => this.handleRequester(req, requester);
13378
+ if (cache === undefined)
13379
+ return network();
13380
+ if (req.isFetch()) {
13381
+ if (req.fetchPolicy === 'no-cache')
13382
+ return network();
13383
+ const fetch = () => this.handleCacheFetch(req, cache, network);
13384
+ return !cacheReady && req.fetchPolicy !== 'network-only' && cache.ready !== undefined
13385
+ ? from(cache.ready()).pipe(switchMap(fetch))
13386
+ : fetch();
13387
+ }
13388
+ return req.isMutate() && !req.isBatch()
13389
+ ? network().pipe(tap((res) => {
13390
+ if (res.type === HttpEventType.Response && res.ok)
13391
+ this.invalidateCache(req, cache);
13392
+ }))
13393
+ : network();
13394
+ });
13395
+ }
13396
+ /** @internal Whether the error is a `cache-only` miss rather than a cache failure. */
13397
+ isCacheMiss(error) {
13398
+ return error instanceof Error && CACHE_MISSES.has(error);
13399
+ }
13400
+ handleRequester(req, requester) {
13401
+ return (requester !== undefined ? requester(req) : NEVER).pipe(map((res) => res.type === HttpEventType.Response ? ODataResponse.fromHttpResponse(req, res) : res));
13402
+ }
13403
+ cacheGeneration(req, cache) {
13404
+ const generations = CACHE_GENERATIONS.get(cache);
13405
+ const apiKey = JSON.stringify(req.cacheScope);
13406
+ const scopeKey = JSON.stringify(cache.scope(req));
13407
+ return ((generations?.get(apiKey) ?? 0) +
13408
+ (scopeKey !== apiKey ? generations?.get(scopeKey) ?? 0 : 0));
13409
+ }
13410
+ invalidateCache(req, cache) {
13411
+ let generations = CACHE_GENERATIONS.get(cache);
13412
+ if (generations === undefined) {
13413
+ generations = new Map();
13414
+ CACHE_GENERATIONS.set(cache, generations);
13415
+ }
13416
+ const scope = cache.scope(req);
13417
+ // Custom scopes outside the API partition use scoped invalidation.
13418
+ const target = req.cacheInvalidation === 'api' &&
13419
+ req.cacheScope.every((part, index) => scope[index] === part) ? req.cacheScope : scope;
13420
+ const key = JSON.stringify(target);
13421
+ generations.set(key, (generations.get(key) ?? 0) + 1);
13422
+ cache.forget({ scope: target });
13423
+ }
13424
+ handleCacheFetch(req, cache, network) {
13264
13425
  const policy = req.fetchPolicy;
13265
- const cached = this.cache.getResponse(req);
13266
- if (policy === 'no-cache') {
13267
- return this.handleRequester(req);
13268
- }
13426
+ const cached = policy === 'network-only' ? undefined : cache.getResponse(req);
13269
13427
  if (policy === 'cache-only') {
13270
- if (cached !== undefined) {
13428
+ if (cached !== undefined)
13271
13429
  return of(cached);
13430
+ const miss = new Error('No Cached');
13431
+ CACHE_MISSES.add(miss);
13432
+ return throwError(() => miss);
13433
+ }
13434
+ if (cached !== undefined && policy === 'cache-first')
13435
+ return of(cached);
13436
+ const generation = this.cacheGeneration(req, cache);
13437
+ const response = defer(network).pipe(tap((res) => {
13438
+ if (res.type !== HttpEventType.Response ||
13439
+ !res.ok ||
13440
+ generation !== this.cacheGeneration(req, cache)) {
13441
+ return;
13272
13442
  }
13273
- else {
13274
- return throwError(() => new Error('No Cached'));
13275
- }
13276
- }
13277
- let res$ = cached !== undefined && policy !== 'network-only'
13278
- ? policy === 'cache-and-network'
13279
- ? this.handleRequester(req).pipe(startWith(cached))
13280
- : of(cached)
13281
- : this.handleRequester(req);
13282
- if (cached === undefined &&
13283
- (policy === 'cache-first' || policy === 'cache-and-network' || policy === 'network-only')) {
13284
- res$ = res$.pipe(tap((res) => {
13285
- if (res.options.cacheability !== 'no-store') {
13286
- this.cache.putResponse(req, res);
13287
- }
13288
- }));
13289
- }
13290
- return res$;
13291
- }
13292
- handleCacheMutate(req) {
13293
- const requests = req.isBatch()
13294
- ? req.resource.requests().filter((r) => r.isMutate())
13295
- : [req];
13296
- for (var r of requests) {
13297
- const scope = this.cache.scope(r);
13298
- this.cache.forget({ scope });
13299
- }
13300
- return this.handleRequester(req);
13443
+ const response = res instanceof ODataResponse ? res : ODataResponse.fromHttpResponse(req, res);
13444
+ if (response.isCacheable(req.ignoreCacheControl))
13445
+ cache.putResponse(req, response);
13446
+ else
13447
+ cache.forget({ name: req.cacheKey, scope: cache.scope(req) });
13448
+ }));
13449
+ return cached !== undefined && policy === 'cache-and-network'
13450
+ ? response.pipe(startWith(cached))
13451
+ : response;
13301
13452
  }
13302
13453
  //# region Find by Type
13303
13454
  // Memoize
@@ -13713,6 +13864,8 @@ function addBody(options, body) {
13713
13864
  body,
13714
13865
  etag: options.etag,
13715
13866
  fetchPolicy: options.fetchPolicy,
13867
+ ignoreCacheControl: options.ignoreCacheControl,
13868
+ cacheInvalidation: options.cacheInvalidation,
13716
13869
  headers: options.headers,
13717
13870
  observe: options.observe,
13718
13871
  params: options.params,
@@ -14280,8 +14433,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
14280
14433
 
14281
14434
  class ODataBaseCache {
14282
14435
  maxAge;
14283
- constructor({ maxAge = DEFAULT_MAXAGE }) {
14436
+ _onError;
14437
+ _error;
14438
+ constructor({ maxAge = DEFAULT_MAXAGE, onError, }) {
14439
+ this.validateMaxAge(maxAge);
14284
14440
  this.maxAge = maxAge;
14441
+ this._onError = onError;
14285
14442
  }
14286
14443
  /**
14287
14444
  * Check if the entry is expired
@@ -14289,7 +14446,7 @@ class ODataBaseCache {
14289
14446
  * @returns Boolean indicating if the entry is expired
14290
14447
  */
14291
14448
  isExpired(entry) {
14292
- return entry.date < Date.now() - entry.maxAge;
14449
+ return entry.date <= Date.now() - entry.maxAge;
14293
14450
  }
14294
14451
  /**
14295
14452
  * Using the resource on the request build an array of string to identify the scope of the request
@@ -14298,11 +14455,11 @@ class ODataBaseCache {
14298
14455
  */
14299
14456
  scope(req) {
14300
14457
  const segments = req.resource.cloneSegments();
14301
- return segments.segments({ key: true }).reduce((acc, s) => {
14302
- if (s.name === PathSegment.entitySet)
14458
+ return segments.segments().reduce((acc, s) => {
14459
+ if (s.name === PathSegment.entitySet || s.name === PathSegment.singleton)
14303
14460
  acc = [...acc, s.path()];
14304
14461
  return acc;
14305
- }, ['request']);
14462
+ }, req.cacheScope);
14306
14463
  }
14307
14464
  /**
14308
14465
  * Using the odata context on the response build an array of string to identify the tags of the response
@@ -14327,11 +14484,13 @@ class ODataBaseCache {
14327
14484
  * @returns The entry to store in the cache
14328
14485
  */
14329
14486
  buildEntry(payload, { maxAge, tags }) {
14487
+ const seconds = maxAge ?? this.maxAge;
14488
+ this.validateMaxAge(seconds);
14330
14489
  return {
14331
14490
  payload,
14332
14491
  date: Date.now(),
14333
- maxAge: (maxAge ?? this.maxAge) * 1000,
14334
- tags: tags ?? [],
14492
+ maxAge: seconds * 1000,
14493
+ tags: [...(tags ?? [])],
14335
14494
  };
14336
14495
  }
14337
14496
  /**
@@ -14342,14 +14501,147 @@ class ODataBaseCache {
14342
14501
  buildKey(names) {
14343
14502
  return names.join(CACHE_KEY_SEPARATOR);
14344
14503
  }
14504
+ validateMaxAge(maxAge) {
14505
+ if (!Number.isFinite(maxAge * 1000) || maxAge < 0) {
14506
+ throw new RangeError('Cache maxAge must be a finite, non-negative number of seconds');
14507
+ }
14508
+ }
14509
+ reportError(message, cause) {
14510
+ const error = new Error(message, { cause });
14511
+ if (this._onError !== undefined)
14512
+ this._onError(error);
14513
+ else {
14514
+ this._error = error;
14515
+ throw error;
14516
+ }
14517
+ }
14518
+ /** Report a discarded entry; corrupt entries are treated as cache misses. */
14519
+ reportCorruption(message, cause) {
14520
+ this._onError?.(new Error(message, { cause }));
14521
+ }
14522
+ checkError() {
14523
+ if (this._error !== undefined)
14524
+ throw this._error;
14525
+ }
14526
+ isEntry(value) {
14527
+ return (typeof value === 'object' &&
14528
+ value !== null &&
14529
+ 'payload' in value &&
14530
+ 'date' in value &&
14531
+ typeof value.date === 'number' &&
14532
+ Number.isFinite(value.date) &&
14533
+ 'maxAge' in value &&
14534
+ typeof value.maxAge === 'number' &&
14535
+ Number.isFinite(value.maxAge) &&
14536
+ value.maxAge >= 0 &&
14537
+ 'tags' in value &&
14538
+ Array.isArray(value.tags) &&
14539
+ value.tags.every((tag) => typeof tag === 'string'));
14540
+ }
14541
+ matches(key, entry, { name, scope = [], tags = [] }) {
14542
+ const scopedKey = this.buildKey(name !== undefined ? [...scope, name] : scope);
14543
+ return (this.isExpired(entry) ||
14544
+ (name !== undefined
14545
+ ? key === scopedKey
14546
+ : scope.length > 0 &&
14547
+ (key === scopedKey || key.startsWith(`${scopedKey}${CACHE_KEY_SEPARATOR}`))) ||
14548
+ tags.some((tag) => entry.tags.includes(tag)));
14549
+ }
14550
+ isResponseJson(value) {
14551
+ return (typeof value === 'object' &&
14552
+ value !== null &&
14553
+ 'body' in value &&
14554
+ 'status' in value &&
14555
+ typeof value.status === 'number' &&
14556
+ Number.isInteger(value.status) &&
14557
+ 'statusText' in value &&
14558
+ typeof value.statusText === 'string' &&
14559
+ 'url' in value &&
14560
+ (value.url === null || typeof value.url === 'string') &&
14561
+ 'headers' in value &&
14562
+ typeof value.headers === 'object' &&
14563
+ value.headers !== null &&
14564
+ !Array.isArray(value.headers) &&
14565
+ Object.values(value.headers).every((header) => typeof header === 'string' ||
14566
+ (Array.isArray(header) && header.every((part) => typeof part === 'string'))));
14567
+ }
14568
+ snapshot(response) {
14569
+ const body = response.body;
14570
+ return {
14571
+ ...response,
14572
+ body: typeof Blob !== 'undefined' && body instanceof Blob
14573
+ ? body.slice(0, body.size, body.type)
14574
+ : structuredClone(body),
14575
+ headers: structuredClone(response.headers),
14576
+ };
14577
+ }
14578
+ /** Store a detached response snapshot, respecting server reuse restrictions. */
14579
+ putResponse(req, res) {
14580
+ const scope = this.scope(req);
14581
+ if (!res.isCacheable(req.ignoreCacheControl)) {
14582
+ this.forget({ name: req.cacheKey, scope });
14583
+ return;
14584
+ }
14585
+ this.put(req.cacheKey, this.snapshot(res.toJson()), {
14586
+ maxAge: req.maxAge ?? (req.ignoreCacheControl ? undefined : res.options.maxAge),
14587
+ scope,
14588
+ tags: this.tags(res),
14589
+ });
14590
+ }
14591
+ /** Restore a detached response using the current request's resource and parser. */
14592
+ getResponse(req) {
14593
+ const scope = this.scope(req);
14594
+ const data = this.get(req.cacheKey, { scope });
14595
+ if (data === undefined)
14596
+ return undefined;
14597
+ if (!this.isResponseJson(data)) {
14598
+ this.forget({ name: req.cacheKey, scope });
14599
+ this.reportCorruption('Discarding an invalid cached OData response');
14600
+ return undefined;
14601
+ }
14602
+ const response = ODataResponse.fromJson(req, this.snapshot(data));
14603
+ if (!response.isCacheable(req.ignoreCacheControl)) {
14604
+ this.forget({ name: req.cacheKey, scope });
14605
+ return undefined;
14606
+ }
14607
+ return response;
14608
+ }
14345
14609
  }
14346
14610
 
14347
14611
  class ODataInMemoryCache extends ODataBaseCache {
14348
14612
  entries;
14349
- constructor({ maxAge } = {}) {
14350
- super({ maxAge });
14613
+ _nextExpiry = Infinity;
14614
+ constructor(options = {}) {
14615
+ super(options);
14351
14616
  this.entries = new Map();
14352
14617
  }
14618
+ setEntry(key, entry) {
14619
+ this.entries.set(key, entry);
14620
+ this._nextExpiry = Math.min(this._nextExpiry, entry.date + entry.maxAge);
14621
+ }
14622
+ deleteEntry(key) {
14623
+ this.entries.delete(key);
14624
+ }
14625
+ expireEntry(key) {
14626
+ this.deleteEntry(key);
14627
+ }
14628
+ clearEntries() {
14629
+ this.entries.clear();
14630
+ this._nextExpiry = Infinity;
14631
+ }
14632
+ prune() {
14633
+ if (Date.now() < this._nextExpiry)
14634
+ return;
14635
+ this._nextExpiry = Infinity;
14636
+ this.entries.forEach((entry, key) => {
14637
+ if (this.isExpired(entry))
14638
+ this.expireEntry(key);
14639
+ const current = this.entries.get(key);
14640
+ if (current !== undefined) {
14641
+ this._nextExpiry = Math.min(this._nextExpiry, current.date + current.maxAge);
14642
+ }
14643
+ });
14644
+ }
14353
14645
  /**
14354
14646
  * Put some payload in the cache
14355
14647
  * @param name The name for the entry
@@ -14359,9 +14651,11 @@ class ODataInMemoryCache extends ODataBaseCache {
14359
14651
  * @param tags The tags for the entry
14360
14652
  */
14361
14653
  put(name, payload, { maxAge, scope, tags } = {}) {
14654
+ this.checkError();
14362
14655
  const entry = this.buildEntry(payload, { maxAge, tags });
14363
14656
  const key = this.buildKey([...(scope ?? []), name]);
14364
- this.entries.set(key, entry);
14657
+ this.prune();
14658
+ this.setEntry(key, entry);
14365
14659
  }
14366
14660
  /**
14367
14661
  * Return the payload from the cache if it exists and is not expired
@@ -14370,266 +14664,416 @@ class ODataInMemoryCache extends ODataBaseCache {
14370
14664
  * @returns The payload of the entry
14371
14665
  */
14372
14666
  get(name, { scope } = {}) {
14667
+ this.checkError();
14668
+ this.prune();
14373
14669
  const key = this.buildKey([...(scope || []), name]);
14374
14670
  const entry = this.entries.get(key);
14375
14671
  return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined;
14376
14672
  }
14377
- /**
14378
- * Store the response in the cache
14379
- * @param req The request with the resource to store the response
14380
- * @param res The response to store in the cache
14381
- */
14382
- putResponse(req, res) {
14383
- let scope = this.scope(req);
14384
- let tags = this.tags(res);
14385
- this.put(req.cacheKey, res, {
14386
- maxAge: req.maxAge ?? res.options.maxAge,
14387
- scope,
14388
- tags,
14389
- });
14390
- }
14391
- /**
14392
- * Restore the response from the cache
14393
- * @param req The request with the resource to get the response
14394
- * @returns The response from the cache
14395
- */
14396
- getResponse(req) {
14397
- let scope = this.scope(req);
14398
- return this.get(req.cacheKey, { scope });
14399
- }
14400
14673
  /**
14401
14674
  * Remove all cache entries that are matching with the given options
14402
14675
  * @param options The options to forget
14403
14676
  */
14404
- forget({ name, scope = [], tags = [], } = {}) {
14405
- if (name)
14406
- scope.push(name);
14407
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14677
+ forget(options = {}) {
14678
+ this.checkError();
14408
14679
  this.entries.forEach((entry, k) => {
14409
- if (this.isExpired(entry) || // Expired
14410
- (key !== undefined && k.startsWith(key)) || // Key
14411
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1)) // Tags
14412
- ) {
14413
- this.entries.delete(k);
14414
- }
14680
+ if (this.matches(k, entry, options))
14681
+ this.deleteEntry(k);
14415
14682
  });
14416
14683
  }
14417
14684
  /**
14418
14685
  * Remove all cache entries
14419
14686
  */
14420
14687
  flush() {
14421
- this.entries = new Map();
14688
+ this.checkError();
14689
+ this.clearEntries();
14422
14690
  }
14691
+ /** Return the number of unexpired entries. */
14423
14692
  size() {
14693
+ this.checkError();
14694
+ this.prune();
14424
14695
  return this.entries.size;
14425
14696
  }
14426
14697
  }
14427
14698
 
14428
- class ODataInStorageCache extends ODataBaseCache {
14699
+ class ODataInStorageCache extends ODataInMemoryCache {
14429
14700
  prefix;
14430
- storage;
14431
- constructor({ prefix, storage = sessionStorage, maxAge, }) {
14432
- super({ maxAge });
14701
+ _storage;
14702
+ _persistent = true;
14703
+ _memoryOnly = new Set();
14704
+ constructor({ prefix, storage, ...options }) {
14705
+ super(options);
14433
14706
  this.prefix = prefix;
14434
- this.storage = storage;
14707
+ try {
14708
+ this._storage = storage ?? globalThis.sessionStorage;
14709
+ if (this._storage === undefined)
14710
+ throw new Error('Web Storage is unavailable');
14711
+ }
14712
+ catch (error) {
14713
+ this.disablePersistence(error);
14714
+ }
14715
+ this.loadFromStorage();
14716
+ }
14717
+ /** The backing storage. Throws if the platform has no Web Storage implementation. */
14718
+ get storage() {
14719
+ if (this._storage === undefined)
14720
+ throw new Error('Web Storage is unavailable');
14721
+ return this._storage;
14722
+ }
14723
+ set storage(storage) {
14724
+ this._storage = storage;
14725
+ this._persistent = true;
14726
+ this.loadFromStorage();
14435
14727
  }
14436
14728
  buildKey(names) {
14437
14729
  return super.buildKey([this.prefix, ...names]);
14438
14730
  }
14439
- put(name, payload, { maxAge, scope, tags } = {}) {
14440
- const entry = this.buildEntry(payload, { maxAge, tags });
14441
- const key = this.buildKey([...(scope ?? []), name]);
14442
- this.storage.setItem(key, JSON.stringify(entry));
14731
+ disablePersistence(cause) {
14732
+ if (!this._persistent)
14733
+ return;
14734
+ this._persistent = false;
14735
+ this.reportError('Web Storage cache persistence failed; using memory only', cause);
14443
14736
  }
14444
- get(name, { scope } = {}) {
14445
- const key = this.buildKey([...(scope || []), name]);
14446
- const entry = JSON.parse(this.storage.getItem(key) ?? '{}');
14447
- return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined;
14737
+ storageKeys() {
14738
+ if (this._storage === undefined)
14739
+ return [];
14740
+ try {
14741
+ const keys = [];
14742
+ const prefix = `${this.prefix}${CACHE_KEY_SEPARATOR}`;
14743
+ for (let index = 0; index < this.storage.length; index++) {
14744
+ const key = this.storage.key(index);
14745
+ if (key !== null && key.startsWith(prefix))
14746
+ keys.push(key);
14747
+ }
14748
+ return keys;
14749
+ }
14750
+ catch (error) {
14751
+ this.disablePersistence(error);
14752
+ return [];
14753
+ }
14754
+ }
14755
+ readEntry(key) {
14756
+ let raw;
14757
+ try {
14758
+ raw = this.storage.getItem(key);
14759
+ }
14760
+ catch (error) {
14761
+ this.disablePersistence(error);
14762
+ return this.entries.get(key);
14763
+ }
14764
+ if (raw === null)
14765
+ return undefined;
14766
+ let value;
14767
+ try {
14768
+ value = JSON.parse(raw);
14769
+ }
14770
+ catch (error) {
14771
+ this.deleteEntry(key);
14772
+ this.reportCorruption('Discarding invalid JSON from the Web Storage cache', error);
14773
+ return undefined;
14774
+ }
14775
+ if (!this.isEntry(value)) {
14776
+ this.deleteEntry(key);
14777
+ this.reportCorruption('Discarding an invalid Web Storage cache entry');
14778
+ return undefined;
14779
+ }
14780
+ if (this.isExpired(value)) {
14781
+ this.deleteEntry(key);
14782
+ return undefined;
14783
+ }
14784
+ return value;
14785
+ }
14786
+ loadFromStorage() {
14787
+ if (!this._persistent)
14788
+ return;
14789
+ const keys = this.storageKeys();
14790
+ if (!this._persistent)
14791
+ return;
14792
+ const stored = new Set(keys);
14793
+ this.entries.forEach((_, key) => {
14794
+ if (!stored.has(key) && !this._memoryOnly.has(key))
14795
+ super.deleteEntry(key);
14796
+ });
14797
+ for (const key of keys) {
14798
+ if (!this._persistent)
14799
+ break;
14800
+ if (this._memoryOnly.has(key))
14801
+ continue;
14802
+ const entry = this.readEntry(key);
14803
+ if (entry !== undefined)
14804
+ super.setEntry(key, entry);
14805
+ }
14806
+ this.prune();
14807
+ }
14808
+ // Removal is attempted after a memory fallback so flush/forget still clear persisted data.
14809
+ removeStoredEntry(key) {
14810
+ if (this._storage === undefined)
14811
+ return;
14812
+ try {
14813
+ this.storage.removeItem(key);
14814
+ }
14815
+ catch (error) {
14816
+ this.disablePersistence(error);
14817
+ }
14448
14818
  }
14449
- forget({ name, scope = [], tags = [], }) {
14450
- if (name)
14451
- scope.push(name);
14452
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14453
- Object.keys(this.storage)
14454
- .filter((k) => k.startsWith(this.prefix))
14455
- .forEach((k) => {
14456
- const entry = JSON.parse(this.storage.getItem(k) ?? '{}');
14457
- if (this.isExpired(entry) || // Expired
14458
- (key !== undefined && k.startsWith(key)) || // Key
14459
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1)) // Tags
14460
- ) {
14461
- this.storage.removeItem(k);
14819
+ setEntry(key, entry) {
14820
+ super.setEntry(key, entry);
14821
+ const payload = this.isResponseJson(entry.payload) ? entry.payload.body : entry.payload;
14822
+ if (['ArrayBuffer', 'Blob', 'File'].includes(Types.rawType(payload))) {
14823
+ this._memoryOnly.add(key);
14824
+ this.removeStoredEntry(key);
14825
+ return;
14826
+ }
14827
+ this._memoryOnly.delete(key);
14828
+ if (!this._persistent) {
14829
+ this.removeStoredEntry(key);
14830
+ return;
14831
+ }
14832
+ try {
14833
+ const serialized = JSON.stringify(entry);
14834
+ if (!this.isEntry(JSON.parse(serialized))) {
14835
+ throw new TypeError('Cache payload is not JSON serializable');
14462
14836
  }
14463
- });
14837
+ this.storage.setItem(key, serialized);
14838
+ }
14839
+ catch (error) {
14840
+ this.disablePersistence(error);
14841
+ }
14464
14842
  }
14465
- /**
14466
- * Flush the cache and clean the storage
14467
- */
14468
- flush() {
14469
- Object.keys(this.storage)
14470
- .filter((k) => k.startsWith(this.prefix))
14471
- .forEach((k) => {
14472
- this.storage.removeItem(k);
14473
- });
14843
+ deleteEntry(key) {
14844
+ super.deleteEntry(key);
14845
+ this._memoryOnly.delete(key);
14846
+ this.removeStoredEntry(key);
14474
14847
  }
14475
- /**
14476
- * Store the response in the cache
14477
- * @param req The request with the resource to store the response
14478
- * @param res The response to store in the cache
14479
- */
14480
- putResponse(req, res) {
14481
- const scope = this.scope(req);
14482
- const tags = this.tags(res);
14483
- this.put(req.cacheKey, res.toJson(), {
14484
- maxAge: req.maxAge ?? res.options.maxAge,
14485
- scope,
14486
- tags,
14487
- });
14848
+ expireEntry(key) {
14849
+ if (this._persistent && !this._memoryOnly.has(key)) {
14850
+ const latest = this.readEntry(key);
14851
+ if (latest !== undefined && !this.isExpired(latest)) {
14852
+ super.setEntry(key, latest);
14853
+ return;
14854
+ }
14855
+ }
14856
+ super.expireEntry(key);
14488
14857
  }
14489
- /**
14490
- * Restore the response from the cache
14491
- * @param req The request with the resource to get the response
14492
- * @returns The response from the cache
14493
- */
14494
- getResponse(req) {
14495
- const scope = this.scope(req);
14496
- const data = this.get(req.cacheKey, { scope });
14497
- return data !== undefined ? ODataResponse.fromJson(req, data) : undefined;
14858
+ clearEntries() {
14859
+ const keys = this.storageKeys();
14860
+ super.clearEntries();
14861
+ this._memoryOnly.clear();
14862
+ keys.forEach((key) => this.removeStoredEntry(key));
14498
14863
  }
14864
+ /** Read a persisted entry, or its in-memory fallback, without renewing its age. */
14865
+ get(name, { scope } = {}) {
14866
+ const key = this.buildKey([...(scope ?? []), name]);
14867
+ if (this._persistent && !this._memoryOnly.has(key)) {
14868
+ const entry = this.readEntry(key);
14869
+ if (entry === undefined)
14870
+ super.deleteEntry(key);
14871
+ else
14872
+ super.setEntry(key, entry);
14873
+ }
14874
+ return super.get(name, { scope });
14875
+ }
14876
+ /** Remove matching entries from both persistent storage and the memory fallback. */
14877
+ forget(options = {}) {
14878
+ this.loadFromStorage();
14879
+ super.forget(options);
14880
+ }
14881
+ /** Count unexpired entries, including memory-only binary responses. */
14499
14882
  size() {
14500
- return Object.keys(this.storage).filter((k) => k.startsWith(this.prefix)).length;
14883
+ this.loadFromStorage();
14884
+ return super.size();
14501
14885
  }
14502
14886
  }
14503
14887
 
14504
- class ODataIndexedDBCache extends ODataBaseCache {
14888
+ class ODataIndexedDBCache extends ODataInMemoryCache {
14505
14889
  name;
14506
14890
  version;
14507
14891
  store;
14508
- entries;
14509
- _db;
14510
- constructor({ name = 'ODataCache', store = 'cache', version = 1, maxAge, } = {}) {
14511
- super({ maxAge });
14892
+ _database;
14893
+ _persistent = true;
14894
+ _hydrated = false;
14895
+ _pending = [];
14896
+ _writes = Promise.resolve();
14897
+ _ready;
14898
+ constructor({ name = 'ODataCache', store = 'cache', version = 1, ...options } = {}) {
14899
+ super(options);
14512
14900
  this.name = name;
14513
14901
  this.store = store;
14514
14902
  this.version = version;
14515
- this.entries = new Map();
14516
- this._db = this.initDb();
14903
+ this._ready = this.initialize().catch((error) => {
14904
+ const flushed = this._pending.some((operation) => operation.kind === 'flush');
14905
+ this.disablePersistence(error);
14906
+ this._hydrated = true;
14907
+ this._pending = [];
14908
+ if (flushed)
14909
+ this.persist((store) => store.clear(), true);
14910
+ });
14911
+ }
14912
+ /** Wait for hydration and queued writes, or an explicitly reported memory-only fallback. */
14913
+ ready() {
14914
+ return this._ready.then(() => this._writes).then(() => this.checkError());
14915
+ }
14916
+ reportError(message, cause) {
14917
+ // IndexedDB event callbacks cannot throw into the caller's observable.
14918
+ try {
14919
+ super.reportError(message, cause);
14920
+ }
14921
+ catch (error) {
14922
+ if (!(error instanceof Error))
14923
+ throw error;
14924
+ this._error = error;
14925
+ }
14517
14926
  }
14518
- initDb() {
14927
+ // The database stays open after a fallback so flush/forget can still remove persisted data.
14928
+ disablePersistence(cause) {
14929
+ if (!this._persistent)
14930
+ return;
14931
+ this._persistent = false;
14932
+ this.reportError('IndexedDB cache persistence failed; using memory only', cause);
14933
+ }
14934
+ openDb() {
14519
14935
  return new Promise((resolve, reject) => {
14520
14936
  const request = indexedDB.open(this.name, this.version);
14521
- request.onupgradeneeded = (event) => {
14522
- const db = event.target.result;
14523
- if (!db.objectStoreNames.contains(this.store)) {
14524
- db.createObjectStore(this.store);
14937
+ let failed = false;
14938
+ const fail = (error) => {
14939
+ failed = true;
14940
+ reject(error);
14941
+ };
14942
+ request.onupgradeneeded = () => {
14943
+ if (failed) {
14944
+ request.transaction?.abort();
14945
+ return;
14525
14946
  }
14947
+ const db = request.result;
14948
+ if (!db.objectStoreNames.contains(this.store))
14949
+ db.createObjectStore(this.store);
14526
14950
  };
14527
- request.onsuccess = async (event) => {
14528
- const db = event.target.result;
14529
- await this.loadFromDb(db);
14951
+ request.onsuccess = () => {
14952
+ const db = request.result;
14953
+ if (failed) {
14954
+ db.close();
14955
+ return;
14956
+ }
14957
+ db.onversionchange = () => {
14958
+ db.close();
14959
+ this._database = undefined;
14960
+ this.disablePersistence(new Error('IndexedDB cache version changed'));
14961
+ };
14530
14962
  resolve(db);
14531
14963
  };
14532
- request.onerror = (event) => {
14533
- reject(event.target.error);
14534
- };
14964
+ request.onerror = () => fail(request.error);
14965
+ request.onblocked = () => fail(new Error('IndexedDB cache opening was blocked'));
14535
14966
  });
14536
14967
  }
14537
14968
  loadFromDb(db) {
14538
14969
  return new Promise((resolve, reject) => {
14539
- const transaction = db.transaction(this.store, 'readonly');
14540
- const store = transaction.objectStore(this.store);
14541
- const request = store.openCursor();
14542
- request.onsuccess = (event) => {
14543
- const cursor = event.target.result;
14544
- if (cursor) {
14545
- this.entries.set(cursor.key, cursor.value);
14546
- cursor.continue();
14970
+ const entries = new Map();
14971
+ const transaction = db.transaction(this.store, 'readwrite');
14972
+ const request = transaction.objectStore(this.store).openCursor();
14973
+ request.onsuccess = () => {
14974
+ const cursor = request.result;
14975
+ if (cursor === null)
14976
+ return;
14977
+ const value = cursor.value;
14978
+ if (typeof cursor.key !== 'string' || !this.isEntry(value)) {
14979
+ cursor.delete();
14980
+ this.reportCorruption('Discarding an invalid IndexedDB cache entry');
14981
+ }
14982
+ else if (this.isExpired(value)) {
14983
+ cursor.delete();
14547
14984
  }
14548
14985
  else {
14549
- resolve();
14986
+ entries.set(cursor.key, value);
14550
14987
  }
14988
+ cursor.continue();
14551
14989
  };
14552
- request.onerror = (event) => {
14553
- reject(event.target.error);
14554
- };
14555
- });
14556
- }
14557
- async withDb(fn) {
14558
- const db = await this._db;
14559
- return fn(db);
14990
+ transaction.oncomplete = () => resolve(entries);
14991
+ transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB load failed'));
14992
+ transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB load aborted'));
14993
+ });
14994
+ }
14995
+ async initialize() {
14996
+ const db = await this.openDb();
14997
+ this._database = db;
14998
+ const entries = await this.loadFromDb(db);
14999
+ if (!this._persistent) {
15000
+ this._hydrated = true;
15001
+ this._pending = [];
15002
+ return;
15003
+ }
15004
+ const pending = this._pending;
15005
+ this._pending = [];
15006
+ super.clearEntries();
15007
+ entries.forEach((entry, key) => super.setEntry(key, entry));
15008
+ this._hydrated = true;
15009
+ for (const operation of pending) {
15010
+ switch (operation.kind) {
15011
+ case 'put':
15012
+ this.setEntry(operation.key, operation.entry);
15013
+ break;
15014
+ case 'forget':
15015
+ this.forget(operation.options);
15016
+ break;
15017
+ case 'flush':
15018
+ this.flush();
15019
+ break;
15020
+ }
15021
+ }
15022
+ this.prune();
15023
+ await this._writes;
14560
15024
  }
14561
- put(name, payload, { maxAge, scope, tags } = {}) {
14562
- const entry = this.buildEntry(payload, { maxAge, tags });
14563
- const key = this.buildKey([...(scope ?? []), name]);
14564
- this.entries.set(key, entry);
14565
- this.withDb((db) => {
15025
+ persist(operation, removal = false) {
15026
+ this._writes = this._writes
15027
+ .then(() => {
15028
+ const db = this._database;
15029
+ if (db === undefined || (!this._persistent && !removal))
15030
+ return;
14566
15031
  return new Promise((resolve, reject) => {
14567
15032
  const transaction = db.transaction(this.store, 'readwrite');
14568
- const store = transaction.objectStore(this.store);
14569
- store.put(entry, key);
14570
15033
  transaction.oncomplete = () => resolve();
14571
- transaction.onerror = (event) => reject(event.target.error);
15034
+ transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB write failed'));
15035
+ transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB write aborted'));
15036
+ try {
15037
+ operation(transaction.objectStore(this.store));
15038
+ }
15039
+ catch (error) {
15040
+ transaction.abort();
15041
+ reject(error);
15042
+ }
14572
15043
  });
14573
- });
14574
- }
14575
- get(name, { scope } = {}) {
14576
- const key = this.buildKey([...(scope || []), name]);
14577
- const entry = this.entries.get(key);
14578
- return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined;
15044
+ })
15045
+ .catch((error) => this.disablePersistence(error));
15046
+ }
15047
+ setEntry(key, entry) {
15048
+ super.setEntry(key, entry);
15049
+ if (!this._hydrated)
15050
+ this._pending.push({ kind: 'put', key, entry });
15051
+ else if (this._persistent)
15052
+ this.persist((store) => store.put(entry, key));
15053
+ else
15054
+ this.persist((store) => store.delete(key), true);
14579
15055
  }
14580
- putResponse(req, res) {
14581
- const scope = this.scope(req);
14582
- const tags = this.tags(res);
14583
- this.put(req.cacheKey, res.toJson(), {
14584
- maxAge: req.maxAge ?? res.options.maxAge,
14585
- scope,
14586
- tags,
14587
- });
15056
+ deleteEntry(key) {
15057
+ super.deleteEntry(key);
15058
+ if (this._hydrated)
15059
+ this.persist((store) => store.delete(key), true);
14588
15060
  }
14589
- getResponse(req) {
14590
- const scope = this.scope(req);
14591
- const data = this.get(req.cacheKey, { scope });
14592
- return data !== undefined ? ODataResponse.fromJson(req, data) : undefined;
14593
- }
14594
- forget({ name, scope = [], tags = [], } = {}) {
14595
- if (name)
14596
- scope.push(name);
14597
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14598
- const keysToDelete = [];
14599
- this.entries.forEach((entry, k) => {
14600
- if (this.isExpired(entry) ||
14601
- (key !== undefined && k.startsWith(key)) ||
14602
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1))) {
14603
- keysToDelete.push(k);
14604
- }
14605
- });
14606
- keysToDelete.forEach((k) => this.entries.delete(k));
14607
- if (keysToDelete.length > 0) {
14608
- this.withDb((db) => {
14609
- return new Promise((resolve, reject) => {
14610
- const transaction = db.transaction(this.store, 'readwrite');
14611
- const store = transaction.objectStore(this.store);
14612
- keysToDelete.forEach((k) => store.delete(k));
14613
- transaction.oncomplete = () => resolve();
14614
- transaction.onerror = (event) => reject(event.target.error);
14615
- });
15061
+ clearEntries() {
15062
+ super.clearEntries();
15063
+ if (!this._hydrated)
15064
+ this._pending.push({ kind: 'flush' });
15065
+ else
15066
+ this.persist((store) => store.clear(), true);
15067
+ }
15068
+ /** Invalidate loaded entries and record filters that must also apply during hydration. */
15069
+ forget(options = {}) {
15070
+ if (!this._hydrated) {
15071
+ this._pending.push({
15072
+ kind: 'forget',
15073
+ options: { ...options, scope: [...(options.scope ?? [])], tags: [...(options.tags ?? [])] },
14616
15074
  });
14617
15075
  }
14618
- }
14619
- flush() {
14620
- this.entries = new Map();
14621
- this.withDb((db) => {
14622
- return new Promise((resolve, reject) => {
14623
- const transaction = db.transaction(this.store, 'readwrite');
14624
- const store = transaction.objectStore(this.store);
14625
- store.clear();
14626
- transaction.oncomplete = () => resolve();
14627
- transaction.onerror = (event) => reject(event.target.error);
14628
- });
14629
- });
14630
- }
14631
- size() {
14632
- return this.entries.size;
15076
+ super.forget(options);
14633
15077
  }
14634
15078
  }
14635
15079