@memberjunction/core 5.28.0 → 5.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generic/RegisterForStartup.d.ts.map +1 -1
- package/dist/generic/RegisterForStartup.js +10 -1
- package/dist/generic/RegisterForStartup.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +37 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +30 -4
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/column-descriptors.d.ts +72 -0
- package/dist/generic/column-descriptors.d.ts.map +1 -0
- package/dist/generic/column-descriptors.js +70 -0
- package/dist/generic/column-descriptors.js.map +1 -0
- package/dist/generic/data-snapshot.d.ts +63 -0
- package/dist/generic/data-snapshot.d.ts.map +1 -0
- package/dist/generic/data-snapshot.js +78 -0
- package/dist/generic/data-snapshot.js.map +1 -0
- package/dist/generic/data-table.d.ts +103 -0
- package/dist/generic/data-table.d.ts.map +1 -0
- package/dist/generic/data-table.js +35 -0
- package/dist/generic/data-table.js.map +1 -0
- package/dist/generic/entityInfo.d.ts +27 -3
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +41 -15
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/permissionInterfaces.d.ts +306 -0
- package/dist/generic/permissionInterfaces.d.ts.map +1 -0
- package/dist/generic/permissionInterfaces.js +175 -0
- package/dist/generic/permissionInterfaces.js.map +1 -0
- package/dist/generic/providerBase.d.ts +48 -4
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +236 -47
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +8 -2
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +31 -11
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -26,11 +26,16 @@ import { TransformSimpleObjectToEntityObject } from "./util.js";
|
|
|
26
26
|
export function MetadataFromSimpleObject(data, md) {
|
|
27
27
|
try {
|
|
28
28
|
const newObject = MetadataFromSimpleObjectWithoutUser(data, md);
|
|
29
|
+
if (!newObject) {
|
|
30
|
+
LogError('MetadataFromSimpleObject: MetadataFromSimpleObjectWithoutUser returned undefined');
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
29
33
|
newObject.CurrentUser = data.CurrentUser ? new UserInfo(md, data.CurrentUser) : null;
|
|
30
34
|
return newObject;
|
|
31
35
|
}
|
|
32
36
|
catch (e) {
|
|
33
|
-
LogError(e);
|
|
37
|
+
LogError(`MetadataFromSimpleObject failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
38
|
+
return undefined;
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
@@ -53,14 +58,26 @@ export function MetadataFromSimpleObjectWithoutUser(data, md) {
|
|
|
53
58
|
// at this point, only do this particular property if we have a match, it is either prefixed with All or not
|
|
54
59
|
// for example in our strongly typed AllMetadata class we have AllQueryCategories, but in the simple allMetadata object we have QueryCategories
|
|
55
60
|
// so we need to check for both which is what the above is doing.
|
|
56
|
-
// Build the array of the correct type and initialize with the simple object
|
|
57
|
-
|
|
61
|
+
// Build the array of the correct type and initialize with the simple object.
|
|
62
|
+
// Individual item failures are logged but do not abort the entire deserialization —
|
|
63
|
+
// a cache with 514 of 515 entities is far better than no cache at all.
|
|
64
|
+
const items = [];
|
|
65
|
+
for (const d of data[simpleKey]) {
|
|
66
|
+
try {
|
|
67
|
+
items.push(new m.class(d, md));
|
|
68
|
+
}
|
|
69
|
+
catch (itemErr) {
|
|
70
|
+
LogError(`MetadataFromSimpleObject: failed to construct ${m.class?.name || m.key} item: ${itemErr instanceof Error ? itemErr.message : String(itemErr)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
returnMetadata[m.key] = items;
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
return returnMetadata;
|
|
61
77
|
}
|
|
62
78
|
catch (e) {
|
|
63
|
-
LogError(e);
|
|
79
|
+
LogError(`MetadataFromSimpleObjectWithoutUser failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
80
|
+
return undefined;
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
/**
|
|
@@ -171,8 +188,20 @@ export class ProviderBase {
|
|
|
171
188
|
* Set to false to disable.
|
|
172
189
|
*/
|
|
173
190
|
static { this.FastStartupMode = true; }
|
|
174
|
-
/** Tracks whether fast startup has been consumed (auto-disables after
|
|
191
|
+
/** Tracks whether fast startup has been consumed (auto-disables after startup completes) */
|
|
175
192
|
static { this._fastStartupConsumed = false; }
|
|
193
|
+
/**
|
|
194
|
+
* Marks the fast-startup window as closed. After this call, all RunViews
|
|
195
|
+
* requests will use normal server-validated caching instead of trusting
|
|
196
|
+
* local IndexedDB unconditionally. Called by StartupManager after all
|
|
197
|
+
* engines have completed their initial load.
|
|
198
|
+
*/
|
|
199
|
+
static ConsumeFastStartupMode() {
|
|
200
|
+
if (!ProviderBase._fastStartupConsumed) {
|
|
201
|
+
ProviderBase._fastStartupConsumed = true;
|
|
202
|
+
LogStatusEx({ message: '⚡ [Fast-Start] Startup complete — fast-start mode disabled, server validation re-enabled', verboseOnly: false });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
176
205
|
// ── Request Deduplication + Linger Window ──────────────────────────
|
|
177
206
|
/**
|
|
178
207
|
* How long (ms) a resolved RunViews result stays available for instant
|
|
@@ -949,10 +978,15 @@ export class ProviderBase {
|
|
|
949
978
|
const callerRequestedFields = params.Fields && params.Fields.length > 0
|
|
950
979
|
? params.Fields.map(f => f.trim().toLowerCase())
|
|
951
980
|
: null; // null = caller wants all fields
|
|
952
|
-
//
|
|
953
|
-
//
|
|
981
|
+
// Only override Fields to all entity fields when caching will actually happen
|
|
982
|
+
// for this call. For non-cached calls we respect the caller's narrow Fields
|
|
983
|
+
// end-to-end — there's no cache-coherence concern to preserve.
|
|
954
984
|
const entity = params.EntityName ? this.EntityByName(params.EntityName) : null;
|
|
955
|
-
|
|
985
|
+
const entityCacheAllowed = this.IsServerCacheAllowedForEntity(params);
|
|
986
|
+
const willCache = !params.BypassCache &&
|
|
987
|
+
(params.CacheLocal || this.TrustLocalCacheCompletely) &&
|
|
988
|
+
entityCacheAllowed;
|
|
989
|
+
if (entity && willCache) {
|
|
956
990
|
params.Fields = entity.Fields.map(f => f.Name);
|
|
957
991
|
}
|
|
958
992
|
const entityLookupTime = performance.now() - entityLookupStart;
|
|
@@ -961,8 +995,7 @@ export class ProviderBase {
|
|
|
961
995
|
let cacheStatus = 'disabled';
|
|
962
996
|
let cachedResult;
|
|
963
997
|
let fingerprint;
|
|
964
|
-
|
|
965
|
-
if ((params.CacheLocal || this.TrustLocalCacheCompletely) && entityCacheAllowed && LocalCacheManager.Instance.IsInitialized) {
|
|
998
|
+
if (willCache && LocalCacheManager.Instance.IsInitialized) {
|
|
966
999
|
fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(params, this.InstanceConnectionString);
|
|
967
1000
|
const cached = await LocalCacheManager.Instance.GetRunViewResult(fingerprint);
|
|
968
1001
|
if (cached) {
|
|
@@ -1056,21 +1089,23 @@ export class ProviderBase {
|
|
|
1056
1089
|
&& LocalCacheManager.Instance.IsInitialized
|
|
1057
1090
|
&& params.some(p => p.CacheLocal);
|
|
1058
1091
|
if (useFastStartup) {
|
|
1059
|
-
// Check if we actually have cached data for ALL params
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1092
|
+
// Check if we actually have cached data for ALL params.
|
|
1093
|
+
// Use Promise.all to parallelize IndexedDB reads — sequential awaits
|
|
1094
|
+
// cause ~100ms Zone.js scheduling overhead per read, which adds up to
|
|
1095
|
+
// 10+ seconds across 86+ views.
|
|
1096
|
+
const cacheCheckResults = await Promise.all(params.map(async (param) => {
|
|
1097
|
+
if (!param.CacheLocal)
|
|
1098
|
+
return true; // non-cached params don't block
|
|
1099
|
+
const fp = LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
|
|
1100
|
+
const cached = await LocalCacheManager.Instance.GetRunViewResult(fp);
|
|
1101
|
+
return cached != null;
|
|
1102
|
+
}));
|
|
1103
|
+
const allHaveCachedData = cacheCheckResults.every(Boolean);
|
|
1071
1104
|
if (allHaveCachedData) {
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1105
|
+
// Do NOT consume the fast-start flag here — multiple engines fire
|
|
1106
|
+
// RunViews in parallel during StartupManager.Startup(), and each one
|
|
1107
|
+
// should benefit from the local cache. StartupManager.Startup() will
|
|
1108
|
+
// call ConsumeFastStartupMode() after all engines complete.
|
|
1074
1109
|
const entityNames = params.map(p => p.EntityName || p.ViewName || '?').join(', ');
|
|
1075
1110
|
LogStatusEx({
|
|
1076
1111
|
message: `⚡ [Fast-Start] Trusting local cache for ${params.length} views [${entityNames}] — skipping server validation`,
|
|
@@ -1080,7 +1115,6 @@ export class ProviderBase {
|
|
|
1080
1115
|
}
|
|
1081
1116
|
else {
|
|
1082
1117
|
// Not all params have cached data — use normal smart cache check
|
|
1083
|
-
ProviderBase._fastStartupConsumed = true; // Still consume the fast-start flag
|
|
1084
1118
|
const useSmartCacheCheck = params.some(p => p.CacheLocal);
|
|
1085
1119
|
if (useSmartCacheCheck) {
|
|
1086
1120
|
return this.prepareSmartCacheCheckParams(params, telemetryEventId, contextUser);
|
|
@@ -1108,14 +1142,21 @@ export class ProviderBase {
|
|
|
1108
1142
|
const callerFields = param.Fields && param.Fields.length > 0
|
|
1109
1143
|
? param.Fields.map(f => f.trim().toLowerCase())
|
|
1110
1144
|
: null;
|
|
1145
|
+
// Only override Fields to all entity fields when caching will actually happen
|
|
1146
|
+
// for this call. For non-cached calls we respect the caller's narrow Fields
|
|
1147
|
+
// end-to-end — there's no cache-coherence concern to preserve.
|
|
1111
1148
|
const batchEntity = param.EntityName ? this.EntityByName(param.EntityName) : null;
|
|
1112
|
-
|
|
1149
|
+
const batchEntityCacheAllowed = this.IsServerCacheAllowedForEntity(param);
|
|
1150
|
+
const batchWillCache = !param.BypassCache &&
|
|
1151
|
+
(param.CacheLocal || this.TrustLocalCacheCompletely) &&
|
|
1152
|
+
batchEntityCacheAllowed;
|
|
1153
|
+
if (batchEntity && batchWillCache) {
|
|
1113
1154
|
param.Fields = batchEntity.Fields.map(f => f.Name);
|
|
1114
1155
|
}
|
|
1115
1156
|
// Check local cache if enabled or if server trusts its cache completely
|
|
1116
1157
|
// BypassCache skips cache entirely — used by maintenance actions querying for
|
|
1117
1158
|
// records that were inserted via direct SQL (bypassing BaseEntity.Save())
|
|
1118
|
-
if (
|
|
1159
|
+
if (batchWillCache && LocalCacheManager.Instance.IsInitialized) {
|
|
1119
1160
|
const fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
|
|
1120
1161
|
const cached = await LocalCacheManager.Instance.GetRunViewResult(fingerprint);
|
|
1121
1162
|
if (cached) {
|
|
@@ -1943,9 +1984,10 @@ export class ProviderBase {
|
|
|
1943
1984
|
await this.LoadLocalMetadataFromStorage();
|
|
1944
1985
|
if (this._localMetadata?.AllEntities?.length) {
|
|
1945
1986
|
LogStatusEx({ message: `⚡ [Fast-Start] Loaded ${this._localMetadata.AllEntities.length} entities from local cache — deferring server validation`, verboseOnly: false });
|
|
1946
|
-
//
|
|
1947
|
-
//
|
|
1948
|
-
|
|
1987
|
+
// Do NOT kick off background validation here — it writes to IndexedDB
|
|
1988
|
+
// which contends with engine RunView cache reads during StartupManager.
|
|
1989
|
+
// Instead, the caller (setupGraphQLClient) should call
|
|
1990
|
+
// BackgroundValidateAndRefresh() after StartupManager completes.
|
|
1949
1991
|
return true; // App can proceed immediately with cached metadata
|
|
1950
1992
|
}
|
|
1951
1993
|
}
|
|
@@ -2006,6 +2048,60 @@ export class ProviderBase {
|
|
|
2006
2048
|
// Not critical — app continues with cached metadata
|
|
2007
2049
|
}
|
|
2008
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Synchronous pre-validation of cached metadata before engine startup.
|
|
2053
|
+
*
|
|
2054
|
+
* This is the deterministic counterpart to {@link backgroundValidateAndRefresh}: instead
|
|
2055
|
+
* of letting engines fast-start against potentially-stale cached data and self-healing
|
|
2056
|
+
* a few seconds later, we make one timestamp round-trip up front. The flow:
|
|
2057
|
+
*
|
|
2058
|
+
* - **Cached metadata is current** → keep `FastStartupMode` enabled. Engines trust
|
|
2059
|
+
* their local IndexedDB caches without per-view smart-cache-check round-trips,
|
|
2060
|
+
* and we have just verified at the framework metadata level that nothing has
|
|
2061
|
+
* drifted since this client last loaded.
|
|
2062
|
+
* - **Cached metadata is stale** → refresh framework metadata in place, then call
|
|
2063
|
+
* {@link ProviderBase.ConsumeFastStartupMode} to disable fast-start. Engines
|
|
2064
|
+
* proceed through the normal smart-cache-check path so each per-view cache is
|
|
2065
|
+
* re-validated against the server.
|
|
2066
|
+
*
|
|
2067
|
+
* Cost on the warm-current path is one batched timestamp fetch (~50–200 ms depending
|
|
2068
|
+
* on RTT). On the warm-stale path we additionally pay the full metadata fetch but
|
|
2069
|
+
* avoid serving stale data to the UI in the first place.
|
|
2070
|
+
*
|
|
2071
|
+
* Caller contract: invoke this before `StartupManager.Startup()` so engines see the
|
|
2072
|
+
* correct fast-start state from their first `RunViews()` call. Failures here are
|
|
2073
|
+
* non-fatal — the engine layer's smart-cache-check + event-based invalidation
|
|
2074
|
+
* remain as a safety net.
|
|
2075
|
+
*/
|
|
2076
|
+
async preValidateAndRefresh(providerToUse) {
|
|
2077
|
+
try {
|
|
2078
|
+
const needsRefresh = await this.CheckToSeeIfRefreshNeeded(providerToUse);
|
|
2079
|
+
if (needsRefresh) {
|
|
2080
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Pre-validation: metadata is stale — refreshing before engine startup and disabling fast-start`, verboseOnly: false });
|
|
2081
|
+
const start = Date.now();
|
|
2082
|
+
const res = await this.GetAllMetadata(providerToUse, false);
|
|
2083
|
+
const elapsed = Date.now() - start;
|
|
2084
|
+
if (res) {
|
|
2085
|
+
this.UpdateLocalMetadata(res);
|
|
2086
|
+
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps;
|
|
2087
|
+
await this.SaveLocalMetadataToStorage();
|
|
2088
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Pre-validation refresh complete (${elapsed}ms) — engines will smart-cache-check`, verboseOnly: false });
|
|
2089
|
+
}
|
|
2090
|
+
// Force engines through the normal smart-cache-check path. This re-validates
|
|
2091
|
+
// each per-view IndexedDB cache against the server rather than trusting it.
|
|
2092
|
+
ProviderBase.ConsumeFastStartupMode();
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Pre-validation: metadata is current — fast-start engaged`, verboseOnly: false });
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
catch (e) {
|
|
2099
|
+
LogError(`[Fast-Start] Pre-validation failed: ${e instanceof Error ? e.message : String(e)} — falling back to smart-cache-check`);
|
|
2100
|
+
// On failure, disable fast-start so engines validate per-view rather than
|
|
2101
|
+
// trusting potentially-stale cache against a known-unknown server state.
|
|
2102
|
+
ProviderBase.ConsumeFastStartupMode();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2009
2105
|
CloneAllMetadata(toClone) {
|
|
2010
2106
|
// we need to create a copy but can't do it the standard way becuase we need object instances
|
|
2011
2107
|
// for various things like EntityInfo
|
|
@@ -2786,28 +2882,90 @@ export class ProviderBase {
|
|
|
2786
2882
|
async LoadLocalMetadataFromStorage() {
|
|
2787
2883
|
try {
|
|
2788
2884
|
const ls = this.LocalStorageProvider;
|
|
2789
|
-
if (ls)
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2885
|
+
if (!ls)
|
|
2886
|
+
return;
|
|
2887
|
+
const overallStart = Date.now();
|
|
2888
|
+
// Load timestamps
|
|
2889
|
+
this._latestLocalMetadataTimestamps = JSON.parse(await ls.GetItem(this.LocalStoragePrefix + ProviderBase.localStorageTimestampsKey));
|
|
2890
|
+
// Read raw data from storage
|
|
2891
|
+
const readStart = Date.now();
|
|
2892
|
+
const raw = await ls.GetItem(this.LocalStoragePrefix + ProviderBase.localStorageAllMetadataKey);
|
|
2893
|
+
const readMs = Date.now() - readStart;
|
|
2894
|
+
if (!raw)
|
|
2895
|
+
return;
|
|
2896
|
+
// Decompress if stored in compressed format, otherwise parse directly
|
|
2897
|
+
const parseStart = Date.now();
|
|
2898
|
+
let temp;
|
|
2899
|
+
const format = await ls.GetItem(this.LocalStoragePrefix + ProviderBase.localStorageFormatKey);
|
|
2900
|
+
if (format === 'gzip' && typeof raw === 'string') {
|
|
2901
|
+
// Compressed path: base64 → binary → gzip decompress → JSON parse
|
|
2902
|
+
const binary = ProviderBase.base64ToArrayBuffer(raw);
|
|
2903
|
+
const blob = new Blob([binary]);
|
|
2904
|
+
const ds = new DecompressionStream('gzip');
|
|
2905
|
+
const decompressedStream = blob.stream().pipeThrough(ds);
|
|
2906
|
+
const jsonString = await new Response(decompressedStream).text();
|
|
2907
|
+
temp = JSON.parse(jsonString);
|
|
2908
|
+
}
|
|
2909
|
+
else {
|
|
2910
|
+
// Legacy uncompressed path
|
|
2911
|
+
temp = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
2912
|
+
}
|
|
2913
|
+
const parseMs = Date.now() - parseStart;
|
|
2914
|
+
if (!temp)
|
|
2915
|
+
return;
|
|
2916
|
+
// Reconstruct typed metadata objects from the parsed JSON
|
|
2917
|
+
const deserializeStart = Date.now();
|
|
2918
|
+
const metadata = MetadataFromSimpleObject(temp, this);
|
|
2919
|
+
const deserializeMs = Date.now() - deserializeStart;
|
|
2920
|
+
if (metadata) {
|
|
2921
|
+
this.UpdateLocalMetadata(metadata);
|
|
2922
|
+
const totalMs = Date.now() - overallStart;
|
|
2923
|
+
LogStatusEx({
|
|
2924
|
+
message: `[Fast-Start Cache] Load complete: read=${readMs}ms, parse=${parseMs}ms, deserialize=${deserializeMs}ms, total=${totalMs}ms, entities=${metadata.AllEntities?.length ?? 0}`,
|
|
2925
|
+
verboseOnly: false
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
else {
|
|
2929
|
+
LogError('[Fast-Start Cache] MetadataFromSimpleObject returned undefined — cache deserialization failed. Check console for per-item errors above.');
|
|
2798
2930
|
}
|
|
2799
2931
|
}
|
|
2800
2932
|
catch (e) {
|
|
2801
|
-
|
|
2933
|
+
LogError(`[Fast-Start Cache] LoadLocalMetadataFromStorage failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2802
2934
|
}
|
|
2803
2935
|
}
|
|
2804
2936
|
static { this.localStorageRootKey = '___MJCore_Metadata'; }
|
|
2805
2937
|
static { this.localStorageTimestampsKey = this.localStorageRootKey + '_Timestamps'; }
|
|
2806
2938
|
static { this.localStorageAllMetadataKey = this.localStorageRootKey + '_AllMetadata'; }
|
|
2939
|
+
static { this.localStorageFormatKey = this.localStorageRootKey + '_Format'; }
|
|
2807
2940
|
static { this.localStorageKeys = [
|
|
2808
2941
|
ProviderBase.localStorageTimestampsKey,
|
|
2809
2942
|
ProviderBase.localStorageAllMetadataKey,
|
|
2943
|
+
ProviderBase.localStorageFormatKey,
|
|
2810
2944
|
]; }
|
|
2945
|
+
/**
|
|
2946
|
+
* Converts a base64-encoded string to an ArrayBuffer.
|
|
2947
|
+
* Used for compressed metadata storage/retrieval.
|
|
2948
|
+
*/
|
|
2949
|
+
static base64ToArrayBuffer(base64) {
|
|
2950
|
+
const binaryString = atob(base64);
|
|
2951
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
2952
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
2953
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
2954
|
+
}
|
|
2955
|
+
return bytes.buffer;
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Converts an ArrayBuffer to a base64-encoded string.
|
|
2959
|
+
* Used for compressed metadata storage/retrieval.
|
|
2960
|
+
*/
|
|
2961
|
+
static arrayBufferToBase64(buffer) {
|
|
2962
|
+
const bytes = new Uint8Array(buffer);
|
|
2963
|
+
let binary = '';
|
|
2964
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
2965
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2966
|
+
}
|
|
2967
|
+
return btoa(binary);
|
|
2968
|
+
}
|
|
2811
2969
|
/**
|
|
2812
2970
|
* This property will return the prefix to use for local storage keys. This is useful if you have multiple instances of a provider running in the same environment
|
|
2813
2971
|
* and you want to keep their local storage keys separate. The default implementation returns an empty string, but subclasses can override this to return a unique string
|
|
@@ -2823,16 +2981,47 @@ export class ProviderBase {
|
|
|
2823
2981
|
async SaveLocalMetadataToStorage() {
|
|
2824
2982
|
try {
|
|
2825
2983
|
const ls = this.LocalStorageProvider;
|
|
2826
|
-
if (ls)
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2984
|
+
if (!ls)
|
|
2985
|
+
return;
|
|
2986
|
+
const start = Date.now();
|
|
2987
|
+
// Save timestamps
|
|
2988
|
+
await ls.SetItem(this.LocalStoragePrefix + ProviderBase.localStorageTimestampsKey, JSON.stringify(this._latestLocalMetadataTimestamps));
|
|
2989
|
+
// Serialize the AllMetadata object
|
|
2990
|
+
const jsonString = JSON.stringify(this._localMetadata);
|
|
2991
|
+
// Attempt compressed storage using native CompressionStream (available in modern browsers and Node 18+)
|
|
2992
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
2993
|
+
try {
|
|
2994
|
+
const blob = new Blob([jsonString]);
|
|
2995
|
+
const cs = new CompressionStream('gzip');
|
|
2996
|
+
const compressedStream = blob.stream().pipeThrough(cs);
|
|
2997
|
+
const compressedBuffer = await new Response(compressedStream).arrayBuffer();
|
|
2998
|
+
const base64 = ProviderBase.arrayBufferToBase64(compressedBuffer);
|
|
2999
|
+
await ls.SetItem(this.LocalStoragePrefix + ProviderBase.localStorageAllMetadataKey, base64);
|
|
3000
|
+
await ls.SetItem(this.LocalStoragePrefix + ProviderBase.localStorageFormatKey, 'gzip');
|
|
3001
|
+
const elapsed = Date.now() - start;
|
|
3002
|
+
const ratio = jsonString.length > 0 ? (base64.length / jsonString.length * 100).toFixed(1) : '?';
|
|
3003
|
+
LogStatusEx({
|
|
3004
|
+
message: `[Fast-Start Cache] Save complete: ${elapsed}ms, raw=${(jsonString.length / 1024 / 1024).toFixed(1)}MB, compressed=${(base64.length / 1024 / 1024).toFixed(1)}MB (${ratio}%)`,
|
|
3005
|
+
verboseOnly: false
|
|
3006
|
+
});
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
catch (compressErr) {
|
|
3010
|
+
// Compression failed — fall through to uncompressed save
|
|
3011
|
+
LogError(`[Fast-Start Cache] Compression failed, falling back to uncompressed: ${compressErr instanceof Error ? compressErr.message : String(compressErr)}`);
|
|
3012
|
+
}
|
|
2831
3013
|
}
|
|
3014
|
+
// Fallback: uncompressed save (older environments without CompressionStream)
|
|
3015
|
+
await ls.SetItem(this.LocalStoragePrefix + ProviderBase.localStorageAllMetadataKey, jsonString);
|
|
3016
|
+
await ls.SetItem(this.LocalStoragePrefix + ProviderBase.localStorageFormatKey, 'json');
|
|
3017
|
+
const elapsed = Date.now() - start;
|
|
3018
|
+
LogStatusEx({
|
|
3019
|
+
message: `[Fast-Start Cache] Save complete (uncompressed): ${elapsed}ms, size=${(jsonString.length / 1024 / 1024).toFixed(1)}MB`,
|
|
3020
|
+
verboseOnly: false
|
|
3021
|
+
});
|
|
2832
3022
|
}
|
|
2833
3023
|
catch (e) {
|
|
2834
|
-
|
|
2835
|
-
LogError(e);
|
|
3024
|
+
LogError(`[Fast-Start Cache] SaveLocalMetadataToStorage failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2836
3025
|
}
|
|
2837
3026
|
}
|
|
2838
3027
|
/**
|