angular-odata 0.162.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;
@@ -8147,18 +8214,25 @@ class CsdlRecord {
8147
8214
  }
8148
8215
  }
8149
8216
  class CsdlPropertyValue {
8150
- Name;
8217
+ Property;
8218
+ Bool;
8151
8219
  String;
8152
8220
  Date;
8153
8221
  EnumMember;
8154
- constructor({ Name, String, Date, EnumMember, }) {
8155
- this.Name = Name;
8222
+ TextContent;
8223
+ constructor({ Property, Bool, String, Date, EnumMember, TextContent, }) {
8224
+ this.Property = Property;
8225
+ this.Bool = Bool;
8156
8226
  this.String = String;
8157
8227
  this.Date = Date;
8158
8228
  this.EnumMember = EnumMember?.map((a) => new CsdlEnumMember(a));
8229
+ this.TextContent = TextContent;
8159
8230
  }
8160
8231
  toJson() {
8161
- const json = { Name: this.Name };
8232
+ const json = { Property: this.Property };
8233
+ if (this.Bool !== undefined) {
8234
+ json['Bool'] = this.Bool;
8235
+ }
8162
8236
  if (this.String !== undefined) {
8163
8237
  json['String'] = this.String;
8164
8238
  }
@@ -8168,10 +8242,16 @@ class CsdlPropertyValue {
8168
8242
  if (Array.isArray(this.EnumMember) && this.EnumMember.length > 0) {
8169
8243
  json['EnumMember'] = this.EnumMember.map((m) => m.toJson());
8170
8244
  }
8245
+ if (this.TextContent !== undefined) {
8246
+ json['TextContent'] = this.TextContent;
8247
+ }
8171
8248
  return json;
8172
8249
  }
8173
8250
  toConfig() {
8174
- const config = { name: this.Name };
8251
+ const config = { property: this.Property };
8252
+ if (this.Bool !== undefined) {
8253
+ config['bool'] = this.Bool;
8254
+ }
8175
8255
  if (this.String !== undefined) {
8176
8256
  config['string'] = this.String;
8177
8257
  }
@@ -8181,6 +8261,9 @@ class CsdlPropertyValue {
8181
8261
  if (Array.isArray(this.EnumMember) && this.EnumMember.length > 0) {
8182
8262
  config['enumMembers'] = this.EnumMember.map((m) => m.toConfig());
8183
8263
  }
8264
+ if (this.TextContent !== undefined) {
8265
+ config['textContent'] = this.TextContent;
8266
+ }
8184
8267
  return config;
8185
8268
  }
8186
8269
  }
@@ -8401,10 +8484,8 @@ class CsdlEnumType extends CsdlAnnotable {
8401
8484
  return {
8402
8485
  ...super.toConfig(),
8403
8486
  name: this.Name,
8404
- fields: this.Member.reduce((acc, m) => ({
8405
- ...acc,
8406
- [m.Name]: m.toConfig(),
8407
- }), {}),
8487
+ fields: this.Member.reduce((acc, m) => ({ ...acc, [m.Name]: m.toConfig(), }), {}),
8488
+ members: this.Member.reduce((acc, m) => ({ ...acc, [m.Name]: m.Value, [m.Value]: m.Name }), {}),
8408
8489
  flags: this.IsFlags,
8409
8490
  };
8410
8491
  }
@@ -8425,11 +8506,11 @@ class CsdlMember extends CsdlAnnotable {
8425
8506
  return json;
8426
8507
  }
8427
8508
  toConfig(base) {
8428
- const config = {
8509
+ return {
8429
8510
  ...super.toConfig(),
8511
+ name: this.Name,
8430
8512
  value: this.Value,
8431
8513
  };
8432
- return config;
8433
8514
  }
8434
8515
  }
8435
8516
 
@@ -8446,12 +8527,15 @@ class CsdlStructuralProperty extends CsdlAnnotable {
8446
8527
  this.Type = this.Collection ? Type.substring(11, Type.length - 1) : Type;
8447
8528
  }
8448
8529
  toJson() {
8449
- return {
8530
+ const json = {
8450
8531
  ...super.toJson(),
8451
8532
  Name: this.Name,
8452
- Type: this.Collection ? `Collection(${this.Type})` : this.Type,
8453
- Nullable: this.Nullable,
8533
+ Type: this.Collection ? `Collection(${this.Type})` : this.Type
8454
8534
  };
8535
+ if (this.Nullable !== undefined) {
8536
+ json['Nullable'] = this.Nullable;
8537
+ }
8538
+ return json;
8455
8539
  }
8456
8540
  }
8457
8541
  class CsdlProperty extends CsdlStructuralProperty {
@@ -8794,7 +8878,7 @@ class CsdlFunction extends CsdlCallable {
8794
8878
  entitySetPath: this.EntitySetPath,
8795
8879
  bound: this.IsBound,
8796
8880
  composable: this.IsComposable,
8797
- parameters: this.Parameter?.map((p) => p.toConfig()),
8881
+ parameters: this.Parameter?.reduce((acc, p) => ({ ...acc, [p.Name]: p.toConfig() }), {}),
8798
8882
  return: this.ReturnType?.toConfig(),
8799
8883
  };
8800
8884
  }
@@ -8813,7 +8897,7 @@ class CsdlAction extends CsdlCallable {
8813
8897
  name: this.Name,
8814
8898
  entitySetPath: this.EntitySetPath,
8815
8899
  bound: this.IsBound,
8816
- parameters: this.Parameter?.map((p) => p.toConfig()),
8900
+ parameters: this.Parameter?.reduce((acc, p) => ({ ...acc, [p.Name]: p.toConfig() }), {}),
8817
8901
  return: this.ReturnType?.toConfig(),
8818
8902
  };
8819
8903
  }
@@ -8880,24 +8964,51 @@ class CsdlParameter extends CsdlAnnotable {
8880
8964
  this.SRID = SRID;
8881
8965
  }
8882
8966
  toJson() {
8883
- return {
8967
+ const json = {
8884
8968
  ...super.toJson(),
8885
8969
  Name: this.Name,
8886
- Type: this.Collection ? `Collection(${this.Type})` : this.Type,
8887
- Nullable: this.Nullable,
8888
- MaxLength: this.MaxLength,
8889
- Precision: this.Precision,
8890
- Scale: this.Scale,
8891
- SRID: this.SRID,
8970
+ Type: this.Collection ? `Collection(${this.Type})` : this.Type
8892
8971
  };
8972
+ if (this.Nullable !== undefined) {
8973
+ json['Nullable'] = this.Nullable;
8974
+ }
8975
+ if (this.MaxLength !== undefined) {
8976
+ json['MaxLength'] = this.MaxLength;
8977
+ }
8978
+ if (this.Precision !== undefined) {
8979
+ json['Precision'] = this.Precision;
8980
+ }
8981
+ if (this.Scale !== undefined) {
8982
+ json['Scale'] = this.Scale;
8983
+ }
8984
+ if (this.SRID !== undefined) {
8985
+ json['SRID'] = this.SRID;
8986
+ }
8987
+ return json;
8893
8988
  }
8894
8989
  toConfig() {
8895
- return {
8990
+ const config = {
8896
8991
  ...super.toConfig(),
8992
+ name: this.Name,
8897
8993
  type: this.Type,
8898
- nullable: this.Nullable,
8899
- collection: false,
8994
+ collection: this.Collection,
8900
8995
  };
8996
+ if (this.Nullable !== undefined) {
8997
+ config.nullable = this.Nullable;
8998
+ }
8999
+ if (this.MaxLength !== undefined) {
9000
+ config.maxLength = this.MaxLength;
9001
+ }
9002
+ if (this.Precision !== undefined) {
9003
+ config.precision = this.Precision;
9004
+ }
9005
+ if (this.Scale !== undefined) {
9006
+ config.scale = this.Scale;
9007
+ }
9008
+ if (this.SRID !== undefined) {
9009
+ config.srid = this.SRID;
9010
+ }
9011
+ return config;
8901
9012
  }
8902
9013
  }
8903
9014
  class CsdlReturnType {
@@ -8918,20 +9029,47 @@ class CsdlReturnType {
8918
9029
  this.SRID = SRID;
8919
9030
  }
8920
9031
  toJson() {
8921
- return {
8922
- Type: this.Collection ? `Collection(${this.Type})` : this.Type,
8923
- Nullable: this.Nullable,
8924
- MaxLength: this.MaxLength,
8925
- Precision: this.Precision,
8926
- Scale: this.Scale,
8927
- SRID: this.SRID,
9032
+ const json = {
9033
+ Type: this.Collection ? `Collection(${this.Type})` : this.Type
8928
9034
  };
9035
+ if (this.Nullable !== undefined) {
9036
+ json['Nullable'] = this.Nullable;
9037
+ }
9038
+ if (this.MaxLength !== undefined) {
9039
+ json['MaxLength'] = this.MaxLength;
9040
+ }
9041
+ if (this.Precision !== undefined) {
9042
+ json['Precision'] = this.Precision;
9043
+ }
9044
+ if (this.Scale !== undefined) {
9045
+ json['Scale'] = this.Scale;
9046
+ }
9047
+ if (this.SRID !== undefined) {
9048
+ json['SRID'] = this.SRID;
9049
+ }
9050
+ return json;
8929
9051
  }
8930
9052
  toConfig() {
8931
- return {
9053
+ const config = {
8932
9054
  type: this.Type,
8933
- collection: false,
9055
+ collection: this.Collection
8934
9056
  };
9057
+ if (this.Nullable !== undefined) {
9058
+ config.nullable = this.Nullable;
9059
+ }
9060
+ if (this.MaxLength !== undefined) {
9061
+ config.maxLength = this.MaxLength;
9062
+ }
9063
+ if (this.Precision !== undefined) {
9064
+ config.precision = this.Precision;
9065
+ }
9066
+ if (this.Scale !== undefined) {
9067
+ config.scale = this.Scale;
9068
+ }
9069
+ if (this.SRID !== undefined) {
9070
+ config.srid = this.SRID;
9071
+ }
9072
+ return config;
8935
9073
  }
8936
9074
  }
8937
9075
 
@@ -9459,8 +9597,6 @@ class ODataMetadataParser {
9459
9597
  case ODataMetadataParser.ATTRIBUTE_TERM:
9460
9598
  case ODataMetadataParser.ATTRIBUTE_QUALIFIER:
9461
9599
  case ODataMetadataParser.ATTRIBUTE_STRING:
9462
- case ODataMetadataParser.ATTRIBUTE_BOOL:
9463
- case ODataMetadataParser.ATTRIBUTE_INT:
9464
9600
  case ODataMetadataParser.ATTRIBUTE_TARGET_NAMESPACE:
9465
9601
  case ODataMetadataParser.ATTRIBUTE_NAME:
9466
9602
  case ODataMetadataParser.ATTRIBUTE_TYPE:
@@ -9482,6 +9618,7 @@ class ODataMetadataParser {
9482
9618
  case ODataMetadataParser.ATTRIBUTE_BASE_TERM:
9483
9619
  case ODataMetadataParser.ATTRIBUTE_APPLIES_TO:
9484
9620
  return this.getAttributeValue(attributes, field.name);
9621
+ case ODataMetadataParser.ATTRIBUTE_BOOL:
9485
9622
  case ODataMetadataParser.ATTRIBUTE_NULLABLE:
9486
9623
  case ODataMetadataParser.ATTRIBUTE_UNICODE:
9487
9624
  case ODataMetadataParser.ATTRIBUTE_OPEN_TYPE:
@@ -9493,6 +9630,7 @@ class ODataMetadataParser {
9493
9630
  case ODataMetadataParser.ATTRIBUTE_ABSTRACT:
9494
9631
  case ODataMetadataParser.ATTRIBUTE_IS_FLAGS:
9495
9632
  return this.propertyValueToBoolean(this.getAttributeValue(attributes, field.name));
9633
+ case ODataMetadataParser.ATTRIBUTE_INT:
9496
9634
  case ODataMetadataParser.ATTRIBUTE_VALUE:
9497
9635
  case ODataMetadataParser.ATTRIBUTE_MAX_LENGTH:
9498
9636
  case ODataMetadataParser.ATTRIBUTE_PRECISION:
@@ -10112,16 +10250,30 @@ class ODataResponseOptions {
10112
10250
  });
10113
10251
  }
10114
10252
  setCache(cacheControl) {
10115
- cacheControl.split(',').forEach((directive) => {
10116
- if (directive.startsWith(MAX_AGE)) {
10117
- let maxAge = Number(directive.split('=')[1]);
10118
- if (!Number.isNaN(maxAge))
10119
- this.maxAge = maxAge;
10120
- }
10121
- if (['public', 'private', 'no-cache', 'no-store'].indexOf(directive) !== -1) {
10122
- 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;
10123
10275
  }
10124
- });
10276
+ }
10125
10277
  }
10126
10278
  }
10127
10279
 
@@ -10170,6 +10322,17 @@ class ODataResponse extends HttpResponse {
10170
10322
  url: this.url,
10171
10323
  };
10172
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
+ }
10173
10336
  _options;
10174
10337
  get options() {
10175
10338
  if (this._options === undefined) {
@@ -12845,6 +13008,10 @@ class ODataApiOptions {
12845
13008
  * Cache fetch policy
12846
13009
  */
12847
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;
12848
13015
  /**
12849
13016
  * Extra params to be sent in the request
12850
13017
  */
@@ -12876,6 +13043,8 @@ class ODataApiOptions {
12876
13043
  this.withCredentials = config.withCredentials;
12877
13044
  this.stripMetadata = config.stripMetadata || DEFAULT_STRIP_METADATA;
12878
13045
  this.fetchPolicy = config.fetchPolicy || DEFAULT_FETCH_POLICY;
13046
+ this.ignoreCacheControl = config.ignoreCacheControl ?? false;
13047
+ this.cacheInvalidation = config.cacheInvalidation ?? 'entity-set';
12879
13048
  this.bodyQueryOptions = config.bodyQueryOptions || [];
12880
13049
  this.accept = config.accept;
12881
13050
  Object.assign(this.etag, config.etag || {});
@@ -12945,11 +13114,15 @@ class ODataReference extends ODataAnnotatable {
12945
13114
  }
12946
13115
 
12947
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();
12948
13120
  /**
12949
13121
  * Api abstraction for consuming OData services.
12950
13122
  */
12951
13123
  class ODataApi {
12952
13124
  requester;
13125
+ _requestCapture;
12953
13126
  serviceRootUrl;
12954
13127
  metadataUrl;
12955
13128
  name;
@@ -13141,6 +13314,8 @@ class ODataApi {
13141
13314
  bodyQueryOptions: options.bodyQueryOptions,
13142
13315
  reportProgress: options.reportProgress,
13143
13316
  fetchPolicy: options.fetchPolicy,
13317
+ ignoreCacheControl: options.ignoreCacheControl,
13318
+ cacheInvalidation: options.cacheInvalidation,
13144
13319
  maxAge: options.maxAge,
13145
13320
  parserOptions: options.parserOptions,
13146
13321
  withCredentials: options.withCredentials,
@@ -13180,53 +13355,100 @@ class ODataApi {
13180
13355
  * @returns
13181
13356
  */
13182
13357
  handleRequest(req) {
13183
- return this.cache !== undefined && req.isFetch()
13184
- ? this.handleCacheFetch(req)
13185
- : this.cache !== undefined && req.isMutate()
13186
- ? this.handleCacheMutate(req)
13187
- : this.handleRequester(req);
13188
- }
13189
- handleRequester(req) {
13190
- return (this.requester !== undefined ? this.requester(req) : NEVER).pipe(map((res) => res.type === HttpEventType.Response ? ODataResponse.fromHttpResponse(req, res) : res));
13191
- }
13192
- 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) {
13193
13425
  const policy = req.fetchPolicy;
13194
- const cached = this.cache.getResponse(req);
13195
- if (policy === 'no-cache') {
13196
- return this.handleRequester(req);
13197
- }
13426
+ const cached = policy === 'network-only' ? undefined : cache.getResponse(req);
13198
13427
  if (policy === 'cache-only') {
13199
- if (cached !== undefined) {
13428
+ if (cached !== undefined)
13200
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;
13201
13442
  }
13202
- else {
13203
- return throwError(() => new Error('No Cached'));
13204
- }
13205
- }
13206
- let res$ = cached !== undefined && policy !== 'network-only'
13207
- ? policy === 'cache-and-network'
13208
- ? this.handleRequester(req).pipe(startWith(cached))
13209
- : of(cached)
13210
- : this.handleRequester(req);
13211
- if (cached === undefined &&
13212
- (policy === 'cache-first' || policy === 'cache-and-network' || policy === 'network-only')) {
13213
- res$ = res$.pipe(tap((res) => {
13214
- if (res.options.cacheability !== 'no-store') {
13215
- this.cache.putResponse(req, res);
13216
- }
13217
- }));
13218
- }
13219
- return res$;
13220
- }
13221
- handleCacheMutate(req) {
13222
- const requests = req.isBatch()
13223
- ? req.resource.requests().filter((r) => r.isMutate())
13224
- : [req];
13225
- for (var r of requests) {
13226
- const scope = this.cache.scope(r);
13227
- this.cache.forget({ scope });
13228
- }
13229
- 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;
13230
13452
  }
13231
13453
  //# region Find by Type
13232
13454
  // Memoize
@@ -13642,6 +13864,8 @@ function addBody(options, body) {
13642
13864
  body,
13643
13865
  etag: options.etag,
13644
13866
  fetchPolicy: options.fetchPolicy,
13867
+ ignoreCacheControl: options.ignoreCacheControl,
13868
+ cacheInvalidation: options.cacheInvalidation,
13645
13869
  headers: options.headers,
13646
13870
  observe: options.observe,
13647
13871
  params: options.params,
@@ -14209,8 +14433,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
14209
14433
 
14210
14434
  class ODataBaseCache {
14211
14435
  maxAge;
14212
- constructor({ maxAge = DEFAULT_MAXAGE }) {
14436
+ _onError;
14437
+ _error;
14438
+ constructor({ maxAge = DEFAULT_MAXAGE, onError, }) {
14439
+ this.validateMaxAge(maxAge);
14213
14440
  this.maxAge = maxAge;
14441
+ this._onError = onError;
14214
14442
  }
14215
14443
  /**
14216
14444
  * Check if the entry is expired
@@ -14218,7 +14446,7 @@ class ODataBaseCache {
14218
14446
  * @returns Boolean indicating if the entry is expired
14219
14447
  */
14220
14448
  isExpired(entry) {
14221
- return entry.date < Date.now() - entry.maxAge;
14449
+ return entry.date <= Date.now() - entry.maxAge;
14222
14450
  }
14223
14451
  /**
14224
14452
  * Using the resource on the request build an array of string to identify the scope of the request
@@ -14227,11 +14455,11 @@ class ODataBaseCache {
14227
14455
  */
14228
14456
  scope(req) {
14229
14457
  const segments = req.resource.cloneSegments();
14230
- return segments.segments({ key: true }).reduce((acc, s) => {
14231
- if (s.name === PathSegment.entitySet)
14458
+ return segments.segments().reduce((acc, s) => {
14459
+ if (s.name === PathSegment.entitySet || s.name === PathSegment.singleton)
14232
14460
  acc = [...acc, s.path()];
14233
14461
  return acc;
14234
- }, ['request']);
14462
+ }, req.cacheScope);
14235
14463
  }
14236
14464
  /**
14237
14465
  * Using the odata context on the response build an array of string to identify the tags of the response
@@ -14256,11 +14484,13 @@ class ODataBaseCache {
14256
14484
  * @returns The entry to store in the cache
14257
14485
  */
14258
14486
  buildEntry(payload, { maxAge, tags }) {
14487
+ const seconds = maxAge ?? this.maxAge;
14488
+ this.validateMaxAge(seconds);
14259
14489
  return {
14260
14490
  payload,
14261
14491
  date: Date.now(),
14262
- maxAge: (maxAge ?? this.maxAge) * 1000,
14263
- tags: tags ?? [],
14492
+ maxAge: seconds * 1000,
14493
+ tags: [...(tags ?? [])],
14264
14494
  };
14265
14495
  }
14266
14496
  /**
@@ -14271,14 +14501,147 @@ class ODataBaseCache {
14271
14501
  buildKey(names) {
14272
14502
  return names.join(CACHE_KEY_SEPARATOR);
14273
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
+ }
14274
14609
  }
14275
14610
 
14276
14611
  class ODataInMemoryCache extends ODataBaseCache {
14277
14612
  entries;
14278
- constructor({ maxAge } = {}) {
14279
- super({ maxAge });
14613
+ _nextExpiry = Infinity;
14614
+ constructor(options = {}) {
14615
+ super(options);
14280
14616
  this.entries = new Map();
14281
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
+ }
14282
14645
  /**
14283
14646
  * Put some payload in the cache
14284
14647
  * @param name The name for the entry
@@ -14288,9 +14651,11 @@ class ODataInMemoryCache extends ODataBaseCache {
14288
14651
  * @param tags The tags for the entry
14289
14652
  */
14290
14653
  put(name, payload, { maxAge, scope, tags } = {}) {
14654
+ this.checkError();
14291
14655
  const entry = this.buildEntry(payload, { maxAge, tags });
14292
14656
  const key = this.buildKey([...(scope ?? []), name]);
14293
- this.entries.set(key, entry);
14657
+ this.prune();
14658
+ this.setEntry(key, entry);
14294
14659
  }
14295
14660
  /**
14296
14661
  * Return the payload from the cache if it exists and is not expired
@@ -14299,266 +14664,416 @@ class ODataInMemoryCache extends ODataBaseCache {
14299
14664
  * @returns The payload of the entry
14300
14665
  */
14301
14666
  get(name, { scope } = {}) {
14667
+ this.checkError();
14668
+ this.prune();
14302
14669
  const key = this.buildKey([...(scope || []), name]);
14303
14670
  const entry = this.entries.get(key);
14304
14671
  return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined;
14305
14672
  }
14306
- /**
14307
- * Store the response in the cache
14308
- * @param req The request with the resource to store the response
14309
- * @param res The response to store in the cache
14310
- */
14311
- putResponse(req, res) {
14312
- let scope = this.scope(req);
14313
- let tags = this.tags(res);
14314
- this.put(req.cacheKey, res, {
14315
- maxAge: req.maxAge ?? res.options.maxAge,
14316
- scope,
14317
- tags,
14318
- });
14319
- }
14320
- /**
14321
- * Restore the response from the cache
14322
- * @param req The request with the resource to get the response
14323
- * @returns The response from the cache
14324
- */
14325
- getResponse(req) {
14326
- let scope = this.scope(req);
14327
- return this.get(req.cacheKey, { scope });
14328
- }
14329
14673
  /**
14330
14674
  * Remove all cache entries that are matching with the given options
14331
14675
  * @param options The options to forget
14332
14676
  */
14333
- forget({ name, scope = [], tags = [], } = {}) {
14334
- if (name)
14335
- scope.push(name);
14336
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14677
+ forget(options = {}) {
14678
+ this.checkError();
14337
14679
  this.entries.forEach((entry, k) => {
14338
- if (this.isExpired(entry) || // Expired
14339
- (key !== undefined && k.startsWith(key)) || // Key
14340
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1)) // Tags
14341
- ) {
14342
- this.entries.delete(k);
14343
- }
14680
+ if (this.matches(k, entry, options))
14681
+ this.deleteEntry(k);
14344
14682
  });
14345
14683
  }
14346
14684
  /**
14347
14685
  * Remove all cache entries
14348
14686
  */
14349
14687
  flush() {
14350
- this.entries = new Map();
14688
+ this.checkError();
14689
+ this.clearEntries();
14351
14690
  }
14691
+ /** Return the number of unexpired entries. */
14352
14692
  size() {
14693
+ this.checkError();
14694
+ this.prune();
14353
14695
  return this.entries.size;
14354
14696
  }
14355
14697
  }
14356
14698
 
14357
- class ODataInStorageCache extends ODataBaseCache {
14699
+ class ODataInStorageCache extends ODataInMemoryCache {
14358
14700
  prefix;
14359
- storage;
14360
- constructor({ prefix, storage = sessionStorage, maxAge, }) {
14361
- super({ maxAge });
14701
+ _storage;
14702
+ _persistent = true;
14703
+ _memoryOnly = new Set();
14704
+ constructor({ prefix, storage, ...options }) {
14705
+ super(options);
14362
14706
  this.prefix = prefix;
14363
- 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();
14364
14727
  }
14365
14728
  buildKey(names) {
14366
14729
  return super.buildKey([this.prefix, ...names]);
14367
14730
  }
14368
- put(name, payload, { maxAge, scope, tags } = {}) {
14369
- const entry = this.buildEntry(payload, { maxAge, tags });
14370
- const key = this.buildKey([...(scope ?? []), name]);
14371
- 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);
14372
14736
  }
14373
- get(name, { scope } = {}) {
14374
- const key = this.buildKey([...(scope || []), name]);
14375
- const entry = JSON.parse(this.storage.getItem(key) ?? '{}');
14376
- 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
+ }
14377
14754
  }
14378
- forget({ name, scope = [], tags = [], }) {
14379
- if (name)
14380
- scope.push(name);
14381
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14382
- Object.keys(this.storage)
14383
- .filter((k) => k.startsWith(this.prefix))
14384
- .forEach((k) => {
14385
- const entry = JSON.parse(this.storage.getItem(k) ?? '{}');
14386
- if (this.isExpired(entry) || // Expired
14387
- (key !== undefined && k.startsWith(key)) || // Key
14388
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1)) // Tags
14389
- ) {
14390
- this.storage.removeItem(k);
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
+ }
14818
+ }
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');
14391
14836
  }
14392
- });
14837
+ this.storage.setItem(key, serialized);
14838
+ }
14839
+ catch (error) {
14840
+ this.disablePersistence(error);
14841
+ }
14393
14842
  }
14394
- /**
14395
- * Flush the cache and clean the storage
14396
- */
14397
- flush() {
14398
- Object.keys(this.storage)
14399
- .filter((k) => k.startsWith(this.prefix))
14400
- .forEach((k) => {
14401
- this.storage.removeItem(k);
14402
- });
14843
+ deleteEntry(key) {
14844
+ super.deleteEntry(key);
14845
+ this._memoryOnly.delete(key);
14846
+ this.removeStoredEntry(key);
14403
14847
  }
14404
- /**
14405
- * Store the response in the cache
14406
- * @param req The request with the resource to store the response
14407
- * @param res The response to store in the cache
14408
- */
14409
- putResponse(req, res) {
14410
- const scope = this.scope(req);
14411
- const tags = this.tags(res);
14412
- this.put(req.cacheKey, res.toJson(), {
14413
- maxAge: req.maxAge ?? res.options.maxAge,
14414
- scope,
14415
- tags,
14416
- });
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);
14417
14857
  }
14418
- /**
14419
- * Restore the response from the cache
14420
- * @param req The request with the resource to get the response
14421
- * @returns The response from the cache
14422
- */
14423
- getResponse(req) {
14424
- const scope = this.scope(req);
14425
- const data = this.get(req.cacheKey, { scope });
14426
- 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));
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 });
14427
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. */
14428
14882
  size() {
14429
- return Object.keys(this.storage).filter((k) => k.startsWith(this.prefix)).length;
14883
+ this.loadFromStorage();
14884
+ return super.size();
14430
14885
  }
14431
14886
  }
14432
14887
 
14433
- class ODataIndexedDBCache extends ODataBaseCache {
14888
+ class ODataIndexedDBCache extends ODataInMemoryCache {
14434
14889
  name;
14435
14890
  version;
14436
14891
  store;
14437
- entries;
14438
- _db;
14439
- constructor({ name = 'ODataCache', store = 'cache', version = 1, maxAge, } = {}) {
14440
- 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);
14441
14900
  this.name = name;
14442
14901
  this.store = store;
14443
14902
  this.version = version;
14444
- this.entries = new Map();
14445
- 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
+ }
14926
+ }
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);
14446
14933
  }
14447
- initDb() {
14934
+ openDb() {
14448
14935
  return new Promise((resolve, reject) => {
14449
14936
  const request = indexedDB.open(this.name, this.version);
14450
- request.onupgradeneeded = (event) => {
14451
- const db = event.target.result;
14452
- if (!db.objectStoreNames.contains(this.store)) {
14453
- 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;
14454
14946
  }
14947
+ const db = request.result;
14948
+ if (!db.objectStoreNames.contains(this.store))
14949
+ db.createObjectStore(this.store);
14455
14950
  };
14456
- request.onsuccess = async (event) => {
14457
- const db = event.target.result;
14458
- 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
+ };
14459
14962
  resolve(db);
14460
14963
  };
14461
- request.onerror = (event) => {
14462
- reject(event.target.error);
14463
- };
14964
+ request.onerror = () => fail(request.error);
14965
+ request.onblocked = () => fail(new Error('IndexedDB cache opening was blocked'));
14464
14966
  });
14465
14967
  }
14466
14968
  loadFromDb(db) {
14467
14969
  return new Promise((resolve, reject) => {
14468
- const transaction = db.transaction(this.store, 'readonly');
14469
- const store = transaction.objectStore(this.store);
14470
- const request = store.openCursor();
14471
- request.onsuccess = (event) => {
14472
- const cursor = event.target.result;
14473
- if (cursor) {
14474
- this.entries.set(cursor.key, cursor.value);
14475
- 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();
14476
14984
  }
14477
14985
  else {
14478
- resolve();
14986
+ entries.set(cursor.key, value);
14479
14987
  }
14988
+ cursor.continue();
14480
14989
  };
14481
- request.onerror = (event) => {
14482
- reject(event.target.error);
14483
- };
14484
- });
14485
- }
14486
- async withDb(fn) {
14487
- const db = await this._db;
14488
- 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;
14489
15024
  }
14490
- put(name, payload, { maxAge, scope, tags } = {}) {
14491
- const entry = this.buildEntry(payload, { maxAge, tags });
14492
- const key = this.buildKey([...(scope ?? []), name]);
14493
- this.entries.set(key, entry);
14494
- 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;
14495
15031
  return new Promise((resolve, reject) => {
14496
15032
  const transaction = db.transaction(this.store, 'readwrite');
14497
- const store = transaction.objectStore(this.store);
14498
- store.put(entry, key);
14499
15033
  transaction.oncomplete = () => resolve();
14500
- 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
+ }
14501
15043
  });
14502
- });
14503
- }
14504
- get(name, { scope } = {}) {
14505
- const key = this.buildKey([...(scope || []), name]);
14506
- const entry = this.entries.get(key);
14507
- return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined;
14508
- }
14509
- putResponse(req, res) {
14510
- const scope = this.scope(req);
14511
- const tags = this.tags(res);
14512
- this.put(req.cacheKey, res.toJson(), {
14513
- maxAge: req.maxAge ?? res.options.maxAge,
14514
- scope,
14515
- tags,
14516
- });
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);
14517
15055
  }
14518
- getResponse(req) {
14519
- const scope = this.scope(req);
14520
- const data = this.get(req.cacheKey, { scope });
14521
- return data !== undefined ? ODataResponse.fromJson(req, data) : undefined;
15056
+ deleteEntry(key) {
15057
+ super.deleteEntry(key);
15058
+ if (this._hydrated)
15059
+ this.persist((store) => store.delete(key), true);
14522
15060
  }
14523
- forget({ name, scope = [], tags = [], } = {}) {
14524
- if (name)
14525
- scope.push(name);
14526
- const key = scope.length > 0 ? this.buildKey(scope) : undefined;
14527
- const keysToDelete = [];
14528
- this.entries.forEach((entry, k) => {
14529
- if (this.isExpired(entry) ||
14530
- (key !== undefined && k.startsWith(key)) ||
14531
- (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1))) {
14532
- keysToDelete.push(k);
14533
- }
14534
- });
14535
- keysToDelete.forEach((k) => this.entries.delete(k));
14536
- if (keysToDelete.length > 0) {
14537
- this.withDb((db) => {
14538
- return new Promise((resolve, reject) => {
14539
- const transaction = db.transaction(this.store, 'readwrite');
14540
- const store = transaction.objectStore(this.store);
14541
- keysToDelete.forEach((k) => store.delete(k));
14542
- transaction.oncomplete = () => resolve();
14543
- transaction.onerror = (event) => reject(event.target.error);
14544
- });
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 ?? [])] },
14545
15074
  });
14546
15075
  }
14547
- }
14548
- flush() {
14549
- this.entries = new Map();
14550
- this.withDb((db) => {
14551
- return new Promise((resolve, reject) => {
14552
- const transaction = db.transaction(this.store, 'readwrite');
14553
- const store = transaction.objectStore(this.store);
14554
- store.clear();
14555
- transaction.oncomplete = () => resolve();
14556
- transaction.onerror = (event) => reject(event.target.error);
14557
- });
14558
- });
14559
- }
14560
- size() {
14561
- return this.entries.size;
15076
+ super.forget(options);
14562
15077
  }
14563
15078
  }
14564
15079