http-request-manager 18.12.9 → 18.13.1
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,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, Injectable, APP_ID, Inject, InjectionToken, isDevMode, signal, effect, computed, Injector, Optional, EventEmitter, Input, Output, ViewEncapsulation, Component, NgModule, ViewChild } from '@angular/core';
|
|
3
3
|
import { ComponentStore } from '@ngrx/component-store';
|
|
4
|
-
import { map, catchError,
|
|
4
|
+
import { map, catchError, take, filter, tap, finalize, takeWhile, retry, startWith, mergeMap, takeUntil, concatMap, toArray, withLatestFrom, switchMap, delay, scan, distinctUntilChanged } from 'rxjs/operators';
|
|
5
5
|
import { HttpClient, HttpHeaders, HttpEventType, HttpHeaderResponse, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
6
6
|
import * as CryptoJS from 'crypto-js';
|
|
7
|
-
import { from, BehaviorSubject, EMPTY, throwError, defer, interval, timer, Subject, of,
|
|
7
|
+
import { from, BehaviorSubject, EMPTY, throwError, defer, interval, timer, Subject, of, merge, Subscription, take as take$1, catchError as catchError$1, map as map$1, tap as tap$1, switchMap as switchMap$1, startWith as startWith$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, filter as filter$1, takeUntil as takeUntil$1, ReplaySubject } from 'rxjs';
|
|
8
8
|
import { toObservable } from '@angular/core/rxjs-interop';
|
|
9
9
|
import { ToastMessageDisplayService, ToastDisplay, ToastColors, ToastMessageDisplayModule } from 'toast-message-display';
|
|
10
10
|
import Dexie from 'dexie';
|
|
@@ -875,6 +875,369 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
875
875
|
}]
|
|
876
876
|
}], ctorParameters: () => [] });
|
|
877
877
|
|
|
878
|
+
class PathTrackerStateModel {
|
|
879
|
+
constructor(baselineQuery, consumedValuesByKey = {}, watchExpiresAt) {
|
|
880
|
+
this.baselineQuery = baselineQuery;
|
|
881
|
+
this.consumedValuesByKey = consumedValuesByKey;
|
|
882
|
+
this.watchExpiresAt = watchExpiresAt;
|
|
883
|
+
}
|
|
884
|
+
static adapt(item) {
|
|
885
|
+
return new PathTrackerStateModel(item?.baselineQuery, item?.consumedValuesByKey, item?.watchExpiresAt);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
class QueryTrackerStateModel {
|
|
890
|
+
constructor(paths = {}) {
|
|
891
|
+
this.paths = paths;
|
|
892
|
+
}
|
|
893
|
+
static adapt(item) {
|
|
894
|
+
return new QueryTrackerStateModel(item?.paths
|
|
895
|
+
? Object.keys(item.paths).reduce((acc, key) => {
|
|
896
|
+
acc[key] = PathTrackerStateModel.adapt(item.paths[key]);
|
|
897
|
+
return acc;
|
|
898
|
+
}, {})
|
|
899
|
+
: {});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const TRACKER_STORE_NAME = 'query_params_tracker';
|
|
904
|
+
const TRACKER_SESSION_INIT_KEY = 'query_params_tracker_initialized';
|
|
905
|
+
const DEFAULT_TRACKER_OPTIONS = SettingOptions.adapt({
|
|
906
|
+
storage: StorageType.GLOBAL,
|
|
907
|
+
encrypted: false,
|
|
908
|
+
});
|
|
909
|
+
class QueryParamsTrackerService {
|
|
910
|
+
constructor() {
|
|
911
|
+
this.state = { paths: {} };
|
|
912
|
+
this.stateRestored = false;
|
|
913
|
+
this.localStorageManager = inject(LocalStorageManagerService);
|
|
914
|
+
}
|
|
915
|
+
clearTracking(resetSessionInit = false) {
|
|
916
|
+
this.state = { paths: {} };
|
|
917
|
+
this.localStorageManager.deleteStore({ name: TRACKER_STORE_NAME });
|
|
918
|
+
if (resetSessionInit && this.hasSessionStorage()) {
|
|
919
|
+
sessionStorage.removeItem(TRACKER_SESSION_INIT_KEY);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
checkRequestOptions(requestOptions = [], options = {}) {
|
|
923
|
+
this.ensureStateRestored();
|
|
924
|
+
const normalized = this.normalizeRequestOptions(requestOptions);
|
|
925
|
+
if (!normalized.pathKey)
|
|
926
|
+
return false;
|
|
927
|
+
this.cleanupExpiredEntries();
|
|
928
|
+
if (!normalized.hasQuery) {
|
|
929
|
+
this.ensurePathState(normalized.pathKey);
|
|
930
|
+
this.persistState();
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
if (options.mode === 'exact') {
|
|
934
|
+
return this.checkExact(normalized, options);
|
|
935
|
+
}
|
|
936
|
+
return this.checkVariation(normalized, options);
|
|
937
|
+
}
|
|
938
|
+
matchesPath(requestOptions = [], expectedPathOptions = []) {
|
|
939
|
+
this.ensureStateRestored();
|
|
940
|
+
const requestPath = this.normalizeRequestOptions(requestOptions).pathKey;
|
|
941
|
+
const expectedPath = this.normalizeRequestOptions(expectedPathOptions).pathKey;
|
|
942
|
+
return Boolean(requestPath) && requestPath === expectedPath;
|
|
943
|
+
}
|
|
944
|
+
checkExact(normalized, options) {
|
|
945
|
+
const pathState = this.ensurePathState(normalized.pathKey);
|
|
946
|
+
const filteredQuery = normalized.query;
|
|
947
|
+
if (Object.keys(filteredQuery).length === 0) {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
if (!pathState.baselineQuery) {
|
|
951
|
+
pathState.baselineQuery = filteredQuery;
|
|
952
|
+
this.persistState();
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
return this.stringifyQuery(pathState.baselineQuery) === this.stringifyQuery(filteredQuery);
|
|
956
|
+
}
|
|
957
|
+
checkVariation(normalized, options) {
|
|
958
|
+
const pathState = this.ensurePathState(normalized.pathKey);
|
|
959
|
+
const filteredQuery = normalized.query;
|
|
960
|
+
const keys = Object.keys(filteredQuery);
|
|
961
|
+
if (keys.length === 0) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
this.resetPathStateIfExpired(pathState);
|
|
965
|
+
let accepted = false;
|
|
966
|
+
keys.forEach((key) => {
|
|
967
|
+
const currentValue = filteredQuery[key];
|
|
968
|
+
const consumed = pathState.consumedValuesByKey[key] || [];
|
|
969
|
+
if (!consumed.includes(currentValue)) {
|
|
970
|
+
pathState.consumedValuesByKey[key] = [...consumed, currentValue];
|
|
971
|
+
accepted = true;
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
if (accepted) {
|
|
975
|
+
if (!pathState.baselineQuery) {
|
|
976
|
+
pathState.baselineQuery = filteredQuery;
|
|
977
|
+
}
|
|
978
|
+
pathState.watchExpiresAt = this.buildExpiryEpoch(typeof options.watchExpiresAt !== 'undefined' ? options.watchExpiresAt : options.watchParamsExpire);
|
|
979
|
+
this.persistState();
|
|
980
|
+
}
|
|
981
|
+
return accepted;
|
|
982
|
+
}
|
|
983
|
+
normalizeRequestOptions(requestOptions) {
|
|
984
|
+
const params = Array.isArray(requestOptions) ? requestOptions : [];
|
|
985
|
+
const pathSegments = [];
|
|
986
|
+
const queryObjects = [];
|
|
987
|
+
params.forEach((item) => {
|
|
988
|
+
if (this.isPlainObject(item)) {
|
|
989
|
+
queryObjects.push(item);
|
|
990
|
+
}
|
|
991
|
+
else if (typeof item !== 'undefined' && item !== null) {
|
|
992
|
+
const parsed = this.parsePathSegment(String(item));
|
|
993
|
+
if (parsed.pathSegment) {
|
|
994
|
+
pathSegments.push(parsed.pathSegment);
|
|
995
|
+
}
|
|
996
|
+
if (Object.keys(parsed.queryObject).length > 0) {
|
|
997
|
+
queryObjects.push(parsed.queryObject);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
const query = queryObjects.reduce((acc, current) => {
|
|
1002
|
+
Object.keys(current).forEach((key) => {
|
|
1003
|
+
const normalizedKey = this.normalizeParamKey(key);
|
|
1004
|
+
const value = current[key];
|
|
1005
|
+
if (!normalizedKey || typeof value === 'undefined' || value === null || value === '') {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
acc[normalizedKey] = this.normalizeParamValue(value);
|
|
1009
|
+
});
|
|
1010
|
+
return acc;
|
|
1011
|
+
}, {});
|
|
1012
|
+
return {
|
|
1013
|
+
pathKey: this.normalizePath(pathSegments),
|
|
1014
|
+
query,
|
|
1015
|
+
hasQuery: Object.keys(query).length > 0,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
parsePathSegment(rawSegment) {
|
|
1019
|
+
const [pathPart, ...queryParts] = String(rawSegment).split('?');
|
|
1020
|
+
const queryString = queryParts.join('?');
|
|
1021
|
+
const queryObject = {};
|
|
1022
|
+
if (queryString) {
|
|
1023
|
+
queryString.split('&').forEach((pair) => {
|
|
1024
|
+
if (!pair)
|
|
1025
|
+
return;
|
|
1026
|
+
const [rawKey, ...rawValueParts] = pair.split('=');
|
|
1027
|
+
const key = this.safeDecode(rawKey).trim();
|
|
1028
|
+
const value = this.safeDecode(rawValueParts.join('=')).trim();
|
|
1029
|
+
if (!key)
|
|
1030
|
+
return;
|
|
1031
|
+
queryObject[key] = value;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
pathSegment: pathPart,
|
|
1036
|
+
queryObject,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
safeDecode(value) {
|
|
1040
|
+
if (typeof value === 'undefined') {
|
|
1041
|
+
return '';
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return decodeURIComponent(value);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return String(value);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
normalizePath(pathSegments) {
|
|
1051
|
+
return pathSegments
|
|
1052
|
+
.map((segment) => String(segment).trim())
|
|
1053
|
+
.filter((segment) => segment.length > 0)
|
|
1054
|
+
.join('/')
|
|
1055
|
+
.replace(/([^:]\/+)\/+/g, '$1')
|
|
1056
|
+
.replace(/^\//, '')
|
|
1057
|
+
.replace(/\/$/, '');
|
|
1058
|
+
}
|
|
1059
|
+
normalizeParamKey(key) {
|
|
1060
|
+
return String(key).trim().toLowerCase();
|
|
1061
|
+
}
|
|
1062
|
+
normalizeParamValue(value) {
|
|
1063
|
+
if (Array.isArray(value)) {
|
|
1064
|
+
return value.map((item) => this.normalizeParamValue(item)).join(',');
|
|
1065
|
+
}
|
|
1066
|
+
if (this.isPlainObject(value)) {
|
|
1067
|
+
return this.stringifyQuery(Object.keys(value).reduce((acc, key) => {
|
|
1068
|
+
acc[this.normalizeParamKey(key)] = this.normalizeParamValue(value[key]);
|
|
1069
|
+
return acc;
|
|
1070
|
+
}, {}));
|
|
1071
|
+
}
|
|
1072
|
+
return String(value).trim().toLowerCase();
|
|
1073
|
+
}
|
|
1074
|
+
buildExpiryEpoch(expireIn) {
|
|
1075
|
+
if (typeof expireIn === 'undefined' || expireIn === null || expireIn === '') {
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
if (typeof expireIn === 'number' && Number.isFinite(expireIn) && expireIn > 0) {
|
|
1079
|
+
return Math.floor(Date.now() / 1000) + Math.floor(expireIn);
|
|
1080
|
+
}
|
|
1081
|
+
const raw = String(expireIn).trim().toLowerCase().replace(/\s+/g, '');
|
|
1082
|
+
const parsed = raw.match(/^(\d+)(y|w|d|hr|h|mn|min|m|s)$/);
|
|
1083
|
+
if (!parsed) {
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
const value = Number(parsed[1]);
|
|
1087
|
+
const unit = parsed[2];
|
|
1088
|
+
const toSeconds = {
|
|
1089
|
+
y: 31556926,
|
|
1090
|
+
w: 604800,
|
|
1091
|
+
d: 86400,
|
|
1092
|
+
hr: 3600,
|
|
1093
|
+
h: 3600,
|
|
1094
|
+
mn: 60,
|
|
1095
|
+
min: 60,
|
|
1096
|
+
m: 60,
|
|
1097
|
+
s: 1,
|
|
1098
|
+
};
|
|
1099
|
+
const seconds = toSeconds[unit];
|
|
1100
|
+
if (!seconds) {
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
return Math.floor(Date.now() / 1000) + value * seconds;
|
|
1104
|
+
}
|
|
1105
|
+
resetPathStateIfExpired(pathState) {
|
|
1106
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1107
|
+
if (pathState.watchExpiresAt && pathState.watchExpiresAt <= now) {
|
|
1108
|
+
pathState.consumedValuesByKey = {};
|
|
1109
|
+
pathState.watchExpiresAt = undefined;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
cleanupExpiredEntries() {
|
|
1113
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1114
|
+
Object.keys(this.state.paths).forEach((pathKey) => {
|
|
1115
|
+
const pathState = this.state.paths[pathKey];
|
|
1116
|
+
if (pathState.watchExpiresAt && pathState.watchExpiresAt <= now) {
|
|
1117
|
+
pathState.consumedValuesByKey = {};
|
|
1118
|
+
pathState.watchExpiresAt = undefined;
|
|
1119
|
+
}
|
|
1120
|
+
const hasBaseline = Boolean(pathState.baselineQuery && Object.keys(pathState.baselineQuery).length > 0);
|
|
1121
|
+
const hasTrackedValues = Object.keys(pathState.consumedValuesByKey).some((key) => (pathState.consumedValuesByKey[key] || []).length > 0);
|
|
1122
|
+
if (!hasBaseline && !hasTrackedValues) {
|
|
1123
|
+
delete this.state.paths[pathKey];
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
ensurePathState(pathKey) {
|
|
1128
|
+
if (!this.state.paths[pathKey]) {
|
|
1129
|
+
this.state.paths[pathKey] = {
|
|
1130
|
+
consumedValuesByKey: {},
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
return this.state.paths[pathKey];
|
|
1134
|
+
}
|
|
1135
|
+
ensureStateRestored() {
|
|
1136
|
+
if (this.stateRestored) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
this.stateRestored = true;
|
|
1140
|
+
this.initializeTrackingForSession();
|
|
1141
|
+
this.restoreState();
|
|
1142
|
+
}
|
|
1143
|
+
initializeTrackingForSession() {
|
|
1144
|
+
if (!this.hasSessionStorage()) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const initialized = sessionStorage.getItem(TRACKER_SESSION_INIT_KEY);
|
|
1148
|
+
if (initialized === '1') {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
this.clearTracking();
|
|
1152
|
+
sessionStorage.setItem(TRACKER_SESSION_INIT_KEY, '1');
|
|
1153
|
+
}
|
|
1154
|
+
restoreState() {
|
|
1155
|
+
this.localStorageManager.store$(TRACKER_STORE_NAME)
|
|
1156
|
+
.pipe(take(1))
|
|
1157
|
+
.subscribe({
|
|
1158
|
+
next: (storedState) => {
|
|
1159
|
+
if (this.isTrackerState(storedState)) {
|
|
1160
|
+
this.state = QueryTrackerStateModel.adapt(storedState);
|
|
1161
|
+
this.cleanupExpiredEntries();
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
this.state = { paths: {} };
|
|
1165
|
+
},
|
|
1166
|
+
error: () => {
|
|
1167
|
+
this.state = { paths: {} };
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
persistState() {
|
|
1172
|
+
this.localStorageManager.storeExists$(TRACKER_STORE_NAME)
|
|
1173
|
+
.pipe(take(1))
|
|
1174
|
+
.subscribe((exists) => {
|
|
1175
|
+
if (exists) {
|
|
1176
|
+
this.localStorageManager.updateStore({ name: TRACKER_STORE_NAME, data: this.state });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
this.localStorageManager.createStore({
|
|
1180
|
+
name: TRACKER_STORE_NAME,
|
|
1181
|
+
data: this.state,
|
|
1182
|
+
options: DEFAULT_TRACKER_OPTIONS,
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
stringifyQuery(query) {
|
|
1187
|
+
return JSON.stringify(Object.keys(query)
|
|
1188
|
+
.sort()
|
|
1189
|
+
.reduce((acc, key) => {
|
|
1190
|
+
acc[key] = query[key];
|
|
1191
|
+
return acc;
|
|
1192
|
+
}, {}));
|
|
1193
|
+
}
|
|
1194
|
+
isTrackerState(value) {
|
|
1195
|
+
return this.isPlainObject(value) && this.isPlainObject(value.paths);
|
|
1196
|
+
}
|
|
1197
|
+
isPlainObject(value) {
|
|
1198
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
1199
|
+
}
|
|
1200
|
+
hasSessionStorage() {
|
|
1201
|
+
try {
|
|
1202
|
+
return typeof sessionStorage !== 'undefined';
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1209
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, providedIn: 'root' }); }
|
|
1210
|
+
}
|
|
1211
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, decorators: [{
|
|
1212
|
+
type: Injectable,
|
|
1213
|
+
args: [{
|
|
1214
|
+
providedIn: 'root'
|
|
1215
|
+
}]
|
|
1216
|
+
}] });
|
|
1217
|
+
|
|
1218
|
+
class NormalizedRequestOptionsModel {
|
|
1219
|
+
constructor(pathKey = '', query = {}, hasQuery = false) {
|
|
1220
|
+
this.pathKey = pathKey;
|
|
1221
|
+
this.query = query;
|
|
1222
|
+
this.hasQuery = hasQuery;
|
|
1223
|
+
}
|
|
1224
|
+
static adapt(item) {
|
|
1225
|
+
return new NormalizedRequestOptionsModel(item?.pathKey, item?.query, item?.hasQuery);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
class QueryParamsTrackerOptionsModel {
|
|
1230
|
+
constructor(mode, watchParams, watchExpiresAt, watchParamsExpire) {
|
|
1231
|
+
this.mode = mode;
|
|
1232
|
+
this.watchParams = watchParams;
|
|
1233
|
+
this.watchExpiresAt = watchExpiresAt;
|
|
1234
|
+
this.watchParamsExpire = watchParamsExpire;
|
|
1235
|
+
}
|
|
1236
|
+
static adapt(item) {
|
|
1237
|
+
return new QueryParamsTrackerOptionsModel(item?.mode, Array.isArray(item?.watchParams) ? item.watchParams : [], item?.watchExpiresAt, item?.watchParamsExpire);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
878
1241
|
/**
|
|
879
1242
|
* Stateful processor for managing streaming data and events
|
|
880
1243
|
*/
|
|
@@ -884,7 +1247,6 @@ class StreamingProcessor {
|
|
|
884
1247
|
this.contentType = '';
|
|
885
1248
|
this.maxBufferSize = 10 * 1024 * 1024; // 10MB limit
|
|
886
1249
|
this.lastParsedLength = 0; // Track parsed position to avoid duplicates
|
|
887
|
-
this.lastParsedLineEnd = 0; // Track end of last-parsed complete line for NDJSON incremental parsing
|
|
888
1250
|
this.streamConfig = config || { streamType: StreamType.AI_STREAMING };
|
|
889
1251
|
}
|
|
890
1252
|
/**
|
|
@@ -907,63 +1269,41 @@ class StreamingProcessor {
|
|
|
907
1269
|
return null;
|
|
908
1270
|
case HttpEventType.DownloadProgress:
|
|
909
1271
|
if (event.partialText) {
|
|
910
|
-
|
|
911
|
-
this.buffer = event.partialText;
|
|
1272
|
+
this.appendToBuffer(event.partialText);
|
|
912
1273
|
const parsedData = this.parseBuffer();
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
// received = cumulative total for progress display
|
|
918
|
-
const received = isIncremental
|
|
919
|
-
? this.lastParsedLength + parsedData.length
|
|
920
|
-
: parsedData.length;
|
|
921
|
-
this.lastParsedLength = received;
|
|
922
|
-
// Return null when no new items — prevents empty array emissions
|
|
923
|
-
if (newItems.length === 0) {
|
|
924
|
-
return null;
|
|
925
|
-
}
|
|
926
|
-
const total = this.totalFromHeader;
|
|
927
|
-
const percent = total
|
|
928
|
-
? Math.min(100, Math.round((received / total) * 100))
|
|
929
|
-
: 0;
|
|
1274
|
+
// Only return NEW items since last parse (progressive updates)
|
|
1275
|
+
const newItems = parsedData.slice(this.lastParsedLength);
|
|
1276
|
+
this.lastParsedLength = parsedData.length;
|
|
1277
|
+
// Calculate progress
|
|
930
1278
|
const progress = {
|
|
931
|
-
received,
|
|
932
|
-
total,
|
|
933
|
-
percent
|
|
1279
|
+
received: this.lastParsedLength,
|
|
1280
|
+
total: this.totalFromHeader,
|
|
1281
|
+
percent: this.totalFromHeader
|
|
1282
|
+
? Math.round((this.lastParsedLength / this.totalFromHeader) * 100)
|
|
1283
|
+
: 0,
|
|
934
1284
|
stage: 'streaming'
|
|
935
1285
|
};
|
|
936
1286
|
return {
|
|
937
|
-
data: newItems,
|
|
1287
|
+
data: newItems.length > 0 ? newItems : [],
|
|
938
1288
|
progress
|
|
939
1289
|
};
|
|
940
1290
|
}
|
|
941
1291
|
return null;
|
|
942
1292
|
case HttpEventType.Response:
|
|
943
1293
|
if (event.body) {
|
|
944
|
-
|
|
945
|
-
this.buffer = event.body;
|
|
1294
|
+
this.appendToBuffer(event.body);
|
|
946
1295
|
const parsedData = this.parseBuffer();
|
|
947
|
-
//
|
|
948
|
-
// For other formats, parsedData is cumulative so slice from lastParsedLength.
|
|
949
|
-
const isIncremental = this.streamConfig.streamType === StreamType.NDJSON;
|
|
950
|
-
const remaining = isIncremental ? parsedData : parsedData.slice(this.lastParsedLength);
|
|
951
|
-
const received = isIncremental
|
|
952
|
-
? this.lastParsedLength + parsedData.length
|
|
953
|
-
: parsedData.length;
|
|
954
|
-
this.lastParsedLength = received;
|
|
955
|
-
const total = this.totalFromHeader;
|
|
956
|
-
const percent = total
|
|
957
|
-
? Math.min(100, Math.round((received / total) * 100))
|
|
958
|
-
: 100;
|
|
1296
|
+
// Calculate final progress
|
|
959
1297
|
const progress = {
|
|
960
|
-
received,
|
|
961
|
-
total,
|
|
962
|
-
percent
|
|
1298
|
+
received: parsedData.length,
|
|
1299
|
+
total: this.totalFromHeader,
|
|
1300
|
+
percent: this.totalFromHeader
|
|
1301
|
+
? Math.round((parsedData.length / this.totalFromHeader) * 100)
|
|
1302
|
+
: 100,
|
|
963
1303
|
stage: 'complete'
|
|
964
1304
|
};
|
|
965
1305
|
return {
|
|
966
|
-
data:
|
|
1306
|
+
data: parsedData.length > 0 ? parsedData : [],
|
|
967
1307
|
progress
|
|
968
1308
|
};
|
|
969
1309
|
}
|
|
@@ -980,40 +1320,23 @@ class StreamingProcessor {
|
|
|
980
1320
|
// Implement sliding window if buffer gets too large
|
|
981
1321
|
if (this.buffer.length > this.maxBufferSize) {
|
|
982
1322
|
this.buffer = this.buffer.slice(-this.maxBufferSize / 2);
|
|
983
|
-
this.lastParsedLineEnd = 0;
|
|
984
|
-
this.lastParsedLength = 0;
|
|
985
1323
|
}
|
|
986
1324
|
}
|
|
987
1325
|
/**
|
|
988
|
-
* Parse current buffer content
|
|
989
|
-
*
|
|
990
|
-
* Per-record parsing failures are already handled by the inner parsers
|
|
991
|
-
* (`safeJsonParse`, `parseNdjson`, `parseEventStream`, `extractJsonObjects`),
|
|
992
|
-
* which skip malformed records and return what was successfully parsed.
|
|
993
|
-
* No outer try/catch — wrapping these calls in `catch { return []; }`
|
|
994
|
-
* would discard every successfully-parsed record in the buffer whenever a
|
|
995
|
-
* single unexpected error bubbles up, instead of omitting the bad record.
|
|
1326
|
+
* Parse current buffer content
|
|
996
1327
|
*/
|
|
997
1328
|
parseBuffer() {
|
|
998
1329
|
if (!this.buffer.trim()) {
|
|
999
1330
|
return [];
|
|
1000
1331
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
// Find the last newline in the buffer — everything after it is an incomplete line.
|
|
1004
|
-
const lastNewlineIndex = this.buffer.lastIndexOf('\n');
|
|
1005
|
-
if (lastNewlineIndex === -1) {
|
|
1006
|
-
// No complete lines yet
|
|
1007
|
-
return [];
|
|
1008
|
-
}
|
|
1009
|
-
// Parse only the content from lastParsedLineEnd up to (and including) the last newline
|
|
1010
|
-
const bufferToParse = this.buffer.substring(this.lastParsedLineEnd, lastNewlineIndex + 1);
|
|
1011
|
-
this.lastParsedLineEnd = lastNewlineIndex + 1;
|
|
1012
|
-
const result = parseStreamData(bufferToParse, this.contentType, this.streamConfig);
|
|
1332
|
+
try {
|
|
1333
|
+
const result = parseStreamData(this.buffer, this.contentType, this.streamConfig);
|
|
1013
1334
|
return result;
|
|
1014
1335
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
console.warn('Failed to parse streaming data:', error);
|
|
1338
|
+
return [];
|
|
1339
|
+
}
|
|
1017
1340
|
}
|
|
1018
1341
|
/**
|
|
1019
1342
|
* Reset processor state
|
|
@@ -1022,7 +1345,6 @@ class StreamingProcessor {
|
|
|
1022
1345
|
this.buffer = '';
|
|
1023
1346
|
this.contentType = '';
|
|
1024
1347
|
this.lastParsedLength = 0;
|
|
1025
|
-
this.lastParsedLineEnd = 0;
|
|
1026
1348
|
this.totalFromHeader = undefined;
|
|
1027
1349
|
}
|
|
1028
1350
|
/**
|
|
@@ -1112,8 +1434,6 @@ function extractJsonObjects(text) {
|
|
|
1112
1434
|
*/
|
|
1113
1435
|
function requestStreaming(config) {
|
|
1114
1436
|
const processor = new StreamingProcessor(config);
|
|
1115
|
-
// Reset processor state to prevent stale data from previous aborted streams
|
|
1116
|
-
processor.reset();
|
|
1117
1437
|
return input$ => input$.pipe(
|
|
1118
1438
|
// Filter only streaming-related events
|
|
1119
1439
|
filter((event) => event.type === HttpEventType.DownloadProgress ||
|
|
@@ -2884,9 +3204,6 @@ class RequestService extends WebsocketService {
|
|
|
2884
3204
|
this.headersService = inject(HeadersService);
|
|
2885
3205
|
this.isPending = new BehaviorSubject(false);
|
|
2886
3206
|
this.isPending$ = this.isPending.asObservable();
|
|
2887
|
-
// Separate pending state for streaming operations only
|
|
2888
|
-
this.isStreamingPending = new BehaviorSubject(false);
|
|
2889
|
-
this.isStreamingPending$ = this.isStreamingPending.asObservable();
|
|
2890
3207
|
this.progress = new BehaviorSubject(0);
|
|
2891
3208
|
this.progress$ = this.progress.asObservable();
|
|
2892
3209
|
this.streamProgress = new BehaviorSubject({
|
|
@@ -2896,82 +3213,32 @@ class RequestService extends WebsocketService {
|
|
|
2896
3213
|
stage: 'connecting'
|
|
2897
3214
|
});
|
|
2898
3215
|
this.streamProgress$ = this.streamProgress.asObservable();
|
|
2899
|
-
this.streamProgressReset = {
|
|
2900
|
-
received: 0,
|
|
2901
|
-
total: undefined,
|
|
2902
|
-
percent: 0,
|
|
2903
|
-
stage: 'connecting'
|
|
2904
|
-
};
|
|
2905
3216
|
}
|
|
2906
3217
|
// Implementation
|
|
2907
3218
|
getRecordRequest(options) {
|
|
2908
3219
|
const urlPath = this.buildUrlPath(options);
|
|
2909
3220
|
const headers = this.buildCombinedHeaders(options);
|
|
2910
3221
|
this.isPending.next(true);
|
|
2911
|
-
if (options.stream) {
|
|
2912
|
-
this.isStreamingPending.next(true);
|
|
2913
|
-
this.streamProgress.next({
|
|
2914
|
-
received: 0,
|
|
2915
|
-
total: undefined,
|
|
2916
|
-
percent: 0,
|
|
2917
|
-
stage: 'connecting'
|
|
2918
|
-
});
|
|
2919
|
-
}
|
|
2920
3222
|
return (options.stream)
|
|
2921
3223
|
? this.http.get(urlPath, {
|
|
2922
3224
|
...headers,
|
|
2923
3225
|
observe: 'events',
|
|
2924
3226
|
responseType: 'text',
|
|
2925
3227
|
reportProgress: true
|
|
2926
|
-
}).pipe(
|
|
2927
|
-
// tap(data => console.log('STREAM DATA', data)),
|
|
2928
|
-
requestStreaming({
|
|
2929
|
-
streamType: options.streamType || StreamType.AI_STREAMING,
|
|
2930
|
-
totalHeader: options.totalHeader
|
|
2931
|
-
}), this.requestStreaming(options), finalize(() => {
|
|
2932
|
-
this.isPending.next(false);
|
|
2933
|
-
this.isStreamingPending.next(false);
|
|
2934
|
-
this.streamProgress.next({
|
|
2935
|
-
received: 0,
|
|
2936
|
-
total: undefined,
|
|
2937
|
-
percent: 0,
|
|
2938
|
-
stage: 'connecting'
|
|
2939
|
-
});
|
|
2940
|
-
}))
|
|
3228
|
+
}).pipe(tap(data => console.log('STREAM DATA', data)), requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreaming(options), this.handleFinalize())
|
|
2941
3229
|
: this.http.get(urlPath, headers).pipe(this.request(options));
|
|
2942
3230
|
}
|
|
2943
3231
|
createRecordRequest(options, data) {
|
|
2944
3232
|
const urlPath = this.buildUrlPath(options);
|
|
2945
3233
|
const headers = this.buildCombinedHeaders(options);
|
|
2946
3234
|
this.isPending.next(true);
|
|
2947
|
-
if (options.stream) {
|
|
2948
|
-
this.isStreamingPending.next(true);
|
|
2949
|
-
this.streamProgress.next({
|
|
2950
|
-
received: 0,
|
|
2951
|
-
total: undefined,
|
|
2952
|
-
percent: 0,
|
|
2953
|
-
stage: 'connecting'
|
|
2954
|
-
});
|
|
2955
|
-
}
|
|
2956
3235
|
return (options.stream)
|
|
2957
3236
|
? this.http.post(urlPath, data, {
|
|
2958
3237
|
...headers,
|
|
2959
3238
|
observe: 'events',
|
|
2960
3239
|
responseType: 'text',
|
|
2961
3240
|
reportProgress: true
|
|
2962
|
-
}).pipe(requestStreaming({
|
|
2963
|
-
streamType: options.streamType || StreamType.AI_STREAMING,
|
|
2964
|
-
totalHeader: options.totalHeader
|
|
2965
|
-
}), this.requestStreaming(options), finalize(() => {
|
|
2966
|
-
this.isPending.next(false);
|
|
2967
|
-
this.isStreamingPending.next(false);
|
|
2968
|
-
this.streamProgress.next({
|
|
2969
|
-
received: 0,
|
|
2970
|
-
total: undefined,
|
|
2971
|
-
percent: 0,
|
|
2972
|
-
stage: 'connecting'
|
|
2973
|
-
});
|
|
2974
|
-
}))
|
|
3241
|
+
}).pipe(requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreaming(options), this.handleFinalize())
|
|
2975
3242
|
: this.http.post(urlPath, data, headers).pipe(this.request(options));
|
|
2976
3243
|
}
|
|
2977
3244
|
updateRecordRequest(options, data) {
|
|
@@ -3015,19 +3282,19 @@ class RequestService extends WebsocketService {
|
|
|
3015
3282
|
requestStreaming(options) {
|
|
3016
3283
|
return (source$) => {
|
|
3017
3284
|
return source$.pipe(tap(output => {
|
|
3018
|
-
|
|
3285
|
+
// Update progress from stream output
|
|
3286
|
+
this.progress.next(output.progress.received);
|
|
3019
3287
|
this.streamProgress.next(output.progress);
|
|
3020
|
-
}),
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
const adaptedData = newData.map((item) => options.adapter(item));
|
|
3025
|
-
return [...acc, ...adaptedData];
|
|
3288
|
+
}), map(output => {
|
|
3289
|
+
const data = output.data;
|
|
3290
|
+
if (!data || (Array.isArray(data) && data.length === 0)) {
|
|
3291
|
+
return data;
|
|
3026
3292
|
}
|
|
3027
|
-
|
|
3028
|
-
return
|
|
3293
|
+
if (options?.adapter) {
|
|
3294
|
+
return data.map((item) => options.adapter(item));
|
|
3029
3295
|
}
|
|
3030
|
-
|
|
3296
|
+
return data;
|
|
3297
|
+
}));
|
|
3031
3298
|
};
|
|
3032
3299
|
}
|
|
3033
3300
|
downloadFileRequest(options) {
|
|
@@ -3165,12 +3432,7 @@ class RequestService extends WebsocketService {
|
|
|
3165
3432
|
return formData;
|
|
3166
3433
|
}
|
|
3167
3434
|
handleFinalize() {
|
|
3168
|
-
return finalize(() =>
|
|
3169
|
-
this.isPending.next(false);
|
|
3170
|
-
// Note: do NOT reset streamProgress here — it fires on cancel/cancel too,
|
|
3171
|
-
// not just natural completion. Reset only happens in requestStreaming
|
|
3172
|
-
// operator on Response complete event, or when a new stream starts.
|
|
3173
|
-
});
|
|
3435
|
+
return finalize(() => this.isPending.next(false));
|
|
3174
3436
|
}
|
|
3175
3437
|
downloadFile(file, fileData) {
|
|
3176
3438
|
const navigatorAny = window.navigator;
|
|
@@ -3863,16 +4125,12 @@ class HTTPManagerService extends RequestService {
|
|
|
3863
4125
|
getRequest(options, params) {
|
|
3864
4126
|
this.isPending.next(true);
|
|
3865
4127
|
this.data.next(null);
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
percent: 0,
|
|
3873
|
-
stage: 'connecting'
|
|
3874
|
-
});
|
|
3875
|
-
}
|
|
4128
|
+
this.streamProgress.next({
|
|
4129
|
+
received: 0,
|
|
4130
|
+
total: undefined,
|
|
4131
|
+
percent: 0,
|
|
4132
|
+
stage: 'connecting'
|
|
4133
|
+
});
|
|
3876
4134
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
3877
4135
|
const func = this.getRecordRequest;
|
|
3878
4136
|
const requests = this.createRequest(func, updatedOptions);
|
|
@@ -4204,7 +4462,7 @@ class HTTPManagerService extends RequestService {
|
|
|
4204
4462
|
}
|
|
4205
4463
|
handleSequentialError(request, error, index, options) {
|
|
4206
4464
|
if (options.logErrors !== false) {
|
|
4207
|
-
|
|
4465
|
+
console.error(`Batch request ${index} failed:`, error);
|
|
4208
4466
|
}
|
|
4209
4467
|
if (options.stopOnError) {
|
|
4210
4468
|
return throwError(() => error);
|
|
@@ -4216,7 +4474,7 @@ class HTTPManagerService extends RequestService {
|
|
|
4216
4474
|
}
|
|
4217
4475
|
handleParallelError(request, error, index, options) {
|
|
4218
4476
|
if (options.logErrors !== false) {
|
|
4219
|
-
|
|
4477
|
+
console.error(`Batch request ${index} failed:`, error);
|
|
4220
4478
|
}
|
|
4221
4479
|
if (options.ignoreErrors) {
|
|
4222
4480
|
return of(undefined);
|
|
@@ -4244,186 +4502,49 @@ class RequestSignalsService extends WebsocketService {
|
|
|
4244
4502
|
this.http = inject(HttpClient);
|
|
4245
4503
|
this.pathQueryService = inject(PathQueryService);
|
|
4246
4504
|
this.headersService = inject(HeadersService);
|
|
4247
|
-
|
|
4248
|
-
this._activeRequestCount = signal(0);
|
|
4249
|
-
this.isPending = computed(() => this._activeRequestCount() > 0);
|
|
4505
|
+
this.isPending = signal(false);
|
|
4250
4506
|
this.progress = signal(0);
|
|
4251
|
-
|
|
4252
|
-
this.activeRequests = new Map();
|
|
4253
|
-
this.activeMethod = null;
|
|
4254
|
-
this.activeStreamMethod = null;
|
|
4255
|
-
// Queue for requests that need to wait
|
|
4256
|
-
this.requestQueue = [];
|
|
4257
|
-
// Cleanup subject for cancelling queued requests on destroy
|
|
4258
|
-
this.destroy$ = new Subject();
|
|
4507
|
+
this.isIdle = computed(() => !this.isPending());
|
|
4259
4508
|
}
|
|
4260
4509
|
// Implementation
|
|
4261
4510
|
getRecordRequest(options) {
|
|
4262
4511
|
const urlPath = this.buildUrlPath(options);
|
|
4263
4512
|
const headers = this.buildCombinedHeaders(options);
|
|
4264
|
-
|
|
4265
|
-
|
|
4513
|
+
this.isPending.set(true);
|
|
4514
|
+
return (options.stream)
|
|
4266
4515
|
? this.http.get(urlPath, {
|
|
4267
4516
|
...headers,
|
|
4268
4517
|
observe: 'events',
|
|
4269
4518
|
responseType: 'text',
|
|
4270
4519
|
reportProgress: true
|
|
4271
|
-
}).pipe(requestStreaming({
|
|
4272
|
-
|
|
4273
|
-
totalHeader: options.totalHeader
|
|
4274
|
-
}), this.requestStreamingOperator(options), finalize(() => this._activeRequestCount.update(c => Math.max(0, c - 1))))
|
|
4275
|
-
: this.http.get(urlPath, headers).pipe(this.request(options), finalize(() => this._activeRequestCount.update(c => Math.max(0, c - 1))));
|
|
4276
|
-
return this.queueRequest(request$, 'GET', isStream);
|
|
4520
|
+
}).pipe(requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreamingOperator(options), this.handleFinalize())
|
|
4521
|
+
: this.http.get(urlPath, headers).pipe(this.request(options), this.handleFinalize());
|
|
4277
4522
|
}
|
|
4278
4523
|
// Implementation
|
|
4279
4524
|
createRecordRequest(options, data) {
|
|
4280
4525
|
const urlPath = this.buildUrlPath(options);
|
|
4281
4526
|
const headers = this.buildCombinedHeaders(options);
|
|
4282
|
-
|
|
4283
|
-
|
|
4527
|
+
this.isPending.set(true);
|
|
4528
|
+
return (options.stream)
|
|
4284
4529
|
? this.http.post(urlPath, data, {
|
|
4285
4530
|
...headers,
|
|
4286
4531
|
observe: 'events',
|
|
4287
4532
|
responseType: 'text',
|
|
4288
4533
|
reportProgress: true
|
|
4289
|
-
}).pipe(requestStreaming({
|
|
4290
|
-
|
|
4291
|
-
totalHeader: options.totalHeader
|
|
4292
|
-
}), this.requestStreamingOperator(options), finalize(() => this._activeRequestCount.update(c => Math.max(0, c - 1))))
|
|
4293
|
-
: this.http.post(urlPath, data, headers).pipe(this.request(options), finalize(() => this._activeRequestCount.update(c => Math.max(0, c - 1))));
|
|
4294
|
-
return this.queueRequest(request$, 'POST', isStream);
|
|
4534
|
+
}).pipe(requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreamingOperator(options), this.handleFinalize())
|
|
4535
|
+
: this.http.post(urlPath, data, headers).pipe(this.request(options), this.handleFinalize());
|
|
4295
4536
|
}
|
|
4296
4537
|
updateRecordRequest(options, data) {
|
|
4297
4538
|
const urlPath = this.buildUrlPath(options);
|
|
4298
4539
|
const headers = this.buildHeaders(options);
|
|
4299
|
-
|
|
4300
|
-
return this.
|
|
4540
|
+
this.isPending.set(true);
|
|
4541
|
+
return this.http.put(urlPath, data, headers).pipe(this.request(options), this.handleFinalize());
|
|
4301
4542
|
}
|
|
4302
4543
|
deleteRecordRequest(options) {
|
|
4303
4544
|
const urlPath = this.buildUrlPath(options);
|
|
4304
4545
|
const headers = this.buildHeaders(options);
|
|
4305
|
-
|
|
4306
|
-
return this.
|
|
4307
|
-
}
|
|
4308
|
-
/**
|
|
4309
|
-
* Queue a request to ensure FIFO processing:
|
|
4310
|
-
* - All requests are queued and processed sequentially
|
|
4311
|
-
* - Streams are protected - non-stream requests wait for active streams
|
|
4312
|
-
*/
|
|
4313
|
-
queueRequest(request$, method, isStream = false) {
|
|
4314
|
-
// console.log(`[Queue] ${method} queued (isStream=${isStream})`, {
|
|
4315
|
-
// activeMethod: this.activeMethod,
|
|
4316
|
-
// activeStreamMethod: this.activeStreamMethod,
|
|
4317
|
-
// queueLength: this.requestQueue.length
|
|
4318
|
-
// });
|
|
4319
|
-
return new Observable((observer) => {
|
|
4320
|
-
// console.log(`[Queue] ${method} observer created, checking conditions...`, {
|
|
4321
|
-
// activeMethod: this.activeMethod,
|
|
4322
|
-
// activeStreamMethod: this.activeStreamMethod,
|
|
4323
|
-
// isStream
|
|
4324
|
-
// });
|
|
4325
|
-
const queuedRequest = {
|
|
4326
|
-
observable: request$,
|
|
4327
|
-
resolve: (value) => observer.next(value),
|
|
4328
|
-
reject: (error) => observer.error(error),
|
|
4329
|
-
method,
|
|
4330
|
-
isStream
|
|
4331
|
-
};
|
|
4332
|
-
// If a stream is currently active, non-stream requests must wait
|
|
4333
|
-
if (this.activeStreamMethod && !isStream) {
|
|
4334
|
-
// console.log(`[Queue] ${method} queued (1), waiting for stream ${this.activeStreamMethod}`);
|
|
4335
|
-
this.requestQueue.push(queuedRequest);
|
|
4336
|
-
return () => {
|
|
4337
|
-
const idx = this.requestQueue.findIndex(q => q === queuedRequest);
|
|
4338
|
-
if (idx !== -1)
|
|
4339
|
-
this.requestQueue.splice(idx, 1);
|
|
4340
|
-
};
|
|
4341
|
-
}
|
|
4342
|
-
// If a request is already running, queue this one (FIFO)
|
|
4343
|
-
if (this.activeMethod) {
|
|
4344
|
-
// console.log(`[Queue] ${method} queued (2), waiting for ${this.activeMethod}`);
|
|
4345
|
-
this.requestQueue.push(queuedRequest);
|
|
4346
|
-
return () => {
|
|
4347
|
-
const idx = this.requestQueue.findIndex(q => q === queuedRequest);
|
|
4348
|
-
if (idx !== -1)
|
|
4349
|
-
this.requestQueue.splice(idx, 1);
|
|
4350
|
-
};
|
|
4351
|
-
}
|
|
4352
|
-
// No active request - start immediately
|
|
4353
|
-
// console.log(`[Queue] ${method} starting immediately`);
|
|
4354
|
-
this.startRequest(queuedRequest, observer);
|
|
4355
|
-
return () => {
|
|
4356
|
-
const idx = this.requestQueue.findIndex(q => q === queuedRequest);
|
|
4357
|
-
if (idx !== -1)
|
|
4358
|
-
this.requestQueue.splice(idx, 1);
|
|
4359
|
-
};
|
|
4360
|
-
});
|
|
4361
|
-
}
|
|
4362
|
-
/**
|
|
4363
|
-
* Start processing a request
|
|
4364
|
-
*/
|
|
4365
|
-
startRequest(queuedRequest, observer) {
|
|
4366
|
-
this.activeMethod = queuedRequest.method;
|
|
4367
|
-
if (queuedRequest.isStream) {
|
|
4368
|
-
this.activeStreamMethod = queuedRequest.method;
|
|
4369
|
-
}
|
|
4370
|
-
this._activeRequestCount.update(c => c + 1);
|
|
4371
|
-
// console.log(`[Queue] Starting ${queuedRequest.method}${queuedRequest.isStream ? ' (STREAM)' : ''}`, {
|
|
4372
|
-
// activeMethod: this.activeMethod,
|
|
4373
|
-
// activeStreamMethod: this.activeStreamMethod,
|
|
4374
|
-
// queueLength: this.requestQueue.length
|
|
4375
|
-
// });
|
|
4376
|
-
const sub = queuedRequest.observable.subscribe({
|
|
4377
|
-
next: (value) => {
|
|
4378
|
-
// console.log(`[Queue] ${queuedRequest.method} received value, isStream=${queuedRequest.isStream}`);
|
|
4379
|
-
observer.next(value);
|
|
4380
|
-
},
|
|
4381
|
-
error: (error) => {
|
|
4382
|
-
// console.log(`[Queue] ${queuedRequest.method} error:`, error);
|
|
4383
|
-
observer.error(error);
|
|
4384
|
-
this.cleanupAndProcessNext(queuedRequest.method, !!queuedRequest.isStream);
|
|
4385
|
-
},
|
|
4386
|
-
complete: () => {
|
|
4387
|
-
// console.log(`[Queue] ${queuedRequest.method} complete, isStream=${queuedRequest.isStream}`);
|
|
4388
|
-
observer.complete();
|
|
4389
|
-
this.cleanupAndProcessNext(queuedRequest.method, !!queuedRequest.isStream);
|
|
4390
|
-
}
|
|
4391
|
-
});
|
|
4392
|
-
this.activeRequests.set(queuedRequest.method, sub);
|
|
4393
|
-
}
|
|
4394
|
-
/**
|
|
4395
|
-
* Clean up completed request and process next in queue
|
|
4396
|
-
*/
|
|
4397
|
-
cleanupAndProcessNext(method, wasStream) {
|
|
4398
|
-
// console.log(`[Queue] cleanupAndProcessNext called: method=${method}, wasStream=${wasStream}`, {
|
|
4399
|
-
// activeMethod: this.activeMethod,
|
|
4400
|
-
// activeStreamMethod: this.activeStreamMethod,
|
|
4401
|
-
// queueLength: this.requestQueue.length
|
|
4402
|
-
// });
|
|
4403
|
-
this.activeRequests.delete(method);
|
|
4404
|
-
this.activeMethod = null;
|
|
4405
|
-
if (wasStream) {
|
|
4406
|
-
this.activeStreamMethod = null;
|
|
4407
|
-
}
|
|
4408
|
-
this._activeRequestCount.update(c => Math.max(0, c - 1));
|
|
4409
|
-
// console.log(`[Queue] After cleanup:`, {
|
|
4410
|
-
// activeMethod: this.activeMethod,
|
|
4411
|
-
// activeStreamMethod: this.activeStreamMethod,
|
|
4412
|
-
// queueLength: this.requestQueue.length
|
|
4413
|
-
// });
|
|
4414
|
-
// Process next request in queue
|
|
4415
|
-
if (this.requestQueue.length > 0) {
|
|
4416
|
-
const next = this.requestQueue.shift();
|
|
4417
|
-
// console.log(`[Queue] Processing next from queue: ${next.method}`, {
|
|
4418
|
-
// isStream: next.isStream,
|
|
4419
|
-
// queueLength: this.requestQueue.length
|
|
4420
|
-
// });
|
|
4421
|
-
this.startRequest(next, {
|
|
4422
|
-
next: (value) => next.resolve(value),
|
|
4423
|
-
error: (error) => next.reject(error),
|
|
4424
|
-
complete: () => { }
|
|
4425
|
-
});
|
|
4426
|
-
}
|
|
4546
|
+
this.isPending.set(true);
|
|
4547
|
+
return this.http.delete(urlPath, headers).pipe(this.request(options), this.handleFinalize());
|
|
4427
4548
|
}
|
|
4428
4549
|
buildUrlPath(options) {
|
|
4429
4550
|
return this.pathQueryService.buildAPIPath(options.server, options.path);
|
|
@@ -4452,9 +4573,7 @@ class RequestSignalsService extends WebsocketService {
|
|
|
4452
4573
|
}
|
|
4453
4574
|
requestStreamingOperator(options) {
|
|
4454
4575
|
return (source$) => {
|
|
4455
|
-
return source$.pipe(
|
|
4456
|
-
this.progress.set(output.progress.percent);
|
|
4457
|
-
}), map(output => {
|
|
4576
|
+
return source$.pipe(map(output => {
|
|
4458
4577
|
const data = output.data;
|
|
4459
4578
|
if (!data || (Array.isArray(data) && data.length === 0)) {
|
|
4460
4579
|
return data;
|
|
@@ -4467,12 +4586,14 @@ class RequestSignalsService extends WebsocketService {
|
|
|
4467
4586
|
};
|
|
4468
4587
|
}
|
|
4469
4588
|
downloadFileRequest(options) {
|
|
4470
|
-
this.
|
|
4589
|
+
this.isPending.set(true);
|
|
4471
4590
|
const urlPath = this.buildUrlPath(options);
|
|
4472
|
-
|
|
4591
|
+
return this.http.get(urlPath, { responseType: 'blob', observe: 'events', reportProgress: true })
|
|
4473
4592
|
.pipe(map((event) => {
|
|
4593
|
+
this.isPending.set(true);
|
|
4474
4594
|
if (event instanceof HttpHeaderResponse) {
|
|
4475
4595
|
if (event.status !== 200) {
|
|
4596
|
+
this.isPending.set(false);
|
|
4476
4597
|
throw new Error('Download failed');
|
|
4477
4598
|
}
|
|
4478
4599
|
}
|
|
@@ -4488,54 +4609,59 @@ class RequestSignalsService extends WebsocketService {
|
|
|
4488
4609
|
const file = (header_content) ? header_content.split('=')[1].substring(0, header_content.split('=')[1].length) : '';
|
|
4489
4610
|
const fileName = (fileNamePath !== '') ? fileNamePath : file;
|
|
4490
4611
|
if (fileName === '') {
|
|
4612
|
+
this.isPending.set(false);
|
|
4491
4613
|
throw new Error('Save File: (file name and extension) not found in Headers or Path');
|
|
4492
4614
|
}
|
|
4493
4615
|
this.downloadFile(fileName, event.body);
|
|
4616
|
+
this.isPending.set(false);
|
|
4494
4617
|
return 100;
|
|
4495
4618
|
}
|
|
4496
4619
|
catch (error) {
|
|
4497
4620
|
throw new Error('Download failed');
|
|
4498
4621
|
}
|
|
4499
4622
|
default:
|
|
4623
|
+
this.isPending.set(false);
|
|
4500
4624
|
return 0;
|
|
4501
4625
|
}
|
|
4502
4626
|
}), catchError(err => {
|
|
4503
4627
|
return throwError(() => err);
|
|
4504
|
-
})
|
|
4505
|
-
return this.queueRequest(request$, 'DOWNLOAD');
|
|
4628
|
+
}));
|
|
4506
4629
|
}
|
|
4507
4630
|
uploadFileRequest(options) {
|
|
4631
|
+
this.isPending.set(true);
|
|
4508
4632
|
const files = options.uploadFiles ? (Array.isArray(options.uploadFiles) ? options.uploadFiles : [options.uploadFiles]) : [];
|
|
4509
4633
|
const validation = this.validateUploadFiles(files, options);
|
|
4510
4634
|
if (validation.validFiles.length === 0) {
|
|
4635
|
+
this.isPending.set(false);
|
|
4511
4636
|
return throwError(() => new UploadValidationErrorModel(validation.invalidFiles, 0, files.length));
|
|
4512
4637
|
}
|
|
4513
|
-
this._activeRequestCount.update(c => c + 1);
|
|
4514
4638
|
const fieldName = options.uploadFieldName || (files.length === 1 ? 'file' : 'files');
|
|
4515
4639
|
const formData = this.buildFormData(validation.validFiles, fieldName);
|
|
4516
4640
|
const urlPath = this.buildUrlPath(options);
|
|
4517
4641
|
const method = options.uploadHttpMethod || 'POST';
|
|
4518
|
-
|
|
4642
|
+
return this.http.request(method, urlPath, {
|
|
4519
4643
|
body: formData,
|
|
4520
4644
|
observe: 'events',
|
|
4521
4645
|
reportProgress: true
|
|
4522
4646
|
}).pipe(map((event) => {
|
|
4647
|
+
this.isPending.set(true);
|
|
4523
4648
|
switch (event.type) {
|
|
4524
4649
|
case HttpEventType.UploadProgress:
|
|
4525
4650
|
const status = event.total ? Math.round(event.loaded / (event.total || 1) * 100) : 0;
|
|
4526
4651
|
this.progress.set(status);
|
|
4527
4652
|
return null;
|
|
4528
4653
|
case HttpEventType.Response:
|
|
4654
|
+
this.isPending.set(false);
|
|
4529
4655
|
this.progress.set(0);
|
|
4530
4656
|
return event.body;
|
|
4531
4657
|
default:
|
|
4532
4658
|
return null;
|
|
4533
4659
|
}
|
|
4534
4660
|
}), filter((body) => body !== null), catchError(err => {
|
|
4661
|
+
this.isPending.set(false);
|
|
4535
4662
|
this.progress.set(0);
|
|
4536
4663
|
return throwError(() => err);
|
|
4537
|
-
})
|
|
4538
|
-
return this.queueRequest(request$, 'UPLOAD');
|
|
4664
|
+
}));
|
|
4539
4665
|
}
|
|
4540
4666
|
validateUploadFiles(files, options) {
|
|
4541
4667
|
const validFiles = [];
|
|
@@ -4584,6 +4710,9 @@ class RequestSignalsService extends WebsocketService {
|
|
|
4584
4710
|
});
|
|
4585
4711
|
return formData;
|
|
4586
4712
|
}
|
|
4713
|
+
handleFinalize() {
|
|
4714
|
+
return finalize(() => this.isPending.set(false));
|
|
4715
|
+
}
|
|
4587
4716
|
downloadFile(file, fileData) {
|
|
4588
4717
|
const navigatorAny = window.navigator;
|
|
4589
4718
|
const extension = file.split('.')[1]?.toLowerCase();
|
|
@@ -4797,6 +4926,7 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4797
4926
|
}
|
|
4798
4927
|
// REQUESTS
|
|
4799
4928
|
getRequest(options, params) {
|
|
4929
|
+
this.isPending.set(true);
|
|
4800
4930
|
this.data.set(null);
|
|
4801
4931
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4802
4932
|
const func = this.getRecordRequest;
|
|
@@ -4805,13 +4935,15 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4805
4935
|
this.data.set(data);
|
|
4806
4936
|
if (updatedOptions.displaySuccess)
|
|
4807
4937
|
this.handleSuccessWithSnackBar(updatedOptions.successMessage);
|
|
4808
|
-
}), catchError((err) => {
|
|
4938
|
+
}), finalize(() => this.isPending.set(false)), catchError((err) => {
|
|
4809
4939
|
if (updatedOptions.displayError)
|
|
4810
4940
|
this.handleErrorWithSnackBar(err, updatedOptions.errorMessage || err?.message);
|
|
4941
|
+
this.isPending.set(false);
|
|
4811
4942
|
return this.handleError(err);
|
|
4812
4943
|
}));
|
|
4813
4944
|
}
|
|
4814
4945
|
postRequest(data, options, params) {
|
|
4946
|
+
this.isPending.set(true);
|
|
4815
4947
|
this.data.set(null);
|
|
4816
4948
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4817
4949
|
const func = this.createRecordRequest;
|
|
@@ -4820,13 +4952,15 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4820
4952
|
this.data.set(data);
|
|
4821
4953
|
if (updatedOptions.displaySuccess)
|
|
4822
4954
|
this.handleSuccessWithSnackBar(updatedOptions.successMessage);
|
|
4823
|
-
}), catchError((err) => {
|
|
4955
|
+
}), finalize(() => this.isPending.set(false)), catchError((err) => {
|
|
4824
4956
|
if (updatedOptions.displayError)
|
|
4825
4957
|
this.handleErrorWithSnackBar(err, updatedOptions.errorMessage);
|
|
4958
|
+
this.isPending.set(false);
|
|
4826
4959
|
return this.handleError(err);
|
|
4827
4960
|
}));
|
|
4828
4961
|
}
|
|
4829
4962
|
putRequest(data, options, params) {
|
|
4963
|
+
this.isPending.set(true);
|
|
4830
4964
|
this.data.set(null);
|
|
4831
4965
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4832
4966
|
const func = this.updateRecordRequest;
|
|
@@ -4835,13 +4969,15 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4835
4969
|
this.data.set(data);
|
|
4836
4970
|
if (updatedOptions.displaySuccess)
|
|
4837
4971
|
this.handleSuccessWithSnackBar(updatedOptions.successMessage);
|
|
4838
|
-
}), catchError((err) => {
|
|
4972
|
+
}), finalize(() => this.isPending.set(false)), catchError((err) => {
|
|
4839
4973
|
if (updatedOptions.displayError)
|
|
4840
4974
|
this.handleErrorWithSnackBar(err, updatedOptions.errorMessage);
|
|
4975
|
+
this.isPending.set(false);
|
|
4841
4976
|
return this.handleError(err);
|
|
4842
4977
|
}));
|
|
4843
4978
|
}
|
|
4844
4979
|
deleteRequest(options, params) {
|
|
4980
|
+
this.isPending.set(true);
|
|
4845
4981
|
this.data.set(null);
|
|
4846
4982
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4847
4983
|
const func = this.deleteRecordRequest;
|
|
@@ -4850,22 +4986,26 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4850
4986
|
this.data.set(data);
|
|
4851
4987
|
if (updatedOptions.displaySuccess)
|
|
4852
4988
|
this.handleSuccessWithSnackBar(updatedOptions.successMessage);
|
|
4853
|
-
}), catchError((err) => {
|
|
4989
|
+
}), finalize(() => this.isPending.set(false)), catchError((err) => {
|
|
4854
4990
|
if (updatedOptions.displayError)
|
|
4855
4991
|
this.handleErrorWithSnackBar(err, updatedOptions.errorMessage);
|
|
4992
|
+
this.isPending.set(false);
|
|
4856
4993
|
return this.handleError(err);
|
|
4857
4994
|
}));
|
|
4858
4995
|
}
|
|
4859
4996
|
downloadRequest(options, params, saveAs) {
|
|
4997
|
+
this.isPending.set(true);
|
|
4860
4998
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4861
4999
|
const func = this.downloadFileRequest;
|
|
4862
5000
|
const requests = this.createRequest(func, updatedOptions);
|
|
4863
5001
|
return this.createObservable(updatedOptions, requests, func.name).pipe(catchError((err) => {
|
|
4864
5002
|
this.error.set(true);
|
|
5003
|
+
this.isPending.set(false);
|
|
4865
5004
|
return this.handleError(err);
|
|
4866
5005
|
}));
|
|
4867
5006
|
}
|
|
4868
5007
|
uploadRequest(files, options, params) {
|
|
5008
|
+
this.isPending.set(true);
|
|
4869
5009
|
this.data.set(null);
|
|
4870
5010
|
const updatedOptions = this.defineReqOptions(options, params);
|
|
4871
5011
|
updatedOptions.uploadFiles = files;
|
|
@@ -4875,9 +5015,10 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4875
5015
|
this.data.set(data);
|
|
4876
5016
|
if (updatedOptions.displaySuccess)
|
|
4877
5017
|
this.handleSuccessWithSnackBar(updatedOptions.successMessage);
|
|
4878
|
-
}), catchError((err) => {
|
|
5018
|
+
}), finalize(() => this.isPending.set(false)), catchError((err) => {
|
|
4879
5019
|
if (updatedOptions.displayError)
|
|
4880
5020
|
this.handleErrorWithSnackBar(err, updatedOptions.errorMessage || err?.message);
|
|
5021
|
+
this.isPending.set(false);
|
|
4881
5022
|
return this.handleError(err);
|
|
4882
5023
|
}));
|
|
4883
5024
|
}
|
|
@@ -4971,6 +5112,7 @@ class HTTPManagerSignalsService extends RequestSignalsService {
|
|
|
4971
5112
|
this.toastMessage.toastMessage(displaySuccess);
|
|
4972
5113
|
}
|
|
4973
5114
|
stopPolling() {
|
|
5115
|
+
this.isPending.set(false);
|
|
4974
5116
|
this.polling$.next();
|
|
4975
5117
|
}
|
|
4976
5118
|
defineReqOptions(options, params) {
|
|
@@ -5137,7 +5279,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
5137
5279
|
}] }] });
|
|
5138
5280
|
|
|
5139
5281
|
class ApiRequest {
|
|
5140
|
-
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType,
|
|
5282
|
+
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, displaySuccess, successMessage, errorMessage, saveAs, fileContentHeader, ws, env, uploadFiles, uploadFieldName, uploadHttpMethod, allowedTypes, maxFileSize, maxTotalSize) {
|
|
5141
5283
|
this.server = server;
|
|
5142
5284
|
this.path = path;
|
|
5143
5285
|
this.headers = headers;
|
|
@@ -5147,7 +5289,6 @@ class ApiRequest {
|
|
|
5147
5289
|
this.retry = retry;
|
|
5148
5290
|
this.stream = stream;
|
|
5149
5291
|
this.streamType = streamType;
|
|
5150
|
-
this.totalHeader = totalHeader;
|
|
5151
5292
|
this.displayError = displayError;
|
|
5152
5293
|
this.displaySuccess = displaySuccess;
|
|
5153
5294
|
this.successMessage = successMessage;
|
|
@@ -5165,17 +5306,20 @@ class ApiRequest {
|
|
|
5165
5306
|
}
|
|
5166
5307
|
static adapt(item) {
|
|
5167
5308
|
const server = Array.isArray(item?.server) ? item.server.join('/') : item?.server || '';
|
|
5168
|
-
return new ApiRequest(server, item?.path ? item.path : [], item?.headers ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), item?.stream ? item.stream : false, item?.streamType
|
|
5309
|
+
return new ApiRequest(server, item?.path ? item.path : [], item?.headers ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), item?.stream ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, item?.displayError ? item.displayError : false, item?.displaySuccess ? item.displaySuccess : false, item?.successMessage, item?.errorMessage, item?.saveAs, item?.fileContentHeader, item?.ws, item?.env || 'dev', item?.uploadFiles, item?.uploadFieldName, item?.uploadHttpMethod, Array.isArray(item?.allowedTypes) ? item.allowedTypes : [], item?.maxFileSize, item?.maxTotalSize);
|
|
5169
5310
|
}
|
|
5170
5311
|
}
|
|
5171
5312
|
|
|
5172
5313
|
class RequestOptions {
|
|
5173
|
-
constructor(path = [], headers = {}) {
|
|
5314
|
+
constructor(path = [], headers = {}, forceRefresh, watchParams, watchExpiresAt) {
|
|
5174
5315
|
this.path = path;
|
|
5175
5316
|
this.headers = headers;
|
|
5317
|
+
this.forceRefresh = forceRefresh;
|
|
5318
|
+
this.watchParams = watchParams;
|
|
5319
|
+
this.watchExpiresAt = watchExpiresAt;
|
|
5176
5320
|
}
|
|
5177
5321
|
static adapt(item) {
|
|
5178
|
-
return new RequestOptions(item?.path, item?.headers);
|
|
5322
|
+
return new RequestOptions(item?.path, item?.headers, item?.forceRefresh, Array.isArray(item?.watchParams) ? item.watchParams : [], item?.watchExpiresAt);
|
|
5179
5323
|
}
|
|
5180
5324
|
}
|
|
5181
5325
|
|
|
@@ -5970,8 +6114,8 @@ class DbService extends Dexie {
|
|
|
5970
6114
|
console.warn('Database upgrade blocked! Please close other tabs/windows of this app.');
|
|
5971
6115
|
});
|
|
5972
6116
|
this.on('versionchange', () => {
|
|
5973
|
-
console.warn('Database version changed in another tab.
|
|
5974
|
-
|
|
6117
|
+
console.warn('Database version changed in another tab. Closing current database connection to allow upgrade.');
|
|
6118
|
+
this.close();
|
|
5975
6119
|
});
|
|
5976
6120
|
this.dbReady = this.init();
|
|
5977
6121
|
}
|
|
@@ -6051,9 +6195,12 @@ class DbService extends Dexie {
|
|
|
6051
6195
|
await this.dbReady;
|
|
6052
6196
|
const safeTableName = this.cleanTableName(tableName);
|
|
6053
6197
|
const safeSchema = schema.trim();
|
|
6054
|
-
if (this.tableExists(safeTableName))
|
|
6055
|
-
return;
|
|
6056
6198
|
const currentSchema = this.getCurrentSchema();
|
|
6199
|
+
const existingSchema = currentSchema[safeTableName]?.trim();
|
|
6200
|
+
// No-op only when table already exists and schema is identical.
|
|
6201
|
+
if (this.tableExists(safeTableName) && existingSchema === safeSchema) {
|
|
6202
|
+
return;
|
|
6203
|
+
}
|
|
6057
6204
|
console.log('Current Schema before update:', currentSchema);
|
|
6058
6205
|
currentSchema[safeTableName] = safeSchema;
|
|
6059
6206
|
const nextVersion = this.verno + 1;
|
|
@@ -6067,6 +6214,7 @@ class DbService extends Dexie {
|
|
|
6067
6214
|
const created = this.tables.some(t => t.name === safeTableName);
|
|
6068
6215
|
if (!created) {
|
|
6069
6216
|
console.error(`CRITICAL: Table ${safeTableName} was NOT created after upgrade! Tables found:`, this.tables.map(t => t.name));
|
|
6217
|
+
throw new Error(`Table '${safeTableName}' was not created after schema upgrade`);
|
|
6070
6218
|
}
|
|
6071
6219
|
else {
|
|
6072
6220
|
console.log(`Database opened successfully version ${this.verno}. Table ${safeTableName} verified.`);
|
|
@@ -6074,15 +6222,24 @@ class DbService extends Dexie {
|
|
|
6074
6222
|
}
|
|
6075
6223
|
catch (err) {
|
|
6076
6224
|
console.error('Error opening database after schema update:', err);
|
|
6225
|
+
throw err;
|
|
6077
6226
|
}
|
|
6078
6227
|
}
|
|
6079
6228
|
async DBOpened() {
|
|
6229
|
+
try {
|
|
6230
|
+
await this.dbReady;
|
|
6231
|
+
}
|
|
6232
|
+
catch (err) {
|
|
6233
|
+
console.error('DBOpened: init failed', err);
|
|
6234
|
+
return false;
|
|
6235
|
+
}
|
|
6080
6236
|
if (!this.isOpen()) {
|
|
6081
6237
|
try {
|
|
6082
6238
|
await this.open();
|
|
6083
6239
|
return true;
|
|
6084
6240
|
}
|
|
6085
6241
|
catch (err) {
|
|
6242
|
+
console.error('DBOpened: open failed', err);
|
|
6086
6243
|
return false;
|
|
6087
6244
|
}
|
|
6088
6245
|
}
|
|
@@ -6177,7 +6334,11 @@ class DatabaseManagerService extends DbService {
|
|
|
6177
6334
|
}));
|
|
6178
6335
|
}
|
|
6179
6336
|
createDatabaseTable(tableDef) {
|
|
6180
|
-
|
|
6337
|
+
const tableName = this.cleanTableName(tableDef.table);
|
|
6338
|
+
return from(this.createTable(tableDef.table, tableDef.schema)).pipe(switchMap(() => from(this.DBOpened())), map((opened) => opened && this.tableExists(tableName)), catchError((error) => {
|
|
6339
|
+
console.error(`createDatabaseTable: failed for table '${tableName}'`, error);
|
|
6340
|
+
return of(false);
|
|
6341
|
+
}));
|
|
6181
6342
|
}
|
|
6182
6343
|
updateDatabaseTableSchema(tableDef) {
|
|
6183
6344
|
return from(this.createTable(tableDef.table, tableDef.schema)).pipe(switchMap(() => this.getDatabaseTable(tableDef.table)));
|
|
@@ -6224,26 +6385,23 @@ class DatabaseManagerService extends DbService {
|
|
|
6224
6385
|
}
|
|
6225
6386
|
createTableRecords(table, records) {
|
|
6226
6387
|
const tableName = this.cleanTableName(table);
|
|
6227
|
-
return from(this.DBOpened()).pipe(switchMap(() => {
|
|
6388
|
+
return from(this.DBOpened()).pipe(switchMap((opened) => {
|
|
6389
|
+
if (!opened) {
|
|
6390
|
+
console.error(`createTableRecords: DB not open. Cannot write to '${tableName}'.`);
|
|
6391
|
+
return EMPTY;
|
|
6392
|
+
}
|
|
6228
6393
|
if (!this.tables.some(t => t.name === tableName)) {
|
|
6229
6394
|
console.error(`createTableRecords: Table '${tableName}' does not exist in DB version ${this.verno}. Available:`, this.tables.map(t => t.name));
|
|
6230
6395
|
return EMPTY;
|
|
6231
6396
|
}
|
|
6232
6397
|
const tableInstance = this.table(tableName);
|
|
6233
|
-
//
|
|
6234
|
-
const schema = tableInstance.schema;
|
|
6235
|
-
const validFields = [
|
|
6236
|
-
schema.primKey.name,
|
|
6237
|
-
...schema.indexes.map(idx => idx.name)
|
|
6238
|
-
];
|
|
6398
|
+
// Keep full object payload; Dexie stores non-indexed properties as well.
|
|
6239
6399
|
const insertRecords = records.map((record) => {
|
|
6240
|
-
const
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
});
|
|
6246
|
-
return objectFromSchema;
|
|
6400
|
+
const payload = { ...(record || {}) };
|
|
6401
|
+
if (payload.id === undefined || payload.id === null || payload.id === '') {
|
|
6402
|
+
delete payload.id;
|
|
6403
|
+
}
|
|
6404
|
+
return payload;
|
|
6247
6405
|
});
|
|
6248
6406
|
console.log(`createTableRecords: Bulk putting ${insertRecords.length} records into ${tableName}`);
|
|
6249
6407
|
return from(tableInstance.bulkPut(insertRecords)).pipe(map(() => insertRecords));
|
|
@@ -6257,17 +6415,14 @@ class DatabaseManagerService extends DbService {
|
|
|
6257
6415
|
updateTableRecords(table, records) {
|
|
6258
6416
|
const tableName = this.cleanTableName(table);
|
|
6259
6417
|
return this.getDatabaseTable(tableName).pipe(switchMap((tableData) => {
|
|
6260
|
-
|
|
6261
|
-
const
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
});
|
|
6269
|
-
return from(tableData.bulkPut(insertRecords)).pipe(map(() => insertRecords));
|
|
6270
|
-
}));
|
|
6418
|
+
const insertRecords = records.map((record) => {
|
|
6419
|
+
const payload = { ...(record || {}) };
|
|
6420
|
+
if (payload.id === undefined || payload.id === null || payload.id === '') {
|
|
6421
|
+
delete payload.id;
|
|
6422
|
+
}
|
|
6423
|
+
return payload;
|
|
6424
|
+
});
|
|
6425
|
+
return from(tableData.bulkPut(insertRecords)).pipe(map(() => insertRecords));
|
|
6271
6426
|
}));
|
|
6272
6427
|
}
|
|
6273
6428
|
deleteTableRecord(table, id) {
|
|
@@ -6369,12 +6524,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6369
6524
|
this.httpManagerService = inject(HTTPManagerService);
|
|
6370
6525
|
this.dbManagerService = inject(DatabaseManagerService);
|
|
6371
6526
|
this.localStorageManagerService = inject(LocalStorageManagerService);
|
|
6527
|
+
this.queryParamsTrackerService = inject(QueryParamsTrackerService);
|
|
6372
6528
|
this.utils = inject(UtilsService);
|
|
6373
6529
|
this.logger = inject(LoggerService);
|
|
6374
6530
|
this.error$ = this.httpManagerService.error$;
|
|
6375
|
-
this.isPending$ = this.httpManagerService.isPending
|
|
6376
|
-
this.progress$ = this.httpManagerService.progress$;
|
|
6377
|
-
this.streamProgress$ = this.httpManagerService.streamProgress$;
|
|
6531
|
+
this.isPending$ = this.httpManagerService.isPending$.pipe(delay(1));
|
|
6378
6532
|
this.operationSuccess = new BehaviorSubject(null);
|
|
6379
6533
|
this.operationSuccess$ = this.operationSuccess.asObservable();
|
|
6380
6534
|
// PAGINATION
|
|
@@ -6387,10 +6541,18 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6387
6541
|
this.hasDatabase = false;
|
|
6388
6542
|
this.streamedResponse = [];
|
|
6389
6543
|
this.shouldRetry = true;
|
|
6544
|
+
this.volatileHeaders = new Set([
|
|
6545
|
+
'authorization',
|
|
6546
|
+
'x-request-id',
|
|
6547
|
+
'x-correlation-id',
|
|
6548
|
+
'x-trace-id',
|
|
6549
|
+
'x-amzn-trace-id',
|
|
6550
|
+
'date',
|
|
6551
|
+
'if-none-match'
|
|
6552
|
+
]);
|
|
6553
|
+
this.requestSignatureCache = {};
|
|
6390
6554
|
this.wsRetryAttempts = new BehaviorSubject(0);
|
|
6391
6555
|
this.wsRetryAttempts$ = this.wsRetryAttempts.asObservable();
|
|
6392
|
-
// Guard: prevent concurrent streams and block requests during active stream
|
|
6393
|
-
this.activeStream$ = new BehaviorSubject(false);
|
|
6394
6556
|
this.messages = new BehaviorSubject([]);
|
|
6395
6557
|
this.messages$ = this.messages.asObservable();
|
|
6396
6558
|
this.userListByChannel = new BehaviorSubject(new Map());
|
|
@@ -6658,22 +6820,25 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6658
6820
|
}
|
|
6659
6821
|
}))))));
|
|
6660
6822
|
this.initDBStorage = this.effect((trigger$) => trigger$.pipe(tap(() => {
|
|
6823
|
+
console.log('[initDBStorage effect] Triggered, checking conditions:', {
|
|
6824
|
+
dataType: this.dataType,
|
|
6825
|
+
isARRAY: this.dataType === DataType.ARRAY,
|
|
6826
|
+
hasAdapter: !!this.apiOptions?.adapter,
|
|
6827
|
+
hasTable: !!this.databaseOptions?.table,
|
|
6828
|
+
tableValue: this.databaseOptions?.table
|
|
6829
|
+
});
|
|
6661
6830
|
if (this.dataType !== DataType.ARRAY)
|
|
6662
6831
|
console.warn('Database storage requires dataType to be ARRAY');
|
|
6663
6832
|
if (!this.apiOptions.adapter)
|
|
6664
|
-
console.warn('Database storage
|
|
6833
|
+
console.warn('Database storage adapter missing, using minimal or inferred schema');
|
|
6665
6834
|
if (this.databaseOptions && this.databaseOptions?.table === '')
|
|
6666
6835
|
console.warn('Database storage requires a table name');
|
|
6667
|
-
}), filter(() =>
|
|
6668
|
-
const
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
if (otherKeys.length > 0) {
|
|
6674
|
-
schema += ', ' + otherKeys.join(', ');
|
|
6675
|
-
}
|
|
6676
|
-
}
|
|
6836
|
+
}), filter(() => {
|
|
6837
|
+
const shouldProceed = this.dataType === DataType.ARRAY && !!this.databaseOptions?.table;
|
|
6838
|
+
console.log('[initDBStorage effect] Filter result:', shouldProceed);
|
|
6839
|
+
return shouldProceed;
|
|
6840
|
+
}), switchMap(() => {
|
|
6841
|
+
const schema = this.buildSchemaFromAdapter();
|
|
6677
6842
|
const tableDef = TableSchemaDef.adapt({
|
|
6678
6843
|
table: this.databaseOptions?.table,
|
|
6679
6844
|
schema: schema
|
|
@@ -6701,10 +6866,10 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6701
6866
|
return state;
|
|
6702
6867
|
if (this.dataType === DataType.ARRAY) {
|
|
6703
6868
|
const dataArray = Array.isArray(data) ? data : [data];
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
const updatedData = this.updateArrayState(state.data, dataArray);
|
|
6869
|
+
const stateDataSample = (state.data.length > 0) ? Object.keys(state.data[0]) : [];
|
|
6870
|
+
const newDataSample = (dataArray.length > 0) ? Object.keys(dataArray[0]) : [];
|
|
6871
|
+
const isSame = (state.data.length === 0) ? false : stateDataSample.every((value, index) => value === newDataSample[index]);
|
|
6872
|
+
const updatedData = (!isSame && dataArray.length !== 0) ? this.updateArrayState([], dataArray) : this.updateArrayState(state.data, dataArray);
|
|
6708
6873
|
return { ...state, data: updatedData, dataObject: null };
|
|
6709
6874
|
}
|
|
6710
6875
|
else {
|
|
@@ -6712,18 +6877,6 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6712
6877
|
return { ...state, data: [], dataObject: dataObject };
|
|
6713
6878
|
}
|
|
6714
6879
|
});
|
|
6715
|
-
/**
|
|
6716
|
-
* Streaming-only updater: replaces `data` with the incoming array.
|
|
6717
|
-
*
|
|
6718
|
-
* The streaming operator (`requestStreaming`) accumulates records via `scan`,
|
|
6719
|
-
* so each emission is the full accumulated dataset. Replace, don't merge.
|
|
6720
|
-
*/
|
|
6721
|
-
this.setStreamData$ = this.updater((state, data) => {
|
|
6722
|
-
if (!data)
|
|
6723
|
-
return state;
|
|
6724
|
-
const dataArray = Array.isArray(data) ? data : [data];
|
|
6725
|
-
return { ...state, data: dataArray, dataObject: null };
|
|
6726
|
-
});
|
|
6727
6880
|
this.addData$ = this.updater((state, data) => {
|
|
6728
6881
|
if (this.dataType === DataType.ARRAY) {
|
|
6729
6882
|
const exists = state.data.some(item => item.id === data.id);
|
|
@@ -6774,6 +6927,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6774
6927
|
}
|
|
6775
6928
|
}), concatMap(() => {
|
|
6776
6929
|
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
6930
|
+
this.clearRequestCacheMetadata(this.databaseOptions.table);
|
|
6777
6931
|
const currentData = this.get()?.data;
|
|
6778
6932
|
const idsToDelete = Array.isArray(currentData) ? currentData.map((r) => r.id) : [];
|
|
6779
6933
|
if (idsToDelete.length > 0) {
|
|
@@ -6788,35 +6942,81 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6788
6942
|
this.fetchRecords = (options) => this.effect(() => of(RequestOptions.adapt(options)).pipe(switchMap(() => {
|
|
6789
6943
|
this.streamedResponse = [];
|
|
6790
6944
|
const requestOptions = this.updateRequestOptions(options?.headers);
|
|
6945
|
+
const effectiveParams = this.getEffectiveParams(options?.path);
|
|
6946
|
+
const requestSignature = this.buildRequestSignature('GET', requestOptions, effectiveParams);
|
|
6791
6947
|
const fetchFromAPI = () => {
|
|
6792
|
-
|
|
6793
|
-
|
|
6948
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
6949
|
+
this.setCachedRequestSignature(this.databaseOptions.table, 'GET', requestSignature);
|
|
6950
|
+
}
|
|
6951
|
+
return this.httpManagerService.getRequest(requestOptions, effectiveParams).pipe(tap((data) => {
|
|
6952
|
+
// Extract array from paginated response if needed
|
|
6953
|
+
const arrayData = (data?.results && Array.isArray(data.results)) ? data.results : data;
|
|
6954
|
+
data = (!arrayData) ? (this.dataType === DataType.ARRAY) ? [] : {} : arrayData;
|
|
6794
6955
|
this.setData$(data);
|
|
6795
6956
|
}), concatMap((data) => {
|
|
6796
|
-
|
|
6957
|
+
// Extract array from paginated response for database storage
|
|
6958
|
+
const dbData = (data?.results && Array.isArray(data.results)) ? data.results : data;
|
|
6959
|
+
if (this.hasDatabase && this.databaseOptions?.table && Array.isArray(dbData) && dbData.length > 0) {
|
|
6960
|
+
const tableName = this.databaseOptions.table;
|
|
6797
6961
|
this.localStorageManagerService.updateStore({
|
|
6798
|
-
name:
|
|
6962
|
+
name: tableName,
|
|
6799
6963
|
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } }
|
|
6800
6964
|
});
|
|
6801
|
-
|
|
6965
|
+
const schema = this.buildSchemaFromSample(dbData[0]);
|
|
6966
|
+
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
6967
|
+
const schemaSignature = this.buildSchemaSignature(schema);
|
|
6968
|
+
// Always ensure table exists immediately before writing to avoid stale schema/store races.
|
|
6969
|
+
return this.localStorageManagerService.store$(tableName).pipe(take(1), switchMap((storeData) => {
|
|
6970
|
+
const storedSchemaSignature = this.getStoredSchemaSignature(storeData);
|
|
6971
|
+
const schemaChanged = !!storedSchemaSignature && storedSchemaSignature !== schemaSignature;
|
|
6972
|
+
const ensureTable$ = schemaChanged
|
|
6973
|
+
? this.dbManagerService.clearTable(tableName).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)))
|
|
6974
|
+
: this.dbManagerService.createDatabaseTable(tableDef);
|
|
6975
|
+
return ensureTable$.pipe(switchMap((created) => {
|
|
6976
|
+
if (!created) {
|
|
6977
|
+
console.warn('[DB STORAGE] Table create/open not ready, skipping DB write for this payload:', { table: tableName });
|
|
6978
|
+
return of(data);
|
|
6979
|
+
}
|
|
6980
|
+
return this.dbManagerService.createTableRecords(tableName, dbData).pipe(tap(() => this.saveRequestCacheMetadata(tableName, 'GET', requestSignature, schemaSignature, options)));
|
|
6981
|
+
}));
|
|
6982
|
+
}), catchError((error) => {
|
|
6983
|
+
console.error('[DB STORAGE] Failed to ensure table and write records:', { table: tableName, schema, error });
|
|
6984
|
+
return of(data);
|
|
6985
|
+
}));
|
|
6802
6986
|
}
|
|
6803
6987
|
return of(data);
|
|
6804
6988
|
}));
|
|
6805
6989
|
};
|
|
6990
|
+
console.log('[DB STORAGE] Checking database storage:', {
|
|
6991
|
+
hasDatabase: this.hasDatabase,
|
|
6992
|
+
table: this.databaseOptions?.table,
|
|
6993
|
+
databaseOptions: this.databaseOptions
|
|
6994
|
+
});
|
|
6806
6995
|
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
6807
6996
|
return this.dbManagerService.databaseExists().pipe(switchMap((dbExists) => {
|
|
6808
6997
|
if (!dbExists) {
|
|
6809
6998
|
const initObs = this.initDBStorageAsync();
|
|
6810
|
-
return initObs.pipe(switchMap(() => fetchFromAPI()))
|
|
6999
|
+
return initObs.pipe(switchMap(() => fetchFromAPI()), catchError((error) => {
|
|
7000
|
+
console.warn('[DB STORAGE] initDBStorageAsync failed when DB did not exist, continuing with API:', error);
|
|
7001
|
+
return fetchFromAPI();
|
|
7002
|
+
}));
|
|
6811
7003
|
}
|
|
6812
7004
|
return this.dbManagerService.hasDatabaseTable(this.databaseOptions.table).pipe(switchMap((tableExists) => {
|
|
6813
7005
|
if (!tableExists) {
|
|
6814
7006
|
const initObs = this.initDBStorageAsync();
|
|
6815
|
-
return initObs.pipe(switchMap(() => fetchFromAPI()))
|
|
7007
|
+
return initObs.pipe(switchMap(() => fetchFromAPI()), catchError((error) => {
|
|
7008
|
+
console.warn('[DB STORAGE] initDBStorageAsync failed when table missing, continuing with API:', error);
|
|
7009
|
+
return fetchFromAPI();
|
|
7010
|
+
}));
|
|
6816
7011
|
}
|
|
6817
7012
|
return this.localStorageManagerService.store$(this.databaseOptions.table).pipe(take(1), switchMap((storeData) => {
|
|
7013
|
+
const forceRefresh = !!options?.forceRefresh;
|
|
7014
|
+
const storedSchemaSignature = this.getStoredSchemaSignature(storeData);
|
|
6818
7015
|
const expires = storeData?.expires || 0;
|
|
6819
7016
|
const hasExpired = expires > 0 && this.utils.hasExpired(expires);
|
|
7017
|
+
if (forceRefresh) {
|
|
7018
|
+
return fetchFromAPI();
|
|
7019
|
+
}
|
|
6820
7020
|
if (hasExpired) {
|
|
6821
7021
|
return this.dbManagerService.clearTable(this.databaseOptions.table).pipe(switchMap(() => fetchFromAPI()), tap(() => {
|
|
6822
7022
|
this.localStorageManagerService.updateStore({
|
|
@@ -6825,12 +7025,41 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6825
7025
|
});
|
|
6826
7026
|
}));
|
|
6827
7027
|
}
|
|
7028
|
+
const expectedSchema = this.buildSchemaFromAdapter();
|
|
7029
|
+
const expectedSchemaSignature = this.buildSchemaSignature(expectedSchema);
|
|
7030
|
+
if (storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature) {
|
|
7031
|
+
const tableDef = TableSchemaDef.adapt({ table: this.databaseOptions.table, schema: expectedSchema });
|
|
7032
|
+
return this.dbManagerService.clearTable(this.databaseOptions.table).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)), switchMap(() => fetchFromAPI()));
|
|
7033
|
+
}
|
|
7034
|
+
const trackerAllowsRequest = this.queryParamsTrackerService.checkRequestOptions(this.resolvePath(effectiveParams), this.buildQueryTrackerOptions(options));
|
|
7035
|
+
const shouldMakeRequest = trackerAllowsRequest;
|
|
7036
|
+
if (shouldMakeRequest) {
|
|
7037
|
+
return fetchFromAPI();
|
|
7038
|
+
}
|
|
6828
7039
|
return this.dbManagerService.getTableRecords(this.databaseOptions.table).pipe(switchMap((dbData) => {
|
|
6829
7040
|
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
6830
7041
|
this.setData$(dbData);
|
|
6831
7042
|
return of(dbData);
|
|
6832
7043
|
}
|
|
6833
|
-
|
|
7044
|
+
const currentStateData = this.get()?.data;
|
|
7045
|
+
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
7046
|
+
return of(currentStateData);
|
|
7047
|
+
}
|
|
7048
|
+
if (currentStateData &&
|
|
7049
|
+
!Array.isArray(currentStateData) &&
|
|
7050
|
+
Object.keys(currentStateData).length > 0) {
|
|
7051
|
+
return of(currentStateData);
|
|
7052
|
+
}
|
|
7053
|
+
return of(this.dataType === DataType.ARRAY ? [] : {});
|
|
7054
|
+
}), catchError((error) => {
|
|
7055
|
+
const tableName = this.databaseOptions.table;
|
|
7056
|
+
console.warn('[DB STORAGE] getTableRecords failed, recreating table and falling back to API:', { table: tableName, error });
|
|
7057
|
+
const schema = this.buildSchemaFromAdapter();
|
|
7058
|
+
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
7059
|
+
return this.dbManagerService.createDatabaseTable(tableDef).pipe(switchMap(() => fetchFromAPI()), catchError((recreateError) => {
|
|
7060
|
+
console.error('[DB STORAGE] Failed to recreate table after read error, continuing with API only:', recreateError);
|
|
7061
|
+
return fetchFromAPI();
|
|
7062
|
+
}));
|
|
6834
7063
|
}));
|
|
6835
7064
|
}));
|
|
6836
7065
|
}));
|
|
@@ -6954,7 +7183,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6954
7183
|
if (res.length > 0)
|
|
6955
7184
|
this.setData$(res);
|
|
6956
7185
|
this.streamedResponse = res;
|
|
6957
|
-
}), scan((acc, res) => {
|
|
7186
|
+
}), concatMap((res) => this.persistStreamDataToDb(res, options)), scan((acc, res) => {
|
|
6958
7187
|
const previous = acc.current;
|
|
6959
7188
|
const current = res;
|
|
6960
7189
|
return { previous, current };
|
|
@@ -6968,31 +7197,97 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6968
7197
|
}
|
|
6969
7198
|
}));
|
|
6970
7199
|
})));
|
|
6971
|
-
this.fetchStream = this.effect((
|
|
6972
|
-
|
|
6973
|
-
console.warn('[fetchStream] Stream already active, skipping');
|
|
6974
|
-
return;
|
|
6975
|
-
}
|
|
7200
|
+
this.fetchStream = (options) => this.effect(() => of(options).pipe(tap(() => {
|
|
7201
|
+
console.log('[DEBUG] fetchStream called');
|
|
6976
7202
|
this.httpManagerService.isPending.next(true);
|
|
6977
|
-
|
|
6978
|
-
// Reset streamed-response tracker so a new stream starts clean
|
|
6979
|
-
this.streamedResponse = [];
|
|
6980
|
-
}), concatMap((options) => {
|
|
7203
|
+
}), switchMap((options) => {
|
|
6981
7204
|
const requestOptions = this.updateRequestOptions(options?.headers);
|
|
6982
7205
|
requestOptions.stream = true;
|
|
6983
|
-
|
|
7206
|
+
const effectiveParams = this.getEffectiveParams(options?.path);
|
|
7207
|
+
console.log('[DEBUG] Making streaming request:', requestOptions);
|
|
7208
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
7209
|
+
const requestSignature = this.buildRequestSignature('STREAM', requestOptions, effectiveParams);
|
|
7210
|
+
return this.localStorageManagerService.store$(this.databaseOptions.table).pipe(take(1), switchMap((storeData) => {
|
|
7211
|
+
const forceRefresh = !!options?.forceRefresh;
|
|
7212
|
+
const expires = storeData?.expires || 0;
|
|
7213
|
+
const hasExpired = expires > 0 && this.utils.hasExpired(expires);
|
|
7214
|
+
if (forceRefresh) {
|
|
7215
|
+
this.setCachedRequestSignature(this.databaseOptions.table, 'STREAM', requestSignature);
|
|
7216
|
+
return this.httpManagerService.getRequest(requestOptions, effectiveParams).pipe(map((apiData) => ({ data: apiData, fromCache: false })));
|
|
7217
|
+
}
|
|
7218
|
+
if (hasExpired) {
|
|
7219
|
+
return this.dbManagerService.clearTable(this.databaseOptions.table).pipe(tap(() => this.setCachedRequestSignature(this.databaseOptions.table, 'STREAM', requestSignature)), switchMap(() => this.httpManagerService.getRequest(requestOptions, effectiveParams)), map((apiData) => ({ data: apiData, fromCache: false })));
|
|
7220
|
+
}
|
|
7221
|
+
const trackerAllowsRequest = this.queryParamsTrackerService.checkRequestOptions(this.resolvePath(effectiveParams), this.buildQueryTrackerOptions(options));
|
|
7222
|
+
const shouldMakeRequest = trackerAllowsRequest;
|
|
7223
|
+
if (!shouldMakeRequest) {
|
|
7224
|
+
return this.dbManagerService.getTableRecords(this.databaseOptions.table).pipe(switchMap((dbData) => {
|
|
7225
|
+
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
7226
|
+
return of({ data: dbData, fromCache: true });
|
|
7227
|
+
}
|
|
7228
|
+
const currentStateData = this.get()?.data;
|
|
7229
|
+
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
7230
|
+
return of({ data: currentStateData, fromCache: true });
|
|
7231
|
+
}
|
|
7232
|
+
if (currentStateData &&
|
|
7233
|
+
!Array.isArray(currentStateData) &&
|
|
7234
|
+
Object.keys(currentStateData).length > 0) {
|
|
7235
|
+
return of({ data: currentStateData, fromCache: true });
|
|
7236
|
+
}
|
|
7237
|
+
return of({ data: this.dataType === DataType.ARRAY ? [] : {}, fromCache: true });
|
|
7238
|
+
}));
|
|
7239
|
+
}
|
|
7240
|
+
this.setCachedRequestSignature(this.databaseOptions.table, 'STREAM', requestSignature);
|
|
7241
|
+
return this.httpManagerService.getRequest(requestOptions, effectiveParams).pipe(map((apiData) => ({ data: apiData, fromCache: false })));
|
|
7242
|
+
})).pipe(tap((packet) => {
|
|
7243
|
+
const res = packet?.data;
|
|
7244
|
+
console.log('[DEBUG] Streaming response received:', res);
|
|
7245
|
+
if (res && res.length > 0) {
|
|
7246
|
+
console.log('[DEBUG] Updating state with streaming data:', res);
|
|
7247
|
+
this.setData$(res);
|
|
7248
|
+
this.streamedResponse = res;
|
|
7249
|
+
}
|
|
7250
|
+
else {
|
|
7251
|
+
console.log('[DEBUG] No streaming data or empty array:', res);
|
|
7252
|
+
}
|
|
7253
|
+
// Reset pending once we have a response packet (cache or network)
|
|
7254
|
+
this.httpManagerService.isPending.next(false);
|
|
7255
|
+
}), concatMap((packet) => {
|
|
7256
|
+
if (packet?.fromCache) {
|
|
7257
|
+
return of(packet?.data);
|
|
7258
|
+
}
|
|
7259
|
+
return this.persistStreamDataToDb(packet?.data, options);
|
|
7260
|
+
}), map((res) => {
|
|
7261
|
+
console.log('[DEBUG] Returning data to subscribers:', res);
|
|
7262
|
+
return res;
|
|
7263
|
+
}), catchError((error) => {
|
|
7264
|
+
console.error('[DEBUG] Streaming error:', error);
|
|
7265
|
+
this.httpManagerService.isPending.next(false);
|
|
7266
|
+
return of([]);
|
|
7267
|
+
}));
|
|
7268
|
+
}
|
|
7269
|
+
return this.httpManagerService.getRequest(requestOptions, effectiveParams)
|
|
6984
7270
|
.pipe(tap((res) => {
|
|
6985
|
-
|
|
6986
|
-
// Always
|
|
6987
|
-
if (res &&
|
|
6988
|
-
|
|
7271
|
+
console.log('[DEBUG] Streaming response received:', res);
|
|
7272
|
+
// Always update state with streaming data
|
|
7273
|
+
if (res && res.length > 0) {
|
|
7274
|
+
console.log('[DEBUG] Updating state with streaming data:', res);
|
|
7275
|
+
this.setData$(res);
|
|
6989
7276
|
this.streamedResponse = res;
|
|
6990
7277
|
}
|
|
6991
|
-
|
|
7278
|
+
else {
|
|
7279
|
+
console.log('[DEBUG] No streaming data or empty array:', res);
|
|
7280
|
+
}
|
|
7281
|
+
// Reset pending once we have a response packet
|
|
7282
|
+
this.httpManagerService.isPending.next(false);
|
|
7283
|
+
}), concatMap((res) => this.persistStreamDataToDb(res, options)), map((res) => {
|
|
7284
|
+
console.log('[DEBUG] Returning data to subscribers:', res);
|
|
7285
|
+
return res; // Return the data so subscribers can receive it
|
|
7286
|
+
}), catchError((error) => {
|
|
7287
|
+
console.error('[DEBUG] Streaming error:', error);
|
|
6992
7288
|
this.httpManagerService.isPending.next(false);
|
|
6993
7289
|
return of([]);
|
|
6994
|
-
})
|
|
6995
|
-
);
|
|
7290
|
+
}));
|
|
6996
7291
|
})));
|
|
6997
7292
|
try {
|
|
6998
7293
|
this.databaseOptions = database;
|
|
@@ -7012,7 +7307,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7012
7307
|
encrypted: false,
|
|
7013
7308
|
})
|
|
7014
7309
|
});
|
|
7015
|
-
|
|
7310
|
+
// Use initDBStorageAsync directly - the effect initDBStorage requires subscription
|
|
7311
|
+
this.initDBStorageAsync().subscribe({
|
|
7312
|
+
next: () => console.log('[Constructor] Database storage initialized'),
|
|
7313
|
+
error: (err) => console.error('[Constructor] Database storage initialization failed:', err)
|
|
7314
|
+
});
|
|
7016
7315
|
}
|
|
7017
7316
|
}
|
|
7018
7317
|
catch (error) {
|
|
@@ -7066,8 +7365,24 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7066
7365
|
this.dataType = (dataType) ? dataType : DataType.ARRAY;
|
|
7067
7366
|
// Only update database options if a database parameter is explicitly provided
|
|
7068
7367
|
if (database !== undefined) {
|
|
7368
|
+
console.log('[setApiRequestOptions] Database config:', {
|
|
7369
|
+
database,
|
|
7370
|
+
hasTable: !!database?.table,
|
|
7371
|
+
tableValue: database?.table
|
|
7372
|
+
});
|
|
7069
7373
|
this.hasDatabase = (database?.table) ? true : false;
|
|
7070
|
-
|
|
7374
|
+
const adapted = DatabaseStorage.adapt(database);
|
|
7375
|
+
console.log('[setApiRequestOptions] DatabaseStorage.adapt result:', adapted);
|
|
7376
|
+
this.databaseOptions = (this.hasDatabase) ? adapted : undefined;
|
|
7377
|
+
// Trigger database table creation if table is configured
|
|
7378
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
7379
|
+
console.log('[setApiRequestOptions] Initializing database storage for table:', this.databaseOptions.table);
|
|
7380
|
+
// Use initDBStorageAsync directly instead of the effect - effects need subscription to run
|
|
7381
|
+
this.initDBStorageAsync().subscribe({
|
|
7382
|
+
next: () => console.log('[setApiRequestOptions] Database storage initialized successfully'),
|
|
7383
|
+
error: (err) => console.error('[setApiRequestOptions] Database storage initialization failed:', err)
|
|
7384
|
+
});
|
|
7385
|
+
}
|
|
7071
7386
|
}
|
|
7072
7387
|
if (this.apiOptions.ws && this.apiOptions.ws.id !== '') {
|
|
7073
7388
|
// Auto-prefix channel ID for private state manager channels
|
|
@@ -7189,11 +7504,20 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7189
7504
|
this.setData$(data);
|
|
7190
7505
|
}
|
|
7191
7506
|
updateArrayState(currentData, newData) {
|
|
7507
|
+
// For non-streaming requests (GET), REPLACE data entirely
|
|
7508
|
+
// For streaming requests, MERGE with existing data incrementally
|
|
7509
|
+
if (this.streamedResponse.length === 0) {
|
|
7510
|
+
// GET request: return new data as-is (replace)
|
|
7511
|
+
return newData;
|
|
7512
|
+
}
|
|
7513
|
+
// Streaming: merge with existing data
|
|
7192
7514
|
const filterCurrentData = () => {
|
|
7193
7515
|
const ids = this.streamedResponse.map((obj) => obj.id);
|
|
7194
7516
|
return currentData.filter(obj => (obj.id) ? ids.includes(obj.id) : obj);
|
|
7195
7517
|
};
|
|
7196
|
-
const filteredCurrentData = (this.httpManagerService.isPending.value)
|
|
7518
|
+
const filteredCurrentData = (this.httpManagerService.isPending.value)
|
|
7519
|
+
? currentData
|
|
7520
|
+
: filterCurrentData();
|
|
7197
7521
|
const updatedData = filteredCurrentData.map(item => {
|
|
7198
7522
|
const newItem = newData.find(newItem => {
|
|
7199
7523
|
const hasId = (newItem?.id && item?.id) ? true : false;
|
|
@@ -7210,32 +7534,92 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7210
7534
|
return [...updatedData, ...addedData];
|
|
7211
7535
|
}
|
|
7212
7536
|
initDBStorageAsync() {
|
|
7537
|
+
console.log('[initDBStorageAsync] Starting initialization:', {
|
|
7538
|
+
dataType: this.dataType,
|
|
7539
|
+
hasAdapter: !!this.apiOptions?.adapter,
|
|
7540
|
+
table: this.databaseOptions?.table,
|
|
7541
|
+
databaseOptions: this.databaseOptions
|
|
7542
|
+
});
|
|
7213
7543
|
if (this.dataType !== DataType.ARRAY) {
|
|
7214
7544
|
console.warn('Database storage requires dataType to be ARRAY');
|
|
7215
7545
|
return of(null);
|
|
7216
7546
|
}
|
|
7217
7547
|
if (!this.apiOptions.adapter) {
|
|
7218
|
-
console.warn('Database storage
|
|
7219
|
-
return of(null);
|
|
7548
|
+
console.warn('Database storage adapter missing, using minimal or inferred schema');
|
|
7220
7549
|
}
|
|
7221
7550
|
if (!this.databaseOptions?.table) {
|
|
7222
7551
|
console.warn('Database storage requires a table name');
|
|
7223
7552
|
return of(null);
|
|
7224
7553
|
}
|
|
7554
|
+
const schema = this.buildSchemaFromAdapter();
|
|
7555
|
+
const tableDef = TableSchemaDef.adapt({
|
|
7556
|
+
table: this.databaseOptions?.table,
|
|
7557
|
+
schema: schema
|
|
7558
|
+
});
|
|
7559
|
+
return this.dbManagerService.createDatabaseTable(tableDef).pipe(tap((created) => {
|
|
7560
|
+
if (created && this.databaseOptions?.table) {
|
|
7561
|
+
this.saveSchemaSignature(this.databaseOptions.table, this.buildSchemaSignature(schema));
|
|
7562
|
+
}
|
|
7563
|
+
}));
|
|
7564
|
+
}
|
|
7565
|
+
buildSchemaFromAdapter() {
|
|
7225
7566
|
const sampleData = this.apiOptions.adapter?.({}) || {};
|
|
7226
|
-
|
|
7567
|
+
return this.buildSchemaFromSample(sampleData);
|
|
7568
|
+
}
|
|
7569
|
+
buildSchemaFromSample(sampleData) {
|
|
7570
|
+
const schemaKeys = Object.keys(sampleData || {}).filter(key => sampleData[key] !== undefined);
|
|
7227
7571
|
let schema = '++id';
|
|
7228
7572
|
if (schemaKeys.length > 0) {
|
|
7229
|
-
const otherKeys = schemaKeys.filter(k => k !== 'id');
|
|
7573
|
+
const otherKeys = schemaKeys.filter((k) => k !== 'id');
|
|
7230
7574
|
if (otherKeys.length > 0) {
|
|
7231
7575
|
schema += ', ' + otherKeys.join(', ');
|
|
7232
7576
|
}
|
|
7233
7577
|
}
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7578
|
+
return schema;
|
|
7579
|
+
}
|
|
7580
|
+
persistStreamDataToDb(payload, streamOptions) {
|
|
7581
|
+
if (!this.hasDatabase || !this.databaseOptions?.table) {
|
|
7582
|
+
return of(payload);
|
|
7583
|
+
}
|
|
7584
|
+
const dbData = (payload?.results && Array.isArray(payload.results))
|
|
7585
|
+
? payload.results
|
|
7586
|
+
: Array.isArray(payload)
|
|
7587
|
+
? payload
|
|
7588
|
+
: payload
|
|
7589
|
+
? [payload]
|
|
7590
|
+
: [];
|
|
7591
|
+
if (dbData.length === 0) {
|
|
7592
|
+
return of(payload);
|
|
7593
|
+
}
|
|
7594
|
+
const tableName = this.databaseOptions.table;
|
|
7595
|
+
const requestOptions = this.updateRequestOptions(streamOptions?.headers);
|
|
7596
|
+
requestOptions.stream = true;
|
|
7597
|
+
const effectiveParams = this.getEffectiveParams(streamOptions?.path);
|
|
7598
|
+
const requestSignature = this.buildRequestSignature('STREAM', requestOptions, effectiveParams);
|
|
7599
|
+
this.localStorageManagerService.updateStore({
|
|
7600
|
+
name: tableName,
|
|
7601
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } }
|
|
7602
|
+
});
|
|
7603
|
+
const schema = this.buildSchemaFromSample(dbData[0]);
|
|
7604
|
+
const schemaSignature = this.buildSchemaSignature(schema);
|
|
7605
|
+
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
7606
|
+
return this.localStorageManagerService.store$(tableName).pipe(take(1), switchMap((storeData) => {
|
|
7607
|
+
const storedSchemaSignature = this.getStoredSchemaSignature(storeData);
|
|
7608
|
+
const schemaChanged = !!storedSchemaSignature && storedSchemaSignature !== schemaSignature;
|
|
7609
|
+
const ensureTable$ = schemaChanged
|
|
7610
|
+
? this.dbManagerService.clearTable(tableName).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)))
|
|
7611
|
+
: this.dbManagerService.createDatabaseTable(tableDef);
|
|
7612
|
+
return ensureTable$.pipe(switchMap((created) => {
|
|
7613
|
+
if (!created) {
|
|
7614
|
+
console.warn('[DB STORAGE] Stream table create/open not ready, skipping DB write for this chunk:', { table: tableName });
|
|
7615
|
+
return of(payload);
|
|
7616
|
+
}
|
|
7617
|
+
return this.dbManagerService.createTableRecords(tableName, dbData).pipe(tap(() => this.saveRequestCacheMetadata(tableName, 'STREAM', requestSignature, schemaSignature, streamOptions)), map(() => payload));
|
|
7618
|
+
}));
|
|
7619
|
+
}), map(() => payload), catchError((error) => {
|
|
7620
|
+
console.error('[DB STORAGE] Failed to persist streaming payload:', { table: tableName, schema, error });
|
|
7621
|
+
return of(payload);
|
|
7622
|
+
}));
|
|
7239
7623
|
}
|
|
7240
7624
|
// WEBSOCKET COMMUNICATION (STATE MANAGER)
|
|
7241
7625
|
wsCommunication(method, path) {
|
|
@@ -7531,6 +7915,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7531
7915
|
const tableName = this.databaseOptions.table;
|
|
7532
7916
|
this.dbManagerService.clearTable(tableName).subscribe({
|
|
7533
7917
|
next: () => {
|
|
7918
|
+
this.clearRequestCacheMetadata(tableName);
|
|
7534
7919
|
if (this.dataType === DataType.ARRAY) {
|
|
7535
7920
|
this.setData$([]);
|
|
7536
7921
|
}
|
|
@@ -7553,6 +7938,132 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7553
7938
|
: { ...options.headers };
|
|
7554
7939
|
return options;
|
|
7555
7940
|
}
|
|
7941
|
+
buildQueryTrackerOptions(options) {
|
|
7942
|
+
return {
|
|
7943
|
+
watchParams: Array.isArray(options?.watchParams) ? options.watchParams : [],
|
|
7944
|
+
watchExpiresAt: options?.watchExpiresAt,
|
|
7945
|
+
};
|
|
7946
|
+
}
|
|
7947
|
+
normalizeObject(value) {
|
|
7948
|
+
if (Array.isArray(value)) {
|
|
7949
|
+
return value.map((item) => this.normalizeObject(item));
|
|
7950
|
+
}
|
|
7951
|
+
if (value && typeof value === 'object') {
|
|
7952
|
+
return Object.keys(value)
|
|
7953
|
+
.sort()
|
|
7954
|
+
.reduce((acc, key) => {
|
|
7955
|
+
acc[key] = this.normalizeObject(value[key]);
|
|
7956
|
+
return acc;
|
|
7957
|
+
}, {});
|
|
7958
|
+
}
|
|
7959
|
+
return value;
|
|
7960
|
+
}
|
|
7961
|
+
filterHeaders(headers) {
|
|
7962
|
+
const source = headers || {};
|
|
7963
|
+
return Object.keys(source).reduce((acc, key) => {
|
|
7964
|
+
if (!this.volatileHeaders.has(key.toLowerCase())) {
|
|
7965
|
+
acc[key] = source[key];
|
|
7966
|
+
}
|
|
7967
|
+
return acc;
|
|
7968
|
+
}, {});
|
|
7969
|
+
}
|
|
7970
|
+
resolvePath(params) {
|
|
7971
|
+
const basePath = Array.isArray(this.apiOptions.path) ? this.apiOptions.path : [];
|
|
7972
|
+
const effective = this.getEffectiveParams(params);
|
|
7973
|
+
return effective ? [...basePath, ...effective] : [...basePath];
|
|
7974
|
+
}
|
|
7975
|
+
getEffectiveParams(params) {
|
|
7976
|
+
if (!Array.isArray(params) || params.length === 0) {
|
|
7977
|
+
return undefined;
|
|
7978
|
+
}
|
|
7979
|
+
const basePath = Array.isArray(this.apiOptions.path) ? this.apiOptions.path : [];
|
|
7980
|
+
if (basePath.length !== params.length) {
|
|
7981
|
+
return params;
|
|
7982
|
+
}
|
|
7983
|
+
const normalizePart = (value) => {
|
|
7984
|
+
if (value && typeof value === 'object') {
|
|
7985
|
+
return JSON.stringify(this.normalizeObject(value));
|
|
7986
|
+
}
|
|
7987
|
+
return String(value);
|
|
7988
|
+
};
|
|
7989
|
+
const samePath = params.every((part, index) => normalizePart(part) === normalizePart(basePath[index]));
|
|
7990
|
+
return samePath ? undefined : params;
|
|
7991
|
+
}
|
|
7992
|
+
buildRequestSignature(method, requestOptions, params) {
|
|
7993
|
+
const signaturePayload = {
|
|
7994
|
+
method,
|
|
7995
|
+
server: requestOptions.server,
|
|
7996
|
+
path: this.resolvePath(params),
|
|
7997
|
+
headers: this.filterHeaders(requestOptions.headers),
|
|
7998
|
+
stream: !!requestOptions.stream,
|
|
7999
|
+
streamType: requestOptions.streamType || null
|
|
8000
|
+
};
|
|
8001
|
+
return JSON.stringify(this.normalizeObject(signaturePayload));
|
|
8002
|
+
}
|
|
8003
|
+
buildSchemaSignature(schema) {
|
|
8004
|
+
return JSON.stringify(this.normalizeObject(schema.split(',').map((part) => part.trim()).filter(Boolean)));
|
|
8005
|
+
}
|
|
8006
|
+
getCachedRequestSignature(tableName, type) {
|
|
8007
|
+
return this.requestSignatureCache[tableName]?.[type] || null;
|
|
8008
|
+
}
|
|
8009
|
+
setCachedRequestSignature(tableName, type, signature) {
|
|
8010
|
+
const existing = this.requestSignatureCache[tableName] || {};
|
|
8011
|
+
this.requestSignatureCache[tableName] = {
|
|
8012
|
+
...existing,
|
|
8013
|
+
[type]: signature,
|
|
8014
|
+
};
|
|
8015
|
+
}
|
|
8016
|
+
getRequestCacheMetadata(storeData, type) {
|
|
8017
|
+
return storeData?.requestCache?.[type] || null;
|
|
8018
|
+
}
|
|
8019
|
+
getStoredSchemaSignature(storeData) {
|
|
8020
|
+
return storeData?.schemaSignature || null;
|
|
8021
|
+
}
|
|
8022
|
+
saveSchemaSignature(tableName, schemaSignature) {
|
|
8023
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((storeData) => {
|
|
8024
|
+
this.localStorageManagerService.updateStore({
|
|
8025
|
+
name: tableName,
|
|
8026
|
+
data: {
|
|
8027
|
+
...(storeData || {}),
|
|
8028
|
+
schemaSignature,
|
|
8029
|
+
}
|
|
8030
|
+
});
|
|
8031
|
+
})).subscribe();
|
|
8032
|
+
}
|
|
8033
|
+
saveRequestCacheMetadata(tableName, type, signature, schemaSignature, options) {
|
|
8034
|
+
this.setCachedRequestSignature(tableName, type, signature);
|
|
8035
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((storeData) => {
|
|
8036
|
+
const currentCache = storeData?.requestCache || {};
|
|
8037
|
+
this.localStorageManagerService.updateStore({
|
|
8038
|
+
name: tableName,
|
|
8039
|
+
data: {
|
|
8040
|
+
...(storeData || {}),
|
|
8041
|
+
schemaSignature: schemaSignature || storeData?.schemaSignature || null,
|
|
8042
|
+
requestCache: {
|
|
8043
|
+
...currentCache,
|
|
8044
|
+
[type]: {
|
|
8045
|
+
signature,
|
|
8046
|
+
savedAt: Date.now(),
|
|
8047
|
+
path: this.resolvePath(options?.path),
|
|
8048
|
+
headers: this.filterHeaders(options?.headers)
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
});
|
|
8053
|
+
})).subscribe();
|
|
8054
|
+
}
|
|
8055
|
+
clearRequestCacheMetadata(tableName) {
|
|
8056
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((storeData) => {
|
|
8057
|
+
if (!storeData)
|
|
8058
|
+
return;
|
|
8059
|
+
const updated = { ...(storeData || {}) };
|
|
8060
|
+
delete updated.requestCache;
|
|
8061
|
+
this.localStorageManagerService.updateStore({
|
|
8062
|
+
name: tableName,
|
|
8063
|
+
data: updated
|
|
8064
|
+
});
|
|
8065
|
+
})).subscribe();
|
|
8066
|
+
}
|
|
7556
8067
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService, deps: [{ token: API_OPTS }, { token: "dataType" }, { token: DatabaseStorage }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
7557
8068
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService }); }
|
|
7558
8069
|
}
|
|
@@ -8502,7 +9013,7 @@ class RequestManagerBasicDemoComponent {
|
|
|
8502
9013
|
// Update displayed columns when data changes
|
|
8503
9014
|
updateDisplayedColumns(data) {
|
|
8504
9015
|
this.displayedColumns = this.getColumnsFromData(data);
|
|
8505
|
-
|
|
9016
|
+
console.log('[DEMO] Updated columns:', this.displayedColumns);
|
|
8506
9017
|
}
|
|
8507
9018
|
// Helper to check if value is an object
|
|
8508
9019
|
isObject(value) {
|
|
@@ -8762,7 +9273,7 @@ class RequestManagerBasicDemoComponent {
|
|
|
8762
9273
|
this.requestParams.GET = reqParams.apiOptions;
|
|
8763
9274
|
this.STREAM$ = this.httpManagerService.getRequest(reqParams.apiOptions, reqParams.path)
|
|
8764
9275
|
.pipe(tap((data) => {
|
|
8765
|
-
|
|
9276
|
+
console.log("API STREAM response", data);
|
|
8766
9277
|
if (data && data.length > 0) {
|
|
8767
9278
|
this.updateDisplayedColumns(data);
|
|
8768
9279
|
}
|
|
@@ -8859,12 +9370,12 @@ class StateManagerDemoService extends HTTPManagerStateService {
|
|
|
8859
9370
|
setAPIOptions(apiOptions, dataType, database) {
|
|
8860
9371
|
this.setApiRequestOptions(apiOptions, dataType, database);
|
|
8861
9372
|
}
|
|
8862
|
-
getClients() {
|
|
9373
|
+
getClients(options) {
|
|
8863
9374
|
// const headers = {
|
|
8864
9375
|
// auth: "sample-auth-token"
|
|
8865
9376
|
// }
|
|
8866
9377
|
// const sampleOptions = RequestOptions.adapt({ path: ["id", 12], headers, sample: true })
|
|
8867
|
-
this.fetchRecords();
|
|
9378
|
+
this.fetchRecords(options);
|
|
8868
9379
|
}
|
|
8869
9380
|
createClient(data) {
|
|
8870
9381
|
// const headers = {
|
|
@@ -8889,13 +9400,13 @@ class StateManagerDemoService extends HTTPManagerStateService {
|
|
|
8889
9400
|
const sampleOptions = RequestOptions.adapt({ path: [data.id] });
|
|
8890
9401
|
this.deleteRecord(sampleOptions);
|
|
8891
9402
|
}
|
|
8892
|
-
streamRequest() {
|
|
9403
|
+
streamRequest(options) {
|
|
8893
9404
|
console.log('[DEMO SERVICE] streamRequest called');
|
|
8894
9405
|
const headers = {
|
|
8895
9406
|
auth: "sample-auth-token"
|
|
8896
9407
|
};
|
|
8897
9408
|
console.log('[DEMO SERVICE] Calling fetchStream with headers:', headers);
|
|
8898
|
-
this.fetchStream(
|
|
9409
|
+
this.fetchStream(options);
|
|
8899
9410
|
}
|
|
8900
9411
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StateManagerDemoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
8901
9412
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StateManagerDemoService }); }
|
|
@@ -8929,7 +9440,7 @@ class RequestManagerStateDemoComponent {
|
|
|
8929
9440
|
// Update displayed columns when data changes
|
|
8930
9441
|
updateDisplayedColumns(data) {
|
|
8931
9442
|
this.displayedColumns = this.getColumnsFromData(data);
|
|
8932
|
-
|
|
9443
|
+
console.log('[STATE DEMO] Updated columns:', this.displayedColumns);
|
|
8933
9444
|
}
|
|
8934
9445
|
// Helper to check if value is an object
|
|
8935
9446
|
isObject(value) {
|
|
@@ -8983,7 +9494,6 @@ class RequestManagerStateDemoComponent {
|
|
|
8983
9494
|
constructor() {
|
|
8984
9495
|
this.server = 'http://localhost:8080';
|
|
8985
9496
|
this.stateManagerDemoService = inject(StateManagerDemoService);
|
|
8986
|
-
this.progress$ = this.stateManagerDemoService.streamProgress$;
|
|
8987
9497
|
this.displayedColumns = [];
|
|
8988
9498
|
this.fb = inject(FormBuilder);
|
|
8989
9499
|
this.streamTypes = [
|
|
@@ -8996,7 +9506,6 @@ class RequestManagerStateDemoComponent {
|
|
|
8996
9506
|
this.streamType = 'Auto';
|
|
8997
9507
|
this.httpManagerService = inject(HTTPManagerService);
|
|
8998
9508
|
this.isPending$ = this.stateManagerDemoService.isPending$;
|
|
8999
|
-
this.isStreamingPending$ = this.httpManagerService.isStreamingPending$;
|
|
9000
9509
|
this.error$ = this.stateManagerDemoService.error$;
|
|
9001
9510
|
this.countdown$ = this.httpManagerService.countdown$;
|
|
9002
9511
|
this.GET_error$ = new BehaviorSubject('');
|
|
@@ -9009,7 +9518,7 @@ class RequestManagerStateDemoComponent {
|
|
|
9009
9518
|
this.POST$ = new BehaviorSubject(null);
|
|
9010
9519
|
this.PUT$ = new BehaviorSubject(null);
|
|
9011
9520
|
this.DELETE$ = new BehaviorSubject(null);
|
|
9012
|
-
this.STREAM = new BehaviorSubject(
|
|
9521
|
+
this.STREAM = new BehaviorSubject(null);
|
|
9013
9522
|
this.STREAM$ = this.STREAM.asObservable();
|
|
9014
9523
|
this.STREAM_AI = new BehaviorSubject([]);
|
|
9015
9524
|
this.STREAM_AI$ = this.STREAM_AI.asObservable()
|
|
@@ -9045,6 +9554,8 @@ class RequestManagerStateDemoComponent {
|
|
|
9045
9554
|
database: this.fb.group({
|
|
9046
9555
|
table: [''],
|
|
9047
9556
|
expiresIn: ['1m'],
|
|
9557
|
+
watchParams: [''],
|
|
9558
|
+
watchExpiresAt: ['never'],
|
|
9048
9559
|
})
|
|
9049
9560
|
});
|
|
9050
9561
|
this.sampleAdaptors = [
|
|
@@ -9067,7 +9578,7 @@ class RequestManagerStateDemoComponent {
|
|
|
9067
9578
|
this.stateManagerDemoService.data$.pipe(tap$1((data) => console.log("API STREAM_AI response", data)));
|
|
9068
9579
|
this.error$.pipe(tap$1((data) => {
|
|
9069
9580
|
debugger;
|
|
9070
|
-
|
|
9581
|
+
console.log("API STREAM response", data);
|
|
9071
9582
|
}), catchError$1(error => {
|
|
9072
9583
|
return throwError(() => this.errorHandling(error, 'STREAM'));
|
|
9073
9584
|
}));
|
|
@@ -9126,10 +9637,59 @@ class RequestManagerStateDemoComponent {
|
|
|
9126
9637
|
removeHeader(index) {
|
|
9127
9638
|
this.headers.removeAt(index);
|
|
9128
9639
|
}
|
|
9640
|
+
parsePathInput(pathInput) {
|
|
9641
|
+
const raw = typeof pathInput === 'string' ? pathInput.trim() : '';
|
|
9642
|
+
if (!raw) {
|
|
9643
|
+
return [];
|
|
9644
|
+
}
|
|
9645
|
+
const [rawPath = '', rawQuery = ''] = raw.split('?', 2);
|
|
9646
|
+
const pathSegments = rawPath
|
|
9647
|
+
.split('/')
|
|
9648
|
+
.map((segment) => segment.trim())
|
|
9649
|
+
.filter((segment) => segment.length > 0)
|
|
9650
|
+
.map((segment) => decodeURIComponent(segment));
|
|
9651
|
+
if (!rawQuery) {
|
|
9652
|
+
return pathSegments;
|
|
9653
|
+
}
|
|
9654
|
+
const searchParams = new URLSearchParams(rawQuery);
|
|
9655
|
+
const queryObject = {};
|
|
9656
|
+
searchParams.forEach((value, key) => {
|
|
9657
|
+
const normalizedKey = key.trim();
|
|
9658
|
+
if (!normalizedKey) {
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
const normalizedValue = this.normalizeQueryValue(value);
|
|
9662
|
+
if (Object.prototype.hasOwnProperty.call(queryObject, normalizedKey)) {
|
|
9663
|
+
const currentValue = queryObject[normalizedKey];
|
|
9664
|
+
queryObject[normalizedKey] = Array.isArray(currentValue)
|
|
9665
|
+
? [...currentValue, normalizedValue]
|
|
9666
|
+
: [currentValue, normalizedValue];
|
|
9667
|
+
return;
|
|
9668
|
+
}
|
|
9669
|
+
queryObject[normalizedKey] = normalizedValue;
|
|
9670
|
+
});
|
|
9671
|
+
return Object.keys(queryObject).length > 0
|
|
9672
|
+
? [...pathSegments, queryObject]
|
|
9673
|
+
: pathSegments;
|
|
9674
|
+
}
|
|
9675
|
+
normalizeQueryValue(value) {
|
|
9676
|
+
const trimmedValue = value.trim();
|
|
9677
|
+
const lowerValue = trimmedValue.toLowerCase();
|
|
9678
|
+
if (lowerValue === 'true') {
|
|
9679
|
+
return true;
|
|
9680
|
+
}
|
|
9681
|
+
if (lowerValue === 'false') {
|
|
9682
|
+
return false;
|
|
9683
|
+
}
|
|
9684
|
+
if (trimmedValue !== '' && !Number.isNaN(Number(trimmedValue))) {
|
|
9685
|
+
return Number(trimmedValue);
|
|
9686
|
+
}
|
|
9687
|
+
return trimmedValue;
|
|
9688
|
+
}
|
|
9129
9689
|
compileRequest() {
|
|
9130
9690
|
const requestParams = this.requestForm.value;
|
|
9131
9691
|
requestParams.headers = this.arrayObjectsToObjects(requestParams.headers || []);
|
|
9132
|
-
const pathReq =
|
|
9692
|
+
const pathReq = this.parsePathInput(requestParams.path || '');
|
|
9133
9693
|
if (!this.pollingState.checked)
|
|
9134
9694
|
requestParams.polling = 0;
|
|
9135
9695
|
if (!this.failedState.checked) {
|
|
@@ -9143,12 +9703,53 @@ class RequestManagerStateDemoComponent {
|
|
|
9143
9703
|
const apiOptions = ApiRequest.adapt({ ...currentOptions, path: pathReq });
|
|
9144
9704
|
return { apiOptions: apiOptions, path: pathReq };
|
|
9145
9705
|
}
|
|
9706
|
+
parseWatchParams(value) {
|
|
9707
|
+
if (!value || typeof value !== 'string') {
|
|
9708
|
+
return [];
|
|
9709
|
+
}
|
|
9710
|
+
return value
|
|
9711
|
+
.split(',')
|
|
9712
|
+
.map((item) => item.trim())
|
|
9713
|
+
.filter((item) => item.length > 0);
|
|
9714
|
+
}
|
|
9715
|
+
normalizeWatchExpiry(value) {
|
|
9716
|
+
if (typeof value === 'undefined' || value === null || value === '') {
|
|
9717
|
+
return undefined;
|
|
9718
|
+
}
|
|
9719
|
+
if (typeof value === 'string' && value.trim().toLowerCase() === 'never') {
|
|
9720
|
+
return undefined;
|
|
9721
|
+
}
|
|
9722
|
+
return value;
|
|
9723
|
+
}
|
|
9724
|
+
buildDemoRequestOptions(path, headers) {
|
|
9725
|
+
const dbValue = this.database || {};
|
|
9726
|
+
const isDbEnabled = !!this.DBState?.checked;
|
|
9727
|
+
return RequestOptions.adapt({
|
|
9728
|
+
path,
|
|
9729
|
+
headers,
|
|
9730
|
+
forceRefresh: false,
|
|
9731
|
+
watchParams: isDbEnabled ? this.parseWatchParams(dbValue?.watchParams) : [],
|
|
9732
|
+
watchExpiresAt: isDbEnabled ? this.normalizeWatchExpiry(dbValue?.watchExpiresAt) : undefined,
|
|
9733
|
+
});
|
|
9734
|
+
}
|
|
9146
9735
|
onSetStateOptions() {
|
|
9147
|
-
|
|
9736
|
+
const dbValue = this.database;
|
|
9737
|
+
console.log('[onSetStateOptions] Called, checking form values:', {
|
|
9738
|
+
isValid: this.isValid,
|
|
9739
|
+
database: dbValue,
|
|
9740
|
+
hasTable: !!dbValue?.table,
|
|
9741
|
+
tableValue: dbValue?.table,
|
|
9742
|
+
dataType: this.dataType
|
|
9743
|
+
});
|
|
9744
|
+
if (!this.isValid) {
|
|
9745
|
+
console.log('[onSetStateOptions] Form invalid, aborting');
|
|
9148
9746
|
return;
|
|
9747
|
+
}
|
|
9149
9748
|
const reqParams = this.compileRequest();
|
|
9150
|
-
const db = DatabaseStorage.adapt(
|
|
9749
|
+
const db = DatabaseStorage.adapt(dbValue);
|
|
9750
|
+
console.log('[onSetStateOptions] DatabaseStorage.adapt result:', db);
|
|
9151
9751
|
const type = this.dataType === "ARRAY" ? DataType.ARRAY : DataType.OBJECT;
|
|
9752
|
+
console.log('[onSetStateOptions] Calling setAPIOptions with:', { db, type, hasTable: !!db?.table });
|
|
9152
9753
|
this.stateManagerDemoService.setAPIOptions(reqParams.apiOptions, type, db);
|
|
9153
9754
|
this.requestForm.markAsPristine();
|
|
9154
9755
|
}
|
|
@@ -9157,7 +9758,9 @@ class RequestManagerStateDemoComponent {
|
|
|
9157
9758
|
}
|
|
9158
9759
|
onGetRequest() {
|
|
9159
9760
|
this.requestType = 'GET';
|
|
9160
|
-
this.
|
|
9761
|
+
const reqParams = this.compileRequest();
|
|
9762
|
+
const requestOptions = this.buildDemoRequestOptions(reqParams.path, reqParams.apiOptions.headers);
|
|
9763
|
+
this.stateManagerDemoService.getClients(requestOptions);
|
|
9161
9764
|
}
|
|
9162
9765
|
onCreateRequest() {
|
|
9163
9766
|
this.requestType = 'POST';
|
|
@@ -9183,8 +9786,11 @@ class RequestManagerStateDemoComponent {
|
|
|
9183
9786
|
reqParams.apiOptions.stream = true;
|
|
9184
9787
|
reqParams.apiOptions.streamType = this.streamType;
|
|
9185
9788
|
this.requestType = 'STREAM';
|
|
9789
|
+
const streamOptions = RequestOptions.adapt({
|
|
9790
|
+
...this.buildDemoRequestOptions(reqParams.path, reqParams.apiOptions.headers),
|
|
9791
|
+
});
|
|
9186
9792
|
// console.log('[COMPONENT] Calling streamRequest...')
|
|
9187
|
-
this.stateManagerDemoService.streamRequest();
|
|
9793
|
+
this.stateManagerDemoService.streamRequest(streamOptions);
|
|
9188
9794
|
}
|
|
9189
9795
|
errorHandling(err, type) {
|
|
9190
9796
|
console.log(err, type);
|
|
@@ -9208,11 +9814,11 @@ class RequestManagerStateDemoComponent {
|
|
|
9208
9814
|
this.prompts = [];
|
|
9209
9815
|
}
|
|
9210
9816
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
9211
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((isStreamingPending$ | async) || ((STREAM$ | async)?.length)) {\n <mat-progress-bar mode=\"determinate\" [value]=\"(progress$ | async)?.percent ?? 0\"></mat-progress-bar>\n }\n @if ((STREAM$ | async); as data) {\n @if (data?.length) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Rows: {{ data.length }}\n </div>\n </div>\n }\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
9817
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }, { propertyName: "DBState", first: true, predicate: ["DBState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Watch Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"watchParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"watchExpiresAt\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
9212
9818
|
}
|
|
9213
9819
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, decorators: [{
|
|
9214
9820
|
type: Component,
|
|
9215
|
-
args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((isStreamingPending$ | async) || ((STREAM$ | async)?.length)) {\n <mat-progress-bar mode=\"determinate\" [value]=\"(progress$ | async)?.percent ?? 0\"></mat-progress-bar>\n }\n @if ((STREAM$ | async); as data) {\n @if (data?.length) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Rows: {{ data.length }}\n </div>\n </div>\n }\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
|
|
9821
|
+
args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Watch Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"watchParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"watchExpiresAt\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
|
|
9216
9822
|
}], ctorParameters: () => [], propDecorators: { server: [{
|
|
9217
9823
|
type: Input
|
|
9218
9824
|
}], adapter: [{
|
|
@@ -9225,6 +9831,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
9225
9831
|
}], pollingState: [{
|
|
9226
9832
|
type: ViewChild,
|
|
9227
9833
|
args: ["pollingState", { static: true }]
|
|
9834
|
+
}], DBState: [{
|
|
9835
|
+
type: ViewChild,
|
|
9836
|
+
args: ["DBState", { static: true }]
|
|
9228
9837
|
}] } });
|
|
9229
9838
|
|
|
9230
9839
|
class RequestManagerDemoComponent {
|
|
@@ -9243,7 +9852,7 @@ class RequestManagerDemoComponent {
|
|
|
9243
9852
|
// Update displayed columns when data changes
|
|
9244
9853
|
updateDisplayedColumns(data) {
|
|
9245
9854
|
this.displayedColumns = this.getColumnsFromData(data);
|
|
9246
|
-
|
|
9855
|
+
console.log('[DEMO] Updated columns:', this.displayedColumns);
|
|
9247
9856
|
}
|
|
9248
9857
|
// Helper to check if value is an object
|
|
9249
9858
|
isObject(value) {
|
|
@@ -9276,9 +9885,7 @@ class RequestManagerDemoComponent {
|
|
|
9276
9885
|
this.toastMessage = inject(ToastMessageDisplayService);
|
|
9277
9886
|
this.questionControl = this.fb.control("", [Validators.required]);
|
|
9278
9887
|
this.httpManagerService = inject(HTTPManagerService);
|
|
9279
|
-
this.progress$ = this.httpManagerService.streamProgress$;
|
|
9280
9888
|
this.isPending$ = this.httpManagerService.isPending$;
|
|
9281
|
-
this.isStreamingPending$ = this.httpManagerService.isStreamingPending$;
|
|
9282
9889
|
this.countdown$ = this.httpManagerService.countdown$;
|
|
9283
9890
|
this.GET_error$ = new BehaviorSubject('');
|
|
9284
9891
|
this.POST_error$ = new BehaviorSubject('');
|
|
@@ -9523,11 +10130,9 @@ class RequestManagerDemoComponent {
|
|
|
9523
10130
|
reqParams.apiOptions.stream = true;
|
|
9524
10131
|
reqParams.apiOptions.streamType = StreamType.NDJSON;
|
|
9525
10132
|
this.requestParams.GET = reqParams.apiOptions;
|
|
9526
|
-
this.STREAM$ = EMPTY; // Cancel any active stream before starting new one
|
|
9527
|
-
this.STREAM_error$.next('');
|
|
9528
10133
|
this.STREAM$ = this.httpManagerService.getRequest(reqParams.apiOptions, reqParams.path)
|
|
9529
10134
|
.pipe(tap((data) => {
|
|
9530
|
-
|
|
10135
|
+
console.log("API STREAM response", data);
|
|
9531
10136
|
if (data && data.length > 0) {
|
|
9532
10137
|
this.updateDisplayedColumns(data);
|
|
9533
10138
|
}
|
|
@@ -9710,11 +10315,11 @@ class RequestManagerDemoComponent {
|
|
|
9710
10315
|
return results.filter(r => r === undefined).length;
|
|
9711
10316
|
}
|
|
9712
10317
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
9713
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\" class=\"btn\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked && !(isPending$ | async)) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n @if ((GET$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n @if ((POST$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((PUT$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n @if ((DELETE$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <!-- <div *ngIf=\"(STREAM_error$ | async) as stream_error\" style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div> -->\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <mat-progress-bar mode=\"determinate\" [value]=\"(progress$ | async)?.percent ?? 0\" [style.display]=\"(isStreamingPending$ | async) ? 'block' : 'none'\"></mat-progress-bar>\n <table mat-table [dataSource]=\"data\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Records: {{ data.length }}\n </div>\n </div>\n }\n </div>\n\n</div>\n\n<div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n</div>\n\n<div style=\"margin-top: 2rem\">\n <!-- Parallel Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Parallel Requests</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteParallelBatch()\"\n [disabled]=\"isParallelLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>sync</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isParallelLoading ? 'Loading...' : 'Fetch 5 Users' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isParallelLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests (concurrency: 3)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (parallelResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of parallelResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>User #{{ result.id }}:</strong>\n <span>{{ result.name }}</span>\n </div>\n <span style=\"color: #2196f3; font-weight: bold;\">Complete</span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (parallelExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>\u23F1Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(parallelExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Parallel</span>\n </div>\n </div>\n </div>\n }\n\n <!-- Error Display -->\n @if (parallelError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ parallelError }}</mat-error>\n </div>\n }\n </div>\n\n <!-- Sequential Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Sequential Requests</h2>\n <button\n mat-raised-button\n color=\"accent\"\n (click)=\"onExecuteSequentialBatch()\"\n [disabled]=\"isSequentialLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>timeline</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isSequentialLoading ? 'Loading...' : 'Fetch 5 Todos' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isSequentialLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing sequential requests (one at a time)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (sequentialResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of sequentialResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Todo #{{ result.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ result.title }}\n </span>\n </div>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (sequentialExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(sequentialExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Sequential</span>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Stream Mode Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Stream Mode (Real-time States)</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteStreamBatch()\"\n [disabled]=\"isStreamLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>auto_awesome</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isStreamLoading ? 'Loading...' : 'Fetch 5 Posts' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isStreamLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests with real-time state updates...\n </p>\n </div>\n }\n\n <!-- Progress Bar -->\n @if (streamProgress) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem; margin-bottom: 0.5rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(streamExecutionTime || 0) }}\n </span>\n </div>\n <div>\n <strong>Progress:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.completed }} / {{ streamProgress.total }} completed\n </span>\n </div>\n <div>\n <strong>Pending:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.pending }}\n </span>\n </div>\n <div>\n <strong>Failed:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.failed }}\n </span>\n </div>\n </div>\n <mat-progress-bar\n mode=\"determinate\"\n [value]=\"streamProgress.percent\"\n color=\"accent\">\n </mat-progress-bar>\n <p style=\"margin-top: 0.5rem; text-align: right; color: #666;\">\n {{ streamProgress.percent }}%\n </p>\n </div>\n }\n\n <!-- Error Display -->\n @if (streamError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ streamError }}</mat-error>\n </div>\n }\n\n <!-- Individual Request States -->\n @if (streamBatchStates.length > 0) {\n <div style=\"margin-top: 1rem;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (state of filteredStreamStates; track state.index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ isPendingState(state) ? 'background: #fff3e0; border-color: #ff9800;' :\n isSuccessState(state) ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (isPendingState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-spinner diameter=\"16\" style=\"margin-right: 0.5rem;\"></mat-spinner>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Loading...</span>\n </div>\n } @else if (isSuccessState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Post #{{ state.data.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ state.data.title }}\n </span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Failed</span>\n <mat-chip style=\"margin-left: auto; font-size: 0.8em; background: #f44336; color: white;\">\n Error\n </mat-chip>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n </div>\n\n <!-- Comparison Info -->\n <div style=\"padding: 1rem; background: whitesmoke; border-radius: 4px; border: 1px solid #ccc;\">\n <h4 style=\"margin-top: 0;\">Key Differences</h4>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;\">\n <div>\n <strong>Parallel:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Faster execution (concurrent)</li>\n <li>Multiple requests simultaneously</li>\n <li>Results may complete out of order</li>\n <li>Best for: Independent requests</li>\n </ul>\n </div>\n <div>\n <strong>Sequential:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Slower execution (one-by-one)</li>\n <li>Requests execute in order</li>\n <li>Results always in request order</li>\n <li>Best for: Dependent requests</li>\n </ul>\n </div>\n </div>\n </div>\n</div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}.batch-demo-section{margin:2rem 0;padding:1.5rem;border:1px solid #e0e0e0;border-radius:8px;background:#fafafa}.batch-result-item{padding:.75rem;border-radius:4px;margin-bottom:.5rem;display:flex;align-items:center;gap:.5rem}.batch-result-item.success{background:#e8f5e9;border-left:3px solid #4caf50}.batch-result-item.error{background:#ffebee;border-left:3px solid #f44336}.execution-metrics{display:flex;gap:2rem;padding:1rem;background:#f5f5f5;border-radius:4px;margin:1rem 0}.execution-metrics>div{display:flex;align-items:center;gap:.5rem}.comparison-box{padding:1rem;background:#fff3e0;border-radius:4px;margin-top:2rem}.comparison-box h4{margin-top:0;margin-bottom:1rem}.comparison-box .comparison-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}@media(max-width:768px){.comparison-box .comparison-grid{grid-template-columns:1fr}}.loading-indicator{margin:1rem 0}.result-card{margin-top:1rem}.result-card mat-card-content{padding:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i2.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i16.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i16.MatCardContent, selector: "mat-card-content" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
10318
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\" class=\"btn\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked && !(isPending$ | async)) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n @if ((GET$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n @if ((POST$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((PUT$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n @if ((DELETE$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <!-- <div *ngIf=\"(STREAM_error$ | async) as stream_error\" style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div> -->\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Records: {{ data.length }}\n </div>\n </div>\n }\n </div>\n\n</div>\n\n<div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n</div>\n\n<div style=\"margin-top: 2rem\">\n <!-- Parallel Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Parallel Requests</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteParallelBatch()\"\n [disabled]=\"isParallelLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>sync</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isParallelLoading ? 'Loading...' : 'Fetch 5 Users' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isParallelLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests (concurrency: 3)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (parallelResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of parallelResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>User #{{ result.id }}:</strong>\n <span>{{ result.name }}</span>\n </div>\n <span style=\"color: #2196f3; font-weight: bold;\">Complete</span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (parallelExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>\u23F1Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(parallelExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Parallel</span>\n </div>\n </div>\n </div>\n }\n\n <!-- Error Display -->\n @if (parallelError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ parallelError }}</mat-error>\n </div>\n }\n </div>\n\n <!-- Sequential Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Sequential Requests</h2>\n <button\n mat-raised-button\n color=\"accent\"\n (click)=\"onExecuteSequentialBatch()\"\n [disabled]=\"isSequentialLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>timeline</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isSequentialLoading ? 'Loading...' : 'Fetch 5 Todos' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isSequentialLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing sequential requests (one at a time)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (sequentialResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of sequentialResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Todo #{{ result.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ result.title }}\n </span>\n </div>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (sequentialExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(sequentialExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Sequential</span>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Stream Mode Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Stream Mode (Real-time States)</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteStreamBatch()\"\n [disabled]=\"isStreamLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>auto_awesome</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isStreamLoading ? 'Loading...' : 'Fetch 5 Posts' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isStreamLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests with real-time state updates...\n </p>\n </div>\n }\n\n <!-- Progress Bar -->\n @if (streamProgress) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem; margin-bottom: 0.5rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(streamExecutionTime || 0) }}\n </span>\n </div>\n <div>\n <strong>Progress:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.completed }} / {{ streamProgress.total }} completed\n </span>\n </div>\n <div>\n <strong>Pending:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.pending }}\n </span>\n </div>\n <div>\n <strong>Failed:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.failed }}\n </span>\n </div>\n </div>\n <mat-progress-bar\n mode=\"determinate\"\n [value]=\"streamProgress.percent\"\n color=\"accent\">\n </mat-progress-bar>\n <p style=\"margin-top: 0.5rem; text-align: right; color: #666;\">\n {{ streamProgress.percent }}%\n </p>\n </div>\n }\n\n <!-- Error Display -->\n @if (streamError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ streamError }}</mat-error>\n </div>\n }\n\n <!-- Individual Request States -->\n @if (streamBatchStates.length > 0) {\n <div style=\"margin-top: 1rem;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (state of filteredStreamStates; track state.index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ isPendingState(state) ? 'background: #fff3e0; border-color: #ff9800;' :\n isSuccessState(state) ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (isPendingState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-spinner diameter=\"16\" style=\"margin-right: 0.5rem;\"></mat-spinner>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Loading...</span>\n </div>\n } @else if (isSuccessState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Post #{{ state.data.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ state.data.title }}\n </span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Failed</span>\n <mat-chip style=\"margin-left: auto; font-size: 0.8em; background: #f44336; color: white;\">\n Error\n </mat-chip>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n </div>\n\n <!-- Comparison Info -->\n <div style=\"padding: 1rem; background: whitesmoke; border-radius: 4px; border: 1px solid #ccc;\">\n <h4 style=\"margin-top: 0;\">Key Differences</h4>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;\">\n <div>\n <strong>Parallel:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Faster execution (concurrent)</li>\n <li>Multiple requests simultaneously</li>\n <li>Results may complete out of order</li>\n <li>Best for: Independent requests</li>\n </ul>\n </div>\n <div>\n <strong>Sequential:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Slower execution (one-by-one)</li>\n <li>Requests execute in order</li>\n <li>Results always in request order</li>\n <li>Best for: Dependent requests</li>\n </ul>\n </div>\n </div>\n </div>\n</div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}.batch-demo-section{margin:2rem 0;padding:1.5rem;border:1px solid #e0e0e0;border-radius:8px;background:#fafafa}.batch-result-item{padding:.75rem;border-radius:4px;margin-bottom:.5rem;display:flex;align-items:center;gap:.5rem}.batch-result-item.success{background:#e8f5e9;border-left:3px solid #4caf50}.batch-result-item.error{background:#ffebee;border-left:3px solid #f44336}.execution-metrics{display:flex;gap:2rem;padding:1rem;background:#f5f5f5;border-radius:4px;margin:1rem 0}.execution-metrics>div{display:flex;align-items:center;gap:.5rem}.comparison-box{padding:1rem;background:#fff3e0;border-radius:4px;margin-top:2rem}.comparison-box h4{margin-top:0;margin-bottom:1rem}.comparison-box .comparison-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}@media(max-width:768px){.comparison-box .comparison-grid{grid-template-columns:1fr}}.loading-indicator{margin:1rem 0}.result-card{margin-top:1rem}.result-card mat-card-content{padding:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i2.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i16.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i16.MatCardContent, selector: "mat-card-content" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
9714
10319
|
}
|
|
9715
10320
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerDemoComponent, decorators: [{
|
|
9716
10321
|
type: Component,
|
|
9717
|
-
args: [{ selector: 'app-request-manager-demo', providers: [HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\" class=\"btn\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked && !(isPending$ | async)) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n @if ((GET$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n @if ((POST$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((PUT$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n @if ((DELETE$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <!-- <div *ngIf=\"(STREAM_error$ | async) as stream_error\" style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div> -->\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <mat-progress-bar mode=\"determinate\" [value]=\"(progress$ | async)?.percent ?? 0\" [style.display]=\"(isStreamingPending$ | async) ? 'block' : 'none'\"></mat-progress-bar>\n <table mat-table [dataSource]=\"data\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Records: {{ data.length }}\n </div>\n </div>\n }\n </div>\n\n</div>\n\n<div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n</div>\n\n<div style=\"margin-top: 2rem\">\n <!-- Parallel Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Parallel Requests</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteParallelBatch()\"\n [disabled]=\"isParallelLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>sync</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isParallelLoading ? 'Loading...' : 'Fetch 5 Users' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isParallelLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests (concurrency: 3)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (parallelResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of parallelResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>User #{{ result.id }}:</strong>\n <span>{{ result.name }}</span>\n </div>\n <span style=\"color: #2196f3; font-weight: bold;\">Complete</span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (parallelExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>\u23F1Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(parallelExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Parallel</span>\n </div>\n </div>\n </div>\n }\n\n <!-- Error Display -->\n @if (parallelError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ parallelError }}</mat-error>\n </div>\n }\n </div>\n\n <!-- Sequential Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Sequential Requests</h2>\n <button\n mat-raised-button\n color=\"accent\"\n (click)=\"onExecuteSequentialBatch()\"\n [disabled]=\"isSequentialLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>timeline</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isSequentialLoading ? 'Loading...' : 'Fetch 5 Todos' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isSequentialLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing sequential requests (one at a time)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (sequentialResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of sequentialResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Todo #{{ result.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ result.title }}\n </span>\n </div>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (sequentialExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(sequentialExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Sequential</span>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Stream Mode Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Stream Mode (Real-time States)</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteStreamBatch()\"\n [disabled]=\"isStreamLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>auto_awesome</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isStreamLoading ? 'Loading...' : 'Fetch 5 Posts' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isStreamLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests with real-time state updates...\n </p>\n </div>\n }\n\n <!-- Progress Bar -->\n @if (streamProgress) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem; margin-bottom: 0.5rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(streamExecutionTime || 0) }}\n </span>\n </div>\n <div>\n <strong>Progress:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.completed }} / {{ streamProgress.total }} completed\n </span>\n </div>\n <div>\n <strong>Pending:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.pending }}\n </span>\n </div>\n <div>\n <strong>Failed:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.failed }}\n </span>\n </div>\n </div>\n <mat-progress-bar\n mode=\"determinate\"\n [value]=\"streamProgress.percent\"\n color=\"accent\">\n </mat-progress-bar>\n <p style=\"margin-top: 0.5rem; text-align: right; color: #666;\">\n {{ streamProgress.percent }}%\n </p>\n </div>\n }\n\n <!-- Error Display -->\n @if (streamError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ streamError }}</mat-error>\n </div>\n }\n\n <!-- Individual Request States -->\n @if (streamBatchStates.length > 0) {\n <div style=\"margin-top: 1rem;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (state of filteredStreamStates; track state.index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ isPendingState(state) ? 'background: #fff3e0; border-color: #ff9800;' :\n isSuccessState(state) ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (isPendingState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-spinner diameter=\"16\" style=\"margin-right: 0.5rem;\"></mat-spinner>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Loading...</span>\n </div>\n } @else if (isSuccessState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Post #{{ state.data.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ state.data.title }}\n </span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Failed</span>\n <mat-chip style=\"margin-left: auto; font-size: 0.8em; background: #f44336; color: white;\">\n Error\n </mat-chip>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n </div>\n\n <!-- Comparison Info -->\n <div style=\"padding: 1rem; background: whitesmoke; border-radius: 4px; border: 1px solid #ccc;\">\n <h4 style=\"margin-top: 0;\">Key Differences</h4>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;\">\n <div>\n <strong>Parallel:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Faster execution (concurrent)</li>\n <li>Multiple requests simultaneously</li>\n <li>Results may complete out of order</li>\n <li>Best for: Independent requests</li>\n </ul>\n </div>\n <div>\n <strong>Sequential:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Slower execution (one-by-one)</li>\n <li>Requests execute in order</li>\n <li>Results always in request order</li>\n <li>Best for: Dependent requests</li>\n </ul>\n </div>\n </div>\n </div>\n</div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}.batch-demo-section{margin:2rem 0;padding:1.5rem;border:1px solid #e0e0e0;border-radius:8px;background:#fafafa}.batch-result-item{padding:.75rem;border-radius:4px;margin-bottom:.5rem;display:flex;align-items:center;gap:.5rem}.batch-result-item.success{background:#e8f5e9;border-left:3px solid #4caf50}.batch-result-item.error{background:#ffebee;border-left:3px solid #f44336}.execution-metrics{display:flex;gap:2rem;padding:1rem;background:#f5f5f5;border-radius:4px;margin:1rem 0}.execution-metrics>div{display:flex;align-items:center;gap:.5rem}.comparison-box{padding:1rem;background:#fff3e0;border-radius:4px;margin-top:2rem}.comparison-box h4{margin-top:0;margin-bottom:1rem}.comparison-box .comparison-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}@media(max-width:768px){.comparison-box .comparison-grid{grid-template-columns:1fr}}.loading-indicator{margin:1rem 0}.result-card{margin-top:1rem}.result-card mat-card-content{padding:1rem}\n"] }]
|
|
10322
|
+
args: [{ selector: 'app-request-manager-demo', providers: [HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\" class=\"btn\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked && !(isPending$ | async)) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n @if ((GET$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n @if ((POST$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((PUT$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <h3>Include Record ID in the RestPath</h3>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n @if ((DELETE$ | async); as dataRecord) {\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n {{ dataRecord | json }}\n </div>\n }\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\">Request</button>\n </div>\n </div>\n\n <!-- <div *ngIf=\"(STREAM_error$ | async) as stream_error\" style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div> -->\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Records: {{ data.length }}\n </div>\n </div>\n }\n </div>\n\n</div>\n\n<div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n</div>\n\n<div style=\"margin-top: 2rem\">\n <!-- Parallel Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Parallel Requests</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteParallelBatch()\"\n [disabled]=\"isParallelLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>sync</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isParallelLoading ? 'Loading...' : 'Fetch 5 Users' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isParallelLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests (concurrency: 3)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (parallelResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of parallelResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>User #{{ result.id }}:</strong>\n <span>{{ result.name }}</span>\n </div>\n <span style=\"color: #2196f3; font-weight: bold;\">Complete</span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (parallelExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>\u23F1Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(parallelExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Parallel</span>\n </div>\n </div>\n </div>\n }\n\n <!-- Error Display -->\n @if (parallelError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ parallelError }}</mat-error>\n </div>\n }\n </div>\n\n <!-- Sequential Requests Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Sequential Requests</h2>\n <button\n mat-raised-button\n color=\"accent\"\n (click)=\"onExecuteSequentialBatch()\"\n [disabled]=\"isSequentialLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>timeline</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isSequentialLoading ? 'Loading...' : 'Fetch 5 Todos' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isSequentialLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing sequential requests (one at a time)...\n </p>\n </div>\n }\n\n <!-- Results -->\n @if (sequentialResults) {\n <div style=\"margin: 1rem 0;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (result of sequentialResults; track $index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ result ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (result) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Todo #{{ result.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ result.title }}\n </span>\n </div>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem; justify-content: space-between;\">\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ i + 1 }}:</strong>\n <span>Failed</span>\n </div>\n <span style=\"color: #f44336; font-weight: bold;\">Error</span>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n <!-- Execution Metrics -->\n @if (sequentialExecutionTime !== undefined) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(sequentialExecutionTime) }}\n </span>\n </div>\n <div>\n <strong>Mode:</strong>\n <span style=\"margin-left: 0.5rem;\">Sequential</span>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Stream Mode Section -->\n <div>\n <div style=\"display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;\">\n <h2 style=\"flex: 1; margin: 0;\">Stream Mode (Real-time States)</h2>\n <button\n mat-raised-button\n color=\"primary\"\n (click)=\"onExecuteStreamBatch()\"\n [disabled]=\"isStreamLoading\"\n style=\"min-width: 200px;\">\n <mat-icon>auto_awesome</mat-icon>\n <span style=\"margin-left: 8px;\">\n {{ isStreamLoading ? 'Loading...' : 'Fetch 5 Posts' }}\n </span>\n </button>\n </div>\n\n <!-- Loading Indicator -->\n @if (isStreamLoading) {\n <div style=\"margin: 1rem 0;\">\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n <p style=\"margin-top: 0.5rem; color: #666;\">\n Executing parallel requests with real-time state updates...\n </p>\n </div>\n }\n\n <!-- Progress Bar -->\n @if (streamProgress) {\n <div style=\"background: #f5f5f5; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">\n <div style=\"display: flex; gap: 2rem; margin-bottom: 0.5rem;\">\n <div>\n <strong>Execution Time:</strong>\n <span style=\"margin-left: 0.5rem; color: #4caf50;\">\n {{ formatTime(streamExecutionTime || 0) }}\n </span>\n </div>\n <div>\n <strong>Progress:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.completed }} / {{ streamProgress.total }} completed\n </span>\n </div>\n <div>\n <strong>Pending:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.pending }}\n </span>\n </div>\n <div>\n <strong>Failed:</strong>\n <span style=\"margin-left: 0.5rem;\">\n {{ streamProgress.failed }}\n </span>\n </div>\n </div>\n <mat-progress-bar\n mode=\"determinate\"\n [value]=\"streamProgress.percent\"\n color=\"accent\">\n </mat-progress-bar>\n <p style=\"margin-top: 0.5rem; text-align: right; color: #666;\">\n {{ streamProgress.percent }}%\n </p>\n </div>\n }\n\n <!-- Error Display -->\n @if (streamError) {\n <div style=\"margin-top: 1rem;\">\n <mat-error>{{ streamError }}</mat-error>\n </div>\n }\n\n <!-- Individual Request States -->\n @if (streamBatchStates.length > 0) {\n <div style=\"margin-top: 1rem;\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <div style=\"display: grid; gap: 0.5rem;\">\n @for (state of filteredStreamStates; track state.index; let i = $index) {\n <div\n style=\"padding: 0.75rem; border-radius: 4px; border: 1px solid;\n {{ isPendingState(state) ? 'background: #fff3e0; border-color: #ff9800;' :\n isSuccessState(state) ? 'background: #e3f2fd; border-color: #2196f3;' :\n 'background: #ffebee; border-color: #f44336;' }}\">\n @if (isPendingState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-spinner diameter=\"16\" style=\"margin-right: 0.5rem;\"></mat-spinner>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Loading...</span>\n </div>\n } @else if (isSuccessState(state)) {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #2196f3; font-size: 18px;\">check_circle</mat-icon>\n <strong>Post #{{ state.data.id }}:</strong>\n <span style=\"flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">\n {{ state.data.title }}\n </span>\n </div>\n } @else {\n <div style=\"display: flex; align-items: center; gap: 0.5rem;\">\n <mat-icon style=\"color: #f44336; font-size: 18px;\">error</mat-icon>\n <strong>Request {{ state.index + 1 }}:</strong>\n <span>Failed</span>\n <mat-chip style=\"margin-left: auto; font-size: 0.8em; background: #f44336; color: white;\">\n Error\n </mat-chip>\n </div>\n }\n </div>\n }\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n </div>\n\n <!-- Comparison Info -->\n <div style=\"padding: 1rem; background: whitesmoke; border-radius: 4px; border: 1px solid #ccc;\">\n <h4 style=\"margin-top: 0;\">Key Differences</h4>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;\">\n <div>\n <strong>Parallel:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Faster execution (concurrent)</li>\n <li>Multiple requests simultaneously</li>\n <li>Results may complete out of order</li>\n <li>Best for: Independent requests</li>\n </ul>\n </div>\n <div>\n <strong>Sequential:</strong>\n <ul style=\"margin: 0.5rem 0 0 1rem; padding: 0;\">\n <li>Slower execution (one-by-one)</li>\n <li>Requests execute in order</li>\n <li>Results always in request order</li>\n <li>Best for: Dependent requests</li>\n </ul>\n </div>\n </div>\n </div>\n</div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}.batch-demo-section{margin:2rem 0;padding:1.5rem;border:1px solid #e0e0e0;border-radius:8px;background:#fafafa}.batch-result-item{padding:.75rem;border-radius:4px;margin-bottom:.5rem;display:flex;align-items:center;gap:.5rem}.batch-result-item.success{background:#e8f5e9;border-left:3px solid #4caf50}.batch-result-item.error{background:#ffebee;border-left:3px solid #f44336}.execution-metrics{display:flex;gap:2rem;padding:1rem;background:#f5f5f5;border-radius:4px;margin:1rem 0}.execution-metrics>div{display:flex;align-items:center;gap:.5rem}.comparison-box{padding:1rem;background:#fff3e0;border-radius:4px;margin-top:2rem}.comparison-box h4{margin-top:0;margin-bottom:1rem}.comparison-box .comparison-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}@media(max-width:768px){.comparison-box .comparison-grid{grid-template-columns:1fr}}.loading-indicator{margin:1rem 0}.result-card{margin-top:1rem}.result-card mat-card-content{padding:1rem}\n"] }]
|
|
9718
10323
|
}], ctorParameters: () => [], propDecorators: { server: [{
|
|
9719
10324
|
type: Input
|
|
9720
10325
|
}], adapter: [{
|
|
@@ -12075,5 +12680,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12075
12680
|
* Generated bundle index. Do not edit.
|
|
12076
12681
|
*/
|
|
12077
12682
|
|
|
12078
|
-
export { ApiRequest, AppService, AsymmetricalEncryptionService, BatchOptions, BatchResult, CONFIG_SETTINGS_TOKEN, ChannelType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, InvalidFileInfoModel, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsDemoComponent, LocalStorageSignalsManagerService, LoggerService, NotificationMessage, OperationResultModel, PathQueryService, PublicMessage, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateMessage, StateOperationResult, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StoreStateManagerSignalsService, StoreStateSignalsDemoComponent, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UploadDemoComponent, UploadValidationErrorModel, UserData, UtilsService, WSOptions, WebSocketMessageService, WithCredentialsInterceptor, calculateBatchProgress, countdown, createChannelName, delayedRetry, isErrorState, isPendingState, isSuccessState, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
12683
|
+
export { ApiRequest, AppService, AsymmetricalEncryptionService, BatchOptions, BatchResult, CONFIG_SETTINGS_TOKEN, ChannelType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, InvalidFileInfoModel, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsDemoComponent, LocalStorageSignalsManagerService, LoggerService, NormalizedRequestOptionsModel, NotificationMessage, OperationResultModel, PathQueryService, PathTrackerStateModel, PublicMessage, QueryParamsTrackerOptionsModel, QueryParamsTrackerService, QueryTrackerStateModel, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateMessage, StateOperationResult, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StoreStateManagerSignalsService, StoreStateSignalsDemoComponent, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UploadDemoComponent, UploadValidationErrorModel, UserData, UtilsService, WSOptions, WebSocketMessageService, WithCredentialsInterceptor, calculateBatchProgress, countdown, createChannelName, delayedRetry, isErrorState, isPendingState, isSuccessState, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
12079
12684
|
//# sourceMappingURL=http-request-manager.mjs.map
|