@forgeroute/ui 0.4.78 → 0.4.79
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/fesm2022/forgeroute-ui-charts.mjs +30 -22
- package/fesm2022/forgeroute-ui-charts.mjs.map +1 -1
- package/fesm2022/forgeroute-ui-maps.mjs +26 -18
- package/fesm2022/forgeroute-ui-maps.mjs.map +1 -1
- package/fesm2022/forgeroute-ui-messaging.mjs +625 -544
- package/fesm2022/forgeroute-ui-messaging.mjs.map +1 -1
- package/fesm2022/forgeroute-ui-offline.mjs +3036 -0
- package/fesm2022/forgeroute-ui-offline.mjs.map +1 -0
- package/fesm2022/forgeroute-ui-pdf.mjs +104 -71
- package/fesm2022/forgeroute-ui-pdf.mjs.map +1 -1
- package/fesm2022/forgeroute-ui-stripe.mjs +291 -156
- package/fesm2022/forgeroute-ui-stripe.mjs.map +1 -1
- package/fesm2022/forgeroute-ui.mjs +13952 -5665
- package/fesm2022/forgeroute-ui.mjs.map +1 -1
- package/package.json +5 -1
- package/types/forgeroute-ui-charts.d.ts +10 -4
- package/types/forgeroute-ui-maps.d.ts +5 -2
- package/types/forgeroute-ui-messaging.d.ts +57 -36
- package/types/forgeroute-ui-offline.d.ts +887 -0
- package/types/forgeroute-ui-pdf.d.ts +24 -12
- package/types/forgeroute-ui-stripe.d.ts +72 -54
- package/types/forgeroute-ui.d.ts +3257 -334
|
@@ -0,0 +1,3036 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, Injectable, InjectionToken, computed, effect, inject, input, makeEnvironmentProviders, output, provideAppInitializer, signal, untracked, viewChild } from "@angular/core";
|
|
3
|
+
import { DOCUMENT } from "@angular/common";
|
|
4
|
+
import { Subject, defer, firstValueFrom, from, of, throwError, timer } from "rxjs";
|
|
5
|
+
import { ConfirmService, SidePanelComponent, ToastService, UiLabelsService, uiText } from "@forgeroute/ui";
|
|
6
|
+
import { HttpClient, HttpContextToken, HttpErrorResponse, HttpHeaders, HttpParams, HttpResponse } from "@angular/common/http";
|
|
7
|
+
import { catchError, finalize, retry, switchMap, tap, timeout } from "rxjs/operators";
|
|
8
|
+
const OFFLINE_SUPPORT_CONFIG = new InjectionToken("OFFLINE_SUPPORT_CONFIG");
|
|
9
|
+
const OFFLINE_REPLAY_HEADER = "X-Offline-Replay";
|
|
10
|
+
const IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
11
|
+
const FROM_CACHE_HEADER = "X-From-Offline-Cache";
|
|
12
|
+
function usableId(v) {
|
|
13
|
+
if (typeof v === "string") return v.trim() === "" ? void 0 : v;
|
|
14
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
15
|
+
}
|
|
16
|
+
function defaultCreatedId(body) {
|
|
17
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
18
|
+
const b = body;
|
|
19
|
+
const direct = usableId(b.id);
|
|
20
|
+
if (direct !== void 0) return direct;
|
|
21
|
+
const data = b.data;
|
|
22
|
+
return data !== null && typeof data === "object" ? usableId(data.id) : void 0;
|
|
23
|
+
}
|
|
24
|
+
function safeCreatedId(fn) {
|
|
25
|
+
return (body, row) => {
|
|
26
|
+
try {
|
|
27
|
+
return usableId(fn(body, row));
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const DEFAULT_LIST_TTL_MS = 6e5;
|
|
34
|
+
const DEFAULT_DETAIL_TTL_MS = 18e5;
|
|
35
|
+
function positive(value, fallback) {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
function resolveConfig(cfg) {
|
|
39
|
+
return {
|
|
40
|
+
apiPrefix: cfg.apiPrefix || "/api/",
|
|
41
|
+
skip: cfg.skip ?? [],
|
|
42
|
+
getUserId: cfg.getUserId,
|
|
43
|
+
allowQueue: cfg.allowQueue ?? null,
|
|
44
|
+
blockOffline: cfg.blockOffline ?? [],
|
|
45
|
+
neverCache: cfg.neverCache ?? [],
|
|
46
|
+
replayHeaders: cfg.replayHeaders ?? [],
|
|
47
|
+
probe: cfg.probe && cfg.probe.url ? {
|
|
48
|
+
url: cfg.probe.url,
|
|
49
|
+
intervalMs: positive(cfg.probe.intervalMs, 15e3)
|
|
50
|
+
} : null,
|
|
51
|
+
timeouts: {
|
|
52
|
+
read: positive(cfg.timeouts?.read, 3e4),
|
|
53
|
+
write: positive(cfg.timeouts?.write, 6e4),
|
|
54
|
+
long: positive(cfg.timeouts?.long, 18e4)
|
|
55
|
+
},
|
|
56
|
+
longRequests: cfg.longRequests ?? [],
|
|
57
|
+
slow: {
|
|
58
|
+
slowMs: positive(cfg.slow?.slowMs, 5e3),
|
|
59
|
+
slowCount: positive(cfg.slow?.slowCount, 2),
|
|
60
|
+
windowMs: positive(cfg.slow?.windowMs, 6e4),
|
|
61
|
+
stuckMs: positive(cfg.slow?.stuckMs, 8e3),
|
|
62
|
+
quietMs: positive(cfg.slow?.quietMs, 3e4)
|
|
63
|
+
},
|
|
64
|
+
dbName: cfg.dbName || "forgeroute-offline",
|
|
65
|
+
cacheTtl: cfg.cacheTtl ?? null,
|
|
66
|
+
maxQueue: positive(cfg.maxQueue, 500),
|
|
67
|
+
maxAttempts: positive(cfg.maxAttempts, 5),
|
|
68
|
+
createdId: cfg.createdId ? safeCreatedId(cfg.createdId) : defaultCreatedId
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function pathAndQuery(url) {
|
|
72
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]+(.*)$/i.exec(url);
|
|
73
|
+
const noHash = (m ? m[1] : url).split("#")[0];
|
|
74
|
+
return noHash === "" ? "/" : noHash;
|
|
75
|
+
}
|
|
76
|
+
function pathOnly(url) {
|
|
77
|
+
return pathAndQuery(url).split("?")[0];
|
|
78
|
+
}
|
|
79
|
+
function matchesUrl(url, matcher) {
|
|
80
|
+
const target = pathAndQuery(url);
|
|
81
|
+
if (typeof matcher === "string") return matcher !== "" && target.includes(matcher);
|
|
82
|
+
matcher.lastIndex = 0;
|
|
83
|
+
const hit = matcher.test(target);
|
|
84
|
+
matcher.lastIndex = 0;
|
|
85
|
+
return hit;
|
|
86
|
+
}
|
|
87
|
+
function matchesAny(url, matchers) {
|
|
88
|
+
return matchers.some((m) => matchesUrl(url, m));
|
|
89
|
+
}
|
|
90
|
+
function isMutationMethod(method) {
|
|
91
|
+
const m = method.toUpperCase();
|
|
92
|
+
return m === "POST" || m === "PUT" || m === "PATCH" || m === "DELETE";
|
|
93
|
+
}
|
|
94
|
+
const ID_SEGMENT = /^(?:\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
95
|
+
function deriveEntity(url) {
|
|
96
|
+
const segments = pathOnly(url).split("/").filter(Boolean);
|
|
97
|
+
for (let i = 1; i < segments.length; i++) if (ID_SEGMENT.test(segments[i])) return {
|
|
98
|
+
entityType: segments[i - 1],
|
|
99
|
+
entityId: segments[i]
|
|
100
|
+
};
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
function invalidationPrefix(url) {
|
|
104
|
+
const segments = pathOnly(url).split("/").filter(Boolean);
|
|
105
|
+
while (segments.length > 1 && ID_SEGMENT.test(segments[segments.length - 1])) segments.pop();
|
|
106
|
+
return "/" + segments.join("/");
|
|
107
|
+
}
|
|
108
|
+
const resolvedCache = /* @__PURE__ */ new WeakMap();
|
|
109
|
+
function resolveConfigCached(cfg) {
|
|
110
|
+
let resolved = resolvedCache.get(cfg);
|
|
111
|
+
if (!resolved) {
|
|
112
|
+
resolved = resolveConfig(cfg);
|
|
113
|
+
resolvedCache.set(cfg, resolved);
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
function isInScope(url, cfg) {
|
|
118
|
+
return url.includes(cfg.apiPrefix) && !matchesAny(url, cfg.skip);
|
|
119
|
+
}
|
|
120
|
+
function lastSegmentIsId(url) {
|
|
121
|
+
const segments = pathOnly(url).split("/").filter(Boolean);
|
|
122
|
+
return segments.length > 0 && ID_SEGMENT.test(segments[segments.length - 1]);
|
|
123
|
+
}
|
|
124
|
+
function generateUuid() {
|
|
125
|
+
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
126
|
+
if (c && typeof c.randomUUID === "function") try {
|
|
127
|
+
return c.randomUUID();
|
|
128
|
+
} catch {}
|
|
129
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
130
|
+
if (c && typeof c.getRandomValues === "function") c.getRandomValues(bytes);
|
|
131
|
+
else for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
132
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
133
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
134
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
135
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
136
|
+
}
|
|
137
|
+
const ONLINE_DEBOUNCE_MS = 300;
|
|
138
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
139
|
+
var ConnectionStateService = class ConnectionStateService {
|
|
140
|
+
constructor() {
|
|
141
|
+
this.cfg = resolveConfig(inject(OFFLINE_SUPPORT_CONFIG, { optional: true }) ?? { getUserId: () => null });
|
|
142
|
+
this.win = inject(DOCUMENT).defaultView;
|
|
143
|
+
this.browserOnline = signal(this.readNavigatorOnline(), ...ngDevMode ? [{ debugName: "browserOnline" }] : /* istanbul ignore next */ []);
|
|
144
|
+
this.unreachable = signal(false, ...ngDevMode ? [{ debugName: "unreachable" }] : /* istanbul ignore next */ []);
|
|
145
|
+
this.slowFlag = signal(false, ...ngDevMode ? [{ debugName: "slowFlag" }] : /* istanbul ignore next */ []);
|
|
146
|
+
this._slowRequestActive = signal(false, ...ngDevMode ? [{ debugName: "_slowRequestActive" }] : /* istanbul ignore next */ []);
|
|
147
|
+
this._inFlightCount = signal(0, ...ngDevMode ? [{ debugName: "_inFlightCount" }] : /* istanbul ignore next */ []);
|
|
148
|
+
this._state = signal(this.browserOnline() ? "online" : "offline", ...ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []);
|
|
149
|
+
this._lastChangeAt = signal(Date.now(), ...ngDevMode ? [{ debugName: "_lastChangeAt" }] : /* istanbul ignore next */ []);
|
|
150
|
+
this.state = this._state.asReadonly();
|
|
151
|
+
this.isOnline = computed(() => this._state() === "online" || this._state() === "slow", ...ngDevMode ? [{ debugName: "isOnline" }] : /* istanbul ignore next */ []);
|
|
152
|
+
this.isOffline = computed(() => this._state() === "offline" || this._state() === "unreachable", ...ngDevMode ? [{ debugName: "isOffline" }] : /* istanbul ignore next */ []);
|
|
153
|
+
this.slowRequestActive = this._slowRequestActive.asReadonly();
|
|
154
|
+
this.inFlightCount = this._inFlightCount.asReadonly();
|
|
155
|
+
this.lastChangeAt = this._lastChangeAt.asReadonly();
|
|
156
|
+
this.slowRequestHint = computed(() => this._slowRequestActive() || this._state() === "slow", ...ngDevMode ? [{ debugName: "slowRequestHint" }] : /* istanbul ignore next */ []);
|
|
157
|
+
this.recovered$ = new Subject();
|
|
158
|
+
this.failures = [];
|
|
159
|
+
this.slowTimes = [];
|
|
160
|
+
this.quietTimer = null;
|
|
161
|
+
this.onlineTimer = null;
|
|
162
|
+
this.probeTimer = null;
|
|
163
|
+
this.optimisticTimer = null;
|
|
164
|
+
this.probeInFlight = null;
|
|
165
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
166
|
+
this.nextRequestId = 0;
|
|
167
|
+
this.onWindowOnline = () => this.scheduleOnline();
|
|
168
|
+
this.onWindowOffline = () => {
|
|
169
|
+
this.cancelOnlineTimer();
|
|
170
|
+
this.setBrowserOnline(false);
|
|
171
|
+
};
|
|
172
|
+
this.onFocus = () => this.syncFromNavigator();
|
|
173
|
+
const win = this.win;
|
|
174
|
+
if (win) {
|
|
175
|
+
win.addEventListener("online", this.onWindowOnline);
|
|
176
|
+
win.addEventListener("offline", this.onWindowOffline);
|
|
177
|
+
win.addEventListener("focus", this.onFocus);
|
|
178
|
+
win.document?.addEventListener("visibilitychange", this.onFocus);
|
|
179
|
+
}
|
|
180
|
+
inject(DestroyRef).onDestroy(() => this.teardown());
|
|
181
|
+
}
|
|
182
|
+
recordRequest(info) {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
if (info.timedOut || info.status === 0) {
|
|
185
|
+
this.handleFailure(now);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.failures = [];
|
|
189
|
+
if (this.unreachable()) this.clearUnreachable("online");
|
|
190
|
+
if (info.long) return;
|
|
191
|
+
if (info.durationMs >= this.cfg.slow.slowMs) {
|
|
192
|
+
this.slowTimes = this.slowTimes.filter((t) => now - t <= this.cfg.slow.windowMs);
|
|
193
|
+
this.slowTimes.push(now);
|
|
194
|
+
if (this.slowTimes.length >= this.cfg.slow.slowCount || this.slowFlag()) this.markSlow();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
beginRequest(options = {}) {
|
|
198
|
+
const id = ++this.nextRequestId;
|
|
199
|
+
const entry = {
|
|
200
|
+
start: Date.now(),
|
|
201
|
+
stuck: false,
|
|
202
|
+
timer: null
|
|
203
|
+
};
|
|
204
|
+
this.inFlight.set(id, entry);
|
|
205
|
+
this._inFlightCount.set(this.inFlight.size);
|
|
206
|
+
if (!options.long) entry.timer = setTimeout(() => {
|
|
207
|
+
entry.timer = null;
|
|
208
|
+
if (!this.inFlight.has(id)) return;
|
|
209
|
+
entry.stuck = true;
|
|
210
|
+
this._slowRequestActive.set(true);
|
|
211
|
+
this.markSlow();
|
|
212
|
+
}, this.cfg.slow.stuckMs);
|
|
213
|
+
return () => {
|
|
214
|
+
const current = this.inFlight.get(id);
|
|
215
|
+
if (!current) return;
|
|
216
|
+
if (current.timer) clearTimeout(current.timer);
|
|
217
|
+
this.inFlight.delete(id);
|
|
218
|
+
this._inFlightCount.set(this.inFlight.size);
|
|
219
|
+
this._slowRequestActive.set([...this.inFlight.values()].some((e) => e.stuck));
|
|
220
|
+
this.recompute();
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
probeNow() {
|
|
224
|
+
const probe = this.cfg.probe;
|
|
225
|
+
if (!probe) return Promise.resolve(this.browserOnline());
|
|
226
|
+
if (this.probeInFlight) return this.probeInFlight;
|
|
227
|
+
const run = this.runProbe(probe.url).then((ok) => {
|
|
228
|
+
if (ok) this.onProbeSuccess();
|
|
229
|
+
return ok;
|
|
230
|
+
}).finally(() => {
|
|
231
|
+
this.probeInFlight = null;
|
|
232
|
+
});
|
|
233
|
+
this.probeInFlight = run;
|
|
234
|
+
return run;
|
|
235
|
+
}
|
|
236
|
+
async runProbe(url) {
|
|
237
|
+
const win = this.win;
|
|
238
|
+
if (!win || typeof win.fetch !== "function") return false;
|
|
239
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
240
|
+
const timer = controller ? setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS) : null;
|
|
241
|
+
try {
|
|
242
|
+
await win.fetch(url, {
|
|
243
|
+
method: "GET",
|
|
244
|
+
cache: "no-store",
|
|
245
|
+
credentials: "same-origin",
|
|
246
|
+
signal: controller?.signal
|
|
247
|
+
});
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
} finally {
|
|
252
|
+
if (timer) clearTimeout(timer);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
onProbeSuccess() {
|
|
256
|
+
this.failures = [];
|
|
257
|
+
if (this.unreachable()) this.clearUnreachable("probe");
|
|
258
|
+
}
|
|
259
|
+
handleFailure(now) {
|
|
260
|
+
if (!this.browserOnline()) return;
|
|
261
|
+
this.failures = this.failures.filter((t) => now - t <= this.cfg.slow.windowMs);
|
|
262
|
+
this.failures.push(now);
|
|
263
|
+
if (this.failures.length >= 2) {
|
|
264
|
+
this.setUnreachable();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (this.cfg.probe) this.probeNow().then((ok) => {
|
|
268
|
+
if (!ok && this.browserOnline() && this.failures.length > 0) this.setUnreachable();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
setUnreachable() {
|
|
272
|
+
if (this.unreachable()) return;
|
|
273
|
+
this.unreachable.set(true);
|
|
274
|
+
this.recompute();
|
|
275
|
+
if (this.cfg.probe) this.probeTimer = setInterval(() => {
|
|
276
|
+
this.probeNow();
|
|
277
|
+
}, this.cfg.probe.intervalMs);
|
|
278
|
+
else this.optimisticTimer = setTimeout(() => {
|
|
279
|
+
this.optimisticTimer = null;
|
|
280
|
+
if (this.unreachable()) this.clearUnreachable("online");
|
|
281
|
+
}, 15e3);
|
|
282
|
+
}
|
|
283
|
+
clearUnreachable(trigger) {
|
|
284
|
+
if (!this.unreachable()) return;
|
|
285
|
+
this.unreachable.set(false);
|
|
286
|
+
this.stopRecoveryTimers();
|
|
287
|
+
this.recompute();
|
|
288
|
+
if (this.browserOnline()) this.recovered$.next(trigger);
|
|
289
|
+
}
|
|
290
|
+
stopRecoveryTimers() {
|
|
291
|
+
if (this.probeTimer) {
|
|
292
|
+
clearInterval(this.probeTimer);
|
|
293
|
+
this.probeTimer = null;
|
|
294
|
+
}
|
|
295
|
+
if (this.optimisticTimer) {
|
|
296
|
+
clearTimeout(this.optimisticTimer);
|
|
297
|
+
this.optimisticTimer = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
markSlow() {
|
|
301
|
+
this.slowFlag.set(true);
|
|
302
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
303
|
+
this.quietTimer = setTimeout(() => {
|
|
304
|
+
this.quietTimer = null;
|
|
305
|
+
this.slowFlag.set(false);
|
|
306
|
+
this.slowTimes = [];
|
|
307
|
+
this.recompute();
|
|
308
|
+
}, this.cfg.slow.quietMs);
|
|
309
|
+
this.recompute();
|
|
310
|
+
}
|
|
311
|
+
scheduleOnline() {
|
|
312
|
+
this.cancelOnlineTimer();
|
|
313
|
+
this.onlineTimer = setTimeout(() => {
|
|
314
|
+
this.onlineTimer = null;
|
|
315
|
+
this.setBrowserOnline(true);
|
|
316
|
+
}, 300);
|
|
317
|
+
}
|
|
318
|
+
cancelOnlineTimer() {
|
|
319
|
+
if (this.onlineTimer) {
|
|
320
|
+
clearTimeout(this.onlineTimer);
|
|
321
|
+
this.onlineTimer = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
setBrowserOnline(value) {
|
|
325
|
+
if (this.browserOnline() === value) return;
|
|
326
|
+
this.browserOnline.set(value);
|
|
327
|
+
if (value) {
|
|
328
|
+
this.failures = [];
|
|
329
|
+
if (this.unreachable()) {
|
|
330
|
+
this.unreachable.set(false);
|
|
331
|
+
this.stopRecoveryTimers();
|
|
332
|
+
}
|
|
333
|
+
this.recompute();
|
|
334
|
+
this.recovered$.next("online");
|
|
335
|
+
} else this.recompute();
|
|
336
|
+
}
|
|
337
|
+
syncFromNavigator() {
|
|
338
|
+
if (!this.readNavigatorOnline()) {
|
|
339
|
+
this.cancelOnlineTimer();
|
|
340
|
+
this.setBrowserOnline(false);
|
|
341
|
+
} else if (!this.browserOnline()) this.scheduleOnline();
|
|
342
|
+
}
|
|
343
|
+
readNavigatorOnline() {
|
|
344
|
+
try {
|
|
345
|
+
return this.win?.navigator?.onLine !== false;
|
|
346
|
+
} catch {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
recompute() {
|
|
351
|
+
const next = !this.browserOnline() ? "offline" : this.unreachable() ? "unreachable" : this.slowFlag() || this._slowRequestActive() ? "slow" : "online";
|
|
352
|
+
if (next === this._state()) return;
|
|
353
|
+
this._state.set(next);
|
|
354
|
+
this._lastChangeAt.set(Date.now());
|
|
355
|
+
}
|
|
356
|
+
teardown() {
|
|
357
|
+
const win = this.win;
|
|
358
|
+
if (win) {
|
|
359
|
+
win.removeEventListener("online", this.onWindowOnline);
|
|
360
|
+
win.removeEventListener("offline", this.onWindowOffline);
|
|
361
|
+
win.removeEventListener("focus", this.onFocus);
|
|
362
|
+
win.document?.removeEventListener("visibilitychange", this.onFocus);
|
|
363
|
+
}
|
|
364
|
+
this.cancelOnlineTimer();
|
|
365
|
+
this.stopRecoveryTimers();
|
|
366
|
+
if (this.quietTimer) {
|
|
367
|
+
clearTimeout(this.quietTimer);
|
|
368
|
+
this.quietTimer = null;
|
|
369
|
+
}
|
|
370
|
+
for (const e of this.inFlight.values()) if (e.timer) clearTimeout(e.timer);
|
|
371
|
+
this.inFlight.clear();
|
|
372
|
+
this.recovered$.complete();
|
|
373
|
+
}
|
|
374
|
+
static {
|
|
375
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
376
|
+
minVersion: "12.0.0",
|
|
377
|
+
version: "22.2.2",
|
|
378
|
+
ngImport: i0,
|
|
379
|
+
type: ConnectionStateService,
|
|
380
|
+
deps: [],
|
|
381
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
static {
|
|
385
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
386
|
+
minVersion: "12.0.0",
|
|
387
|
+
version: "22.2.2",
|
|
388
|
+
ngImport: i0,
|
|
389
|
+
type: ConnectionStateService,
|
|
390
|
+
providedIn: "root"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
395
|
+
minVersion: "12.0.0",
|
|
396
|
+
version: "22.2.2",
|
|
397
|
+
ngImport: i0,
|
|
398
|
+
type: ConnectionStateService,
|
|
399
|
+
decorators: [{
|
|
400
|
+
type: Injectable,
|
|
401
|
+
args: [{ providedIn: "root" }]
|
|
402
|
+
}],
|
|
403
|
+
ctorParameters: () => []
|
|
404
|
+
});
|
|
405
|
+
const DEFAULT_KEY = "*";
|
|
406
|
+
var StaleDataService = class StaleDataService {
|
|
407
|
+
constructor() {
|
|
408
|
+
this._staleAt = signal(null, ...ngDevMode ? [{ debugName: "_staleAt" }] : /* istanbul ignore next */ []);
|
|
409
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
410
|
+
this.staleAt = this._staleAt.asReadonly();
|
|
411
|
+
}
|
|
412
|
+
markStale(cachedAt, key = DEFAULT_KEY) {
|
|
413
|
+
this.entries.set(key, cachedAt);
|
|
414
|
+
this.publish();
|
|
415
|
+
}
|
|
416
|
+
clearKey(key) {
|
|
417
|
+
if (this.entries.delete(key)) this.publish();
|
|
418
|
+
}
|
|
419
|
+
clear() {
|
|
420
|
+
if (this.entries.size === 0) return;
|
|
421
|
+
this.entries.clear();
|
|
422
|
+
this.publish();
|
|
423
|
+
}
|
|
424
|
+
publish() {
|
|
425
|
+
let latest = null;
|
|
426
|
+
for (const v of this.entries.values()) if (latest === null || v > latest) latest = v;
|
|
427
|
+
this._staleAt.set(latest);
|
|
428
|
+
}
|
|
429
|
+
static {
|
|
430
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
431
|
+
minVersion: "12.0.0",
|
|
432
|
+
version: "22.2.2",
|
|
433
|
+
ngImport: i0,
|
|
434
|
+
type: StaleDataService,
|
|
435
|
+
deps: [],
|
|
436
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
static {
|
|
440
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
441
|
+
minVersion: "12.0.0",
|
|
442
|
+
version: "22.2.2",
|
|
443
|
+
ngImport: i0,
|
|
444
|
+
type: StaleDataService,
|
|
445
|
+
providedIn: "root"
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
450
|
+
minVersion: "12.0.0",
|
|
451
|
+
version: "22.2.2",
|
|
452
|
+
ngImport: i0,
|
|
453
|
+
type: StaleDataService,
|
|
454
|
+
decorators: [{
|
|
455
|
+
type: Injectable,
|
|
456
|
+
args: [{ providedIn: "root" }]
|
|
457
|
+
}]
|
|
458
|
+
});
|
|
459
|
+
const IDMAP_TTL_MS = 6048e5;
|
|
460
|
+
const STALE_CLAIM_MS = 6e4;
|
|
461
|
+
const DB_VERSION = 2;
|
|
462
|
+
const OPEN_TIMEOUT_MS = 1e4;
|
|
463
|
+
const MAX_CACHE_ENTRIES = 500;
|
|
464
|
+
const EVICT_COUNT = 50;
|
|
465
|
+
const MUTATIONS = "mutations";
|
|
466
|
+
const RESPONSES = "responses";
|
|
467
|
+
const META = "meta";
|
|
468
|
+
const IDMAP = "idmap";
|
|
469
|
+
const OFFLINE_IDB_FACTORY = new InjectionToken("OFFLINE_IDB_FACTORY", {
|
|
470
|
+
providedIn: "root",
|
|
471
|
+
factory: () => {
|
|
472
|
+
try {
|
|
473
|
+
return typeof indexedDB !== "undefined" ? indexedDB : null;
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
var StorageUnavailableError = class StorageUnavailableError extends Error {
|
|
480
|
+
constructor(message = uiText("connection.storageNotAvailable")) {
|
|
481
|
+
super(message);
|
|
482
|
+
this.name = "StorageUnavailableError";
|
|
483
|
+
Object.setPrototypeOf(this, StorageUnavailableError.prototype);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var QueueFullError = class QueueFullError extends Error {
|
|
487
|
+
constructor(limit) {
|
|
488
|
+
super(uiText("connection.queueFull", { limit }));
|
|
489
|
+
this.limit = limit;
|
|
490
|
+
this.name = "QueueFullError";
|
|
491
|
+
Object.setPrototypeOf(this, QueueFullError.prototype);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
function isQuotaError(e) {
|
|
495
|
+
const x = e;
|
|
496
|
+
return !!x && (x.name === "QuotaExceededError" || x.name === "NS_ERROR_DOM_QUOTA_REACHED" || x.code === 22 || x.code === 1014);
|
|
497
|
+
}
|
|
498
|
+
function isInvalidState(e) {
|
|
499
|
+
const x = e;
|
|
500
|
+
return !!x && (x.name === "InvalidStateError" || x.name === "TransactionInactiveError");
|
|
501
|
+
}
|
|
502
|
+
const RETRYABLE_FAILURES = [
|
|
503
|
+
"server",
|
|
504
|
+
"network",
|
|
505
|
+
"timeout"
|
|
506
|
+
];
|
|
507
|
+
function isClaimable(row, now) {
|
|
508
|
+
if (row.status === "pending") return true;
|
|
509
|
+
if (row.status === "failed") return !!row.lastError && RETRYABLE_FAILURES.includes(row.lastError.kind);
|
|
510
|
+
if (row.status === "syncing") return now - (row.claimAt ?? 0) > STALE_CLAIM_MS;
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
var OfflineStore = class OfflineStore {
|
|
514
|
+
constructor() {
|
|
515
|
+
this.cfg = resolveConfig(inject(OFFLINE_SUPPORT_CONFIG, { optional: true }) ?? { getUserId: () => null });
|
|
516
|
+
this.factory = inject(OFFLINE_IDB_FACTORY);
|
|
517
|
+
this.mutated$ = new Subject();
|
|
518
|
+
this.db = null;
|
|
519
|
+
this.opening = null;
|
|
520
|
+
this.initPromise = null;
|
|
521
|
+
this.ready = false;
|
|
522
|
+
}
|
|
523
|
+
available() {
|
|
524
|
+
return this.ready;
|
|
525
|
+
}
|
|
526
|
+
init() {
|
|
527
|
+
if (!this.initPromise) this.initPromise = this.doInit().then(() => {
|
|
528
|
+
if (!this.ready) this.initPromise = null;
|
|
529
|
+
});
|
|
530
|
+
return this.initPromise;
|
|
531
|
+
}
|
|
532
|
+
async doInit() {
|
|
533
|
+
if (!this.factory) {
|
|
534
|
+
this.ready = false;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
await this.getDb();
|
|
539
|
+
this.ready = true;
|
|
540
|
+
} catch {
|
|
541
|
+
this.ready = false;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await this.purgeExpired();
|
|
546
|
+
} catch {}
|
|
547
|
+
try {
|
|
548
|
+
await this.purgeExpiredIdMap();
|
|
549
|
+
} catch {}
|
|
550
|
+
}
|
|
551
|
+
async addMutation(row) {
|
|
552
|
+
await this.init();
|
|
553
|
+
if (!this.ready) throw new StorageUnavailableError();
|
|
554
|
+
const limit = this.cfg.maxQueue;
|
|
555
|
+
const saved = await this.runTx([MUTATIONS, META], "readwrite", (s, resolveWith, abortWith) => {
|
|
556
|
+
const existing = s[MUTATIONS].index("userId").getAll(row.userId);
|
|
557
|
+
existing.onsuccess = () => {
|
|
558
|
+
const rows = existing.result;
|
|
559
|
+
const dup = rows.find((r) => r.key === row.key);
|
|
560
|
+
if (dup) {
|
|
561
|
+
resolveWith(dup);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (rows.length >= limit) {
|
|
565
|
+
abortWith(new QueueFullError(limit));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const seqReq = s[META].get("seq");
|
|
569
|
+
seqReq.onsuccess = () => {
|
|
570
|
+
const seq = (seqReq.result?.value ?? 0) + 1;
|
|
571
|
+
const full = {
|
|
572
|
+
...row,
|
|
573
|
+
seq
|
|
574
|
+
};
|
|
575
|
+
s[META].put({
|
|
576
|
+
id: "seq",
|
|
577
|
+
value: seq
|
|
578
|
+
});
|
|
579
|
+
s[MUTATIONS].add(full);
|
|
580
|
+
resolveWith(full);
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
this.mutated$.next();
|
|
585
|
+
return saved;
|
|
586
|
+
}
|
|
587
|
+
async claim(id, owner, now = Date.now()) {
|
|
588
|
+
const claimed = await this.runTx([MUTATIONS], "readwrite", (s, resolveWith) => {
|
|
589
|
+
resolveWith(null);
|
|
590
|
+
const req = s[MUTATIONS].get(id);
|
|
591
|
+
req.onsuccess = () => {
|
|
592
|
+
const row = req.result;
|
|
593
|
+
if (!row || !isClaimable(row, now)) return;
|
|
594
|
+
const next = {
|
|
595
|
+
...row,
|
|
596
|
+
status: "syncing",
|
|
597
|
+
claimOwner: owner,
|
|
598
|
+
claimAt: now
|
|
599
|
+
};
|
|
600
|
+
s[MUTATIONS].put(next);
|
|
601
|
+
resolveWith(next);
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
if (claimed) this.mutated$.next();
|
|
605
|
+
return claimed;
|
|
606
|
+
}
|
|
607
|
+
async update(id, patch, opts = {}) {
|
|
608
|
+
const updated = await this.runTx([MUTATIONS], "readwrite", (s, resolveWith) => {
|
|
609
|
+
resolveWith(null);
|
|
610
|
+
const req = s[MUTATIONS].get(id);
|
|
611
|
+
req.onsuccess = () => {
|
|
612
|
+
const row = req.result;
|
|
613
|
+
if (!row) return;
|
|
614
|
+
if (opts.ifOwner !== void 0 && row.claimOwner !== opts.ifOwner) return;
|
|
615
|
+
const merged = {
|
|
616
|
+
...row,
|
|
617
|
+
...patch,
|
|
618
|
+
id: row.id,
|
|
619
|
+
userId: row.userId
|
|
620
|
+
};
|
|
621
|
+
const loose = merged;
|
|
622
|
+
for (const k of Object.keys(loose)) if (loose[k] === void 0) delete loose[k];
|
|
623
|
+
s[MUTATIONS].put(merged);
|
|
624
|
+
resolveWith(merged);
|
|
625
|
+
};
|
|
626
|
+
});
|
|
627
|
+
if (updated) this.mutated$.next();
|
|
628
|
+
return updated;
|
|
629
|
+
}
|
|
630
|
+
async resetForRetry(id, userId, now = Date.now()) {
|
|
631
|
+
if (!await this.ensureReady()) return null;
|
|
632
|
+
const reset = await this.runTx([MUTATIONS], "readwrite", (s, resolveWith) => {
|
|
633
|
+
resolveWith(null);
|
|
634
|
+
const req = s[MUTATIONS].get(id);
|
|
635
|
+
req.onsuccess = () => {
|
|
636
|
+
const row = req.result;
|
|
637
|
+
if (!row || row.userId !== userId) return;
|
|
638
|
+
if (row.status === "syncing" && now - (row.claimAt ?? 0) <= 6e4) return;
|
|
639
|
+
const next = {
|
|
640
|
+
...row,
|
|
641
|
+
status: "pending",
|
|
642
|
+
attempts: 0
|
|
643
|
+
};
|
|
644
|
+
delete next.nextAttemptAt;
|
|
645
|
+
delete next.lastError;
|
|
646
|
+
delete next.claimOwner;
|
|
647
|
+
delete next.claimAt;
|
|
648
|
+
s[MUTATIONS].put(next);
|
|
649
|
+
resolveWith(next);
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
if (reset) this.mutated$.next();
|
|
653
|
+
return reset;
|
|
654
|
+
}
|
|
655
|
+
async get(id) {
|
|
656
|
+
if (!await this.ensureReady()) return null;
|
|
657
|
+
return this.runTx([MUTATIONS], "readonly", (s, resolveWith) => {
|
|
658
|
+
resolveWith(null);
|
|
659
|
+
const req = s[MUTATIONS].get(id);
|
|
660
|
+
req.onsuccess = () => resolveWith(req.result ?? null);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async delete(id, opts = {}) {
|
|
664
|
+
const deleted = await this.runTx([MUTATIONS], "readwrite", (s, resolveWith) => {
|
|
665
|
+
resolveWith(false);
|
|
666
|
+
const req = s[MUTATIONS].get(id);
|
|
667
|
+
req.onsuccess = () => {
|
|
668
|
+
const row = req.result;
|
|
669
|
+
if (!row) return;
|
|
670
|
+
if (opts.ifOwner !== void 0 && row.claimOwner !== opts.ifOwner) return;
|
|
671
|
+
s[MUTATIONS].delete(id);
|
|
672
|
+
resolveWith(true);
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
if (deleted) this.mutated$.next();
|
|
676
|
+
return deleted;
|
|
677
|
+
}
|
|
678
|
+
async list(userId) {
|
|
679
|
+
if (!await this.ensureReady()) return [];
|
|
680
|
+
return (await this.runTx([MUTATIONS], "readonly", (s, resolveWith) => {
|
|
681
|
+
resolveWith([]);
|
|
682
|
+
const req = s[MUTATIONS].index("userId").getAll(userId);
|
|
683
|
+
req.onsuccess = () => resolveWith(req.result);
|
|
684
|
+
})).filter((r) => r.userId === userId).sort((a, b) => a.seq - b.seq || a.createdAt - b.createdAt);
|
|
685
|
+
}
|
|
686
|
+
async count(userId) {
|
|
687
|
+
return (await this.list(userId)).length;
|
|
688
|
+
}
|
|
689
|
+
async countsByStatus(userId) {
|
|
690
|
+
const counts = {
|
|
691
|
+
pending: 0,
|
|
692
|
+
syncing: 0,
|
|
693
|
+
failed: 0,
|
|
694
|
+
conflict: 0
|
|
695
|
+
};
|
|
696
|
+
for (const r of await this.list(userId)) counts[r.status]++;
|
|
697
|
+
return counts;
|
|
698
|
+
}
|
|
699
|
+
async deleteMutations(userId) {
|
|
700
|
+
if (!await this.ensureReady()) return 0;
|
|
701
|
+
const removed = await this.runTx([MUTATIONS], "readwrite", (s, resolveWith) => {
|
|
702
|
+
resolveWith(0);
|
|
703
|
+
const req = s[MUTATIONS].index("userId").getAll(userId);
|
|
704
|
+
req.onsuccess = () => {
|
|
705
|
+
const rows = req.result.filter((r) => r.userId === userId);
|
|
706
|
+
for (const r of rows) s[MUTATIONS].delete(r.id);
|
|
707
|
+
resolveWith(rows.length);
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
if (removed > 0) this.mutated$.next();
|
|
711
|
+
return removed;
|
|
712
|
+
}
|
|
713
|
+
async clearUser(userId) {
|
|
714
|
+
if (!await this.ensureReady()) return;
|
|
715
|
+
await this.deleteMutations(userId);
|
|
716
|
+
await this.clearCache(userId);
|
|
717
|
+
await this.deleteIdMap(userId);
|
|
718
|
+
}
|
|
719
|
+
async clearAll() {
|
|
720
|
+
if (!await this.ensureReady()) return;
|
|
721
|
+
await this.runTx([
|
|
722
|
+
MUTATIONS,
|
|
723
|
+
RESPONSES,
|
|
724
|
+
IDMAP
|
|
725
|
+
], "readwrite", (s) => {
|
|
726
|
+
s[MUTATIONS].clear();
|
|
727
|
+
s[RESPONSES].clear();
|
|
728
|
+
s[IDMAP].clear();
|
|
729
|
+
});
|
|
730
|
+
this.mutated$.next();
|
|
731
|
+
}
|
|
732
|
+
async putIdMapping(userId, tempId, realId, now = Date.now()) {
|
|
733
|
+
await this.init();
|
|
734
|
+
if (!this.ready) throw new StorageUnavailableError();
|
|
735
|
+
const entry = {
|
|
736
|
+
key: `${userId}:${tempId}`,
|
|
737
|
+
userId,
|
|
738
|
+
tempId,
|
|
739
|
+
realId: String(realId),
|
|
740
|
+
createdAt: now,
|
|
741
|
+
expiresAt: now + IDMAP_TTL_MS,
|
|
742
|
+
...typeof realId === "number" ? { numeric: true } : {}
|
|
743
|
+
};
|
|
744
|
+
await this.runTx([IDMAP], "readwrite", (s) => {
|
|
745
|
+
s[IDMAP].put(entry);
|
|
746
|
+
});
|
|
747
|
+
return entry;
|
|
748
|
+
}
|
|
749
|
+
async getIdMapping(userId, tempId, now = Date.now()) {
|
|
750
|
+
if (!await this.ensureReady()) return null;
|
|
751
|
+
const entry = await this.runTx([IDMAP], "readonly", (s, resolveWith) => {
|
|
752
|
+
resolveWith(null);
|
|
753
|
+
const req = s[IDMAP].get(`${userId}:${tempId}`);
|
|
754
|
+
req.onsuccess = () => resolveWith(req.result ?? null);
|
|
755
|
+
});
|
|
756
|
+
return entry && entry.userId === userId && entry.expiresAt > now ? entry : null;
|
|
757
|
+
}
|
|
758
|
+
async listIdMap(userId, now = Date.now()) {
|
|
759
|
+
if (!await this.ensureReady()) return [];
|
|
760
|
+
return (await this.runTx([IDMAP], "readonly", (s, resolveWith) => {
|
|
761
|
+
resolveWith([]);
|
|
762
|
+
const req = s[IDMAP].index("userId").getAll(userId);
|
|
763
|
+
req.onsuccess = () => resolveWith(req.result);
|
|
764
|
+
})).filter((e) => e.userId === userId && e.expiresAt > now);
|
|
765
|
+
}
|
|
766
|
+
async deleteIdMap(userId) {
|
|
767
|
+
if (!await this.ensureReady()) return 0;
|
|
768
|
+
return this.runTx([IDMAP], "readwrite", (s, resolveWith) => {
|
|
769
|
+
resolveWith(0);
|
|
770
|
+
const req = s[IDMAP].index("userId").getAll(userId);
|
|
771
|
+
req.onsuccess = () => {
|
|
772
|
+
const mine = req.result.filter((e) => e.userId === userId);
|
|
773
|
+
for (const e of mine) s[IDMAP].delete(e.key);
|
|
774
|
+
resolveWith(mine.length);
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async purgeExpiredIdMap(now = Date.now()) {
|
|
779
|
+
return this.runTx([IDMAP], "readwrite", (s, resolveWith) => {
|
|
780
|
+
resolveWith(0);
|
|
781
|
+
const req = s[IDMAP].getAll();
|
|
782
|
+
req.onsuccess = () => {
|
|
783
|
+
const old = req.result.filter((e) => e.expiresAt <= now);
|
|
784
|
+
for (const e of old) s[IDMAP].delete(e.key);
|
|
785
|
+
resolveWith(old.length);
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async cacheResponse(key, userId, data, ttlMs, now = Date.now()) {
|
|
790
|
+
if (!await this.ensureReady()) return false;
|
|
791
|
+
const entry = {
|
|
792
|
+
key: `${userId}::${key}`,
|
|
793
|
+
urlKey: key,
|
|
794
|
+
userId,
|
|
795
|
+
data,
|
|
796
|
+
cachedAt: now,
|
|
797
|
+
expiresAt: now + ttlMs
|
|
798
|
+
};
|
|
799
|
+
const put = () => this.runTx([RESPONSES], "readwrite", (s) => {
|
|
800
|
+
s[RESPONSES].put(entry);
|
|
801
|
+
});
|
|
802
|
+
try {
|
|
803
|
+
await put();
|
|
804
|
+
} catch (e) {
|
|
805
|
+
if (!isQuotaError(e)) return false;
|
|
806
|
+
try {
|
|
807
|
+
await this.evictOldest(EVICT_COUNT, now);
|
|
808
|
+
await put();
|
|
809
|
+
} catch {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
this.enforceCacheLimit().catch(() => void 0);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
async getCached(key, userId, now = Date.now()) {
|
|
817
|
+
if (!await this.ensureReady()) return null;
|
|
818
|
+
const composite = `${userId}::${key}`;
|
|
819
|
+
const entry = await this.runTx([RESPONSES], "readonly", (s, resolveWith) => {
|
|
820
|
+
resolveWith(null);
|
|
821
|
+
const req = s[RESPONSES].get(composite);
|
|
822
|
+
req.onsuccess = () => resolveWith(req.result ?? null);
|
|
823
|
+
}).catch(() => null);
|
|
824
|
+
if (!entry || entry.userId !== userId) return null;
|
|
825
|
+
if (entry.expiresAt <= now) {
|
|
826
|
+
this.runTx([RESPONSES], "readwrite", (s) => {
|
|
827
|
+
s[RESPONSES].delete(composite);
|
|
828
|
+
}).catch(() => void 0);
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
return entry;
|
|
832
|
+
}
|
|
833
|
+
async invalidatePrefix(prefix, userId) {
|
|
834
|
+
if (!await this.ensureReady()) return 0;
|
|
835
|
+
return this.runTx([RESPONSES], "readwrite", (s, resolveWith) => {
|
|
836
|
+
resolveWith(0);
|
|
837
|
+
const req = s[RESPONSES].index("userId").getAll(userId);
|
|
838
|
+
req.onsuccess = () => {
|
|
839
|
+
const hits = req.result.filter((e) => e.userId === userId && e.urlKey.startsWith(prefix));
|
|
840
|
+
for (const e of hits) s[RESPONSES].delete(e.key);
|
|
841
|
+
resolveWith(hits.length);
|
|
842
|
+
};
|
|
843
|
+
}).catch(() => 0);
|
|
844
|
+
}
|
|
845
|
+
async clearCache(userId) {
|
|
846
|
+
if (!await this.ensureReady()) return;
|
|
847
|
+
await this.runTx([RESPONSES], "readwrite", (s) => {
|
|
848
|
+
if (userId === void 0) {
|
|
849
|
+
s[RESPONSES].clear();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const req = s[RESPONSES].index("userId").getAll(userId);
|
|
853
|
+
req.onsuccess = () => {
|
|
854
|
+
for (const e of req.result) if (e.userId === userId) s[RESPONSES].delete(e.key);
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async purgeExpired(now = Date.now()) {
|
|
859
|
+
return this.runTx([RESPONSES], "readwrite", (s, resolveWith) => {
|
|
860
|
+
resolveWith(0);
|
|
861
|
+
const req = s[RESPONSES].getAll();
|
|
862
|
+
req.onsuccess = () => {
|
|
863
|
+
const old = req.result.filter((e) => e.expiresAt <= now);
|
|
864
|
+
for (const e of old) s[RESPONSES].delete(e.key);
|
|
865
|
+
resolveWith(old.length);
|
|
866
|
+
};
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
async evictOldest(count, now) {
|
|
870
|
+
await this.runTx([RESPONSES], "readwrite", (s) => {
|
|
871
|
+
const req = s[RESPONSES].getAll();
|
|
872
|
+
req.onsuccess = () => {
|
|
873
|
+
const sorted = [...req.result].sort((a, b) => (a.expiresAt <= now ? 0 : 1) - (b.expiresAt <= now ? 0 : 1) || a.cachedAt - b.cachedAt);
|
|
874
|
+
for (const e of sorted.slice(0, count)) s[RESPONSES].delete(e.key);
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
async enforceCacheLimit() {
|
|
879
|
+
if (await this.runTx([RESPONSES], "readonly", (s, resolveWith) => {
|
|
880
|
+
resolveWith(0);
|
|
881
|
+
const req = s[RESPONSES].count();
|
|
882
|
+
req.onsuccess = () => resolveWith(req.result);
|
|
883
|
+
}) > MAX_CACHE_ENTRIES) await this.evictOldest(EVICT_COUNT, Date.now());
|
|
884
|
+
}
|
|
885
|
+
async ensureReady() {
|
|
886
|
+
await this.init();
|
|
887
|
+
return this.ready;
|
|
888
|
+
}
|
|
889
|
+
getDb() {
|
|
890
|
+
if (this.db) return Promise.resolve(this.db);
|
|
891
|
+
if (this.opening) return this.opening;
|
|
892
|
+
const opening = this.openWithRetry().then((db) => {
|
|
893
|
+
this.db = db;
|
|
894
|
+
this.opening = null;
|
|
895
|
+
return db;
|
|
896
|
+
}, (err) => {
|
|
897
|
+
this.opening = null;
|
|
898
|
+
throw err;
|
|
899
|
+
});
|
|
900
|
+
this.opening = opening;
|
|
901
|
+
return opening;
|
|
902
|
+
}
|
|
903
|
+
async openWithRetry() {
|
|
904
|
+
try {
|
|
905
|
+
return await this.openOnce();
|
|
906
|
+
} catch (first) {
|
|
907
|
+
if (first instanceof StorageUnavailableError || first?.name === "SecurityError") throw first;
|
|
908
|
+
return this.openOnce();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
openOnce() {
|
|
912
|
+
const factory = this.factory;
|
|
913
|
+
return new Promise((resolve, reject) => {
|
|
914
|
+
if (!factory) {
|
|
915
|
+
reject(new StorageUnavailableError());
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
let req;
|
|
919
|
+
try {
|
|
920
|
+
req = factory.open(this.cfg.dbName, DB_VERSION);
|
|
921
|
+
} catch (e) {
|
|
922
|
+
reject(e);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const timer = setTimeout(() => reject(new StorageUnavailableError(uiText("connection.storageTimeout"))), OPEN_TIMEOUT_MS);
|
|
926
|
+
req.onupgradeneeded = () => {
|
|
927
|
+
const db = req.result;
|
|
928
|
+
if (!db.objectStoreNames.contains(MUTATIONS)) {
|
|
929
|
+
const m = db.createObjectStore(MUTATIONS, { keyPath: "id" });
|
|
930
|
+
m.createIndex("userId", "userId");
|
|
931
|
+
m.createIndex("status", "status");
|
|
932
|
+
m.createIndex("createdAt", "createdAt");
|
|
933
|
+
}
|
|
934
|
+
if (!db.objectStoreNames.contains(RESPONSES)) {
|
|
935
|
+
const r = db.createObjectStore(RESPONSES, { keyPath: "key" });
|
|
936
|
+
r.createIndex("userId", "userId");
|
|
937
|
+
r.createIndex("expiresAt", "expiresAt");
|
|
938
|
+
}
|
|
939
|
+
if (!db.objectStoreNames.contains(META)) db.createObjectStore(META, { keyPath: "id" });
|
|
940
|
+
if (!db.objectStoreNames.contains(IDMAP)) {
|
|
941
|
+
const i = db.createObjectStore(IDMAP, { keyPath: "key" });
|
|
942
|
+
i.createIndex("userId", "userId");
|
|
943
|
+
i.createIndex("expiresAt", "expiresAt");
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
req.onsuccess = () => {
|
|
947
|
+
clearTimeout(timer);
|
|
948
|
+
const db = req.result;
|
|
949
|
+
db.onversionchange = () => {
|
|
950
|
+
db.close();
|
|
951
|
+
if (this.db === db) this.db = null;
|
|
952
|
+
};
|
|
953
|
+
db.onclose = () => {
|
|
954
|
+
if (this.db === db) this.db = null;
|
|
955
|
+
};
|
|
956
|
+
resolve(db);
|
|
957
|
+
};
|
|
958
|
+
req.onerror = () => {
|
|
959
|
+
clearTimeout(timer);
|
|
960
|
+
reject(req.error ?? new StorageUnavailableError());
|
|
961
|
+
};
|
|
962
|
+
req.onblocked = () => {};
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
async runTx(names, mode, work, retried = false) {
|
|
966
|
+
const db = await this.getDb();
|
|
967
|
+
let tx;
|
|
968
|
+
try {
|
|
969
|
+
tx = db.transaction(names, mode);
|
|
970
|
+
} catch (e) {
|
|
971
|
+
if (!retried && isInvalidState(e)) {
|
|
972
|
+
if (this.db === db) this.db = null;
|
|
973
|
+
return this.runTx(names, mode, work, true);
|
|
974
|
+
}
|
|
975
|
+
throw e;
|
|
976
|
+
}
|
|
977
|
+
return new Promise((resolve, reject) => {
|
|
978
|
+
let result;
|
|
979
|
+
let failure;
|
|
980
|
+
let failed = false;
|
|
981
|
+
const abortWith = (e) => {
|
|
982
|
+
failed = true;
|
|
983
|
+
failure = e;
|
|
984
|
+
try {
|
|
985
|
+
tx.abort();
|
|
986
|
+
} catch {}
|
|
987
|
+
};
|
|
988
|
+
tx.oncomplete = () => resolve(result);
|
|
989
|
+
tx.onabort = () => reject(failed ? failure : tx.error ?? new Error(uiText("connection.transactionAborted")));
|
|
990
|
+
const stores = {};
|
|
991
|
+
try {
|
|
992
|
+
for (const n of names) stores[n] = tx.objectStore(n);
|
|
993
|
+
work(stores, (v) => {
|
|
994
|
+
result = v;
|
|
995
|
+
}, abortWith);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
abortWith(e);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
static {
|
|
1002
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
1003
|
+
minVersion: "12.0.0",
|
|
1004
|
+
version: "22.2.2",
|
|
1005
|
+
ngImport: i0,
|
|
1006
|
+
type: OfflineStore,
|
|
1007
|
+
deps: [],
|
|
1008
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
static {
|
|
1012
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1013
|
+
minVersion: "12.0.0",
|
|
1014
|
+
version: "22.2.2",
|
|
1015
|
+
ngImport: i0,
|
|
1016
|
+
type: OfflineStore,
|
|
1017
|
+
providedIn: "root"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1022
|
+
minVersion: "12.0.0",
|
|
1023
|
+
version: "22.2.2",
|
|
1024
|
+
ngImport: i0,
|
|
1025
|
+
type: OfflineStore,
|
|
1026
|
+
decorators: [{
|
|
1027
|
+
type: Injectable,
|
|
1028
|
+
args: [{ providedIn: "root" }]
|
|
1029
|
+
}]
|
|
1030
|
+
});
|
|
1031
|
+
var OfflineBlockedError = class OfflineBlockedError extends Error {
|
|
1032
|
+
constructor(message, reason = "blocked") {
|
|
1033
|
+
super(message);
|
|
1034
|
+
this.offlineBlocked = true;
|
|
1035
|
+
this.status = 0;
|
|
1036
|
+
this.name = "OfflineBlockedError";
|
|
1037
|
+
this.reason = reason;
|
|
1038
|
+
Object.setPrototypeOf(this, OfflineBlockedError.prototype);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
var RequestTimeoutError = class RequestTimeoutError extends Error {
|
|
1042
|
+
constructor(message, method, url, outcomeUnknown) {
|
|
1043
|
+
super(message);
|
|
1044
|
+
this.timeout = true;
|
|
1045
|
+
this.status = 0;
|
|
1046
|
+
this.name = "RequestTimeoutError";
|
|
1047
|
+
this.method = method;
|
|
1048
|
+
this.url = url;
|
|
1049
|
+
this.outcomeUnknown = outcomeUnknown;
|
|
1050
|
+
Object.setPrototypeOf(this, RequestTimeoutError.prototype);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function isOfflineBlockedError(e) {
|
|
1054
|
+
return e instanceof OfflineBlockedError || typeof e === "object" && e !== null && e.offlineBlocked === true;
|
|
1055
|
+
}
|
|
1056
|
+
function isRequestTimeoutError(e) {
|
|
1057
|
+
return e instanceof RequestTimeoutError || typeof e === "object" && e !== null && e.timeout === true && e.status === 0;
|
|
1058
|
+
}
|
|
1059
|
+
function bodyHasOwnId(body) {
|
|
1060
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return false;
|
|
1061
|
+
const id = body["id"];
|
|
1062
|
+
return id !== void 0 && id !== null;
|
|
1063
|
+
}
|
|
1064
|
+
function isTempIdCreate(method, url, body) {
|
|
1065
|
+
return method.toUpperCase() === "POST" && !lastSegmentIsId(url) && !bodyHasOwnId(body);
|
|
1066
|
+
}
|
|
1067
|
+
function splitUrl(url) {
|
|
1068
|
+
const h = url.indexOf("#");
|
|
1069
|
+
const hash = h >= 0 ? url.slice(h) : "";
|
|
1070
|
+
const noHash = h >= 0 ? url.slice(0, h) : url;
|
|
1071
|
+
const q = noHash.indexOf("?");
|
|
1072
|
+
return {
|
|
1073
|
+
path: q >= 0 ? noHash.slice(0, q) : noHash,
|
|
1074
|
+
query: q >= 0 ? noHash.slice(q + 1) : null,
|
|
1075
|
+
hash
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function decode(s) {
|
|
1079
|
+
try {
|
|
1080
|
+
return decodeURIComponent(s);
|
|
1081
|
+
} catch {
|
|
1082
|
+
return s;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function walkStrings(value, visit) {
|
|
1086
|
+
if (typeof value === "string") {
|
|
1087
|
+
visit(value);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (Array.isArray(value)) {
|
|
1091
|
+
for (const v of value) walkStrings(v, visit);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (value !== null && typeof value === "object") for (const v of Object.values(value)) walkStrings(v, visit);
|
|
1095
|
+
}
|
|
1096
|
+
function referenceStrings(url, body) {
|
|
1097
|
+
const out = [];
|
|
1098
|
+
const parts = splitUrl(url);
|
|
1099
|
+
for (const seg of parts.path.split("/")) if (seg !== "") out.push(decode(seg));
|
|
1100
|
+
if (parts.query !== null) for (const pair of parts.query.split("&")) {
|
|
1101
|
+
const eq = pair.indexOf("=");
|
|
1102
|
+
if (eq >= 0) out.push(decode(pair.slice(eq + 1)));
|
|
1103
|
+
}
|
|
1104
|
+
walkStrings(body, (s) => out.push(s));
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
function findTempIds(url, body, candidates) {
|
|
1108
|
+
const found = [];
|
|
1109
|
+
if (candidates.size === 0) return found;
|
|
1110
|
+
for (const s of referenceStrings(url, body)) if (candidates.has(s) && !found.includes(s)) found.push(s);
|
|
1111
|
+
return found;
|
|
1112
|
+
}
|
|
1113
|
+
function rewriteValue(value, map) {
|
|
1114
|
+
if (typeof value === "string") return map.has(value) ? map.get(value) : value;
|
|
1115
|
+
if (Array.isArray(value)) return value.map((v) => rewriteValue(v, map));
|
|
1116
|
+
if (value !== null && typeof value === "object") {
|
|
1117
|
+
const out = {};
|
|
1118
|
+
for (const [k, v] of Object.entries(value)) Object.defineProperty(out, k, {
|
|
1119
|
+
value: rewriteValue(v, map),
|
|
1120
|
+
enumerable: true,
|
|
1121
|
+
writable: true,
|
|
1122
|
+
configurable: true
|
|
1123
|
+
});
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
function rewriteTempIds(url, body, map) {
|
|
1129
|
+
if (map.size === 0) return {
|
|
1130
|
+
url,
|
|
1131
|
+
body
|
|
1132
|
+
};
|
|
1133
|
+
const parts = splitUrl(url);
|
|
1134
|
+
const path = parts.path.split("/").map((seg) => {
|
|
1135
|
+
const d = decode(seg);
|
|
1136
|
+
return map.has(d) ? encodeURIComponent(String(map.get(d))) : seg;
|
|
1137
|
+
}).join("/");
|
|
1138
|
+
let query = parts.query;
|
|
1139
|
+
if (query !== null) query = query.split("&").map((pair) => {
|
|
1140
|
+
const eq = pair.indexOf("=");
|
|
1141
|
+
if (eq < 0) return pair;
|
|
1142
|
+
const d = decode(pair.slice(eq + 1));
|
|
1143
|
+
return map.has(d) ? `${pair.slice(0, eq)}=${encodeURIComponent(String(map.get(d)))}` : pair;
|
|
1144
|
+
}).join("&");
|
|
1145
|
+
return {
|
|
1146
|
+
url: path + (query !== null ? `?${query}` : "") + parts.hash,
|
|
1147
|
+
body: rewriteValue(body, map)
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
const OFFLINE_LOCK_MANAGER = new InjectionToken("OFFLINE_LOCK_MANAGER", {
|
|
1151
|
+
providedIn: "root",
|
|
1152
|
+
factory: () => {
|
|
1153
|
+
try {
|
|
1154
|
+
return typeof navigator !== "undefined" && navigator.locks && typeof navigator.locks.request === "function" ? navigator.locks : null;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
const OFFLINE_LEASE_STORAGE = new InjectionToken("OFFLINE_LEASE_STORAGE", {
|
|
1161
|
+
providedIn: "root",
|
|
1162
|
+
factory: () => {
|
|
1163
|
+
try {
|
|
1164
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
const LEASE_TTL_MS = 3e4;
|
|
1171
|
+
const LEASE_REFRESH_MS = 1e4;
|
|
1172
|
+
const BACKOFF_BASE_MS = 2e3;
|
|
1173
|
+
const BACKOFF_CAP_MS = 3e5;
|
|
1174
|
+
function backoffMs(attempts) {
|
|
1175
|
+
return Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attempts - 1), BACKOFF_CAP_MS);
|
|
1176
|
+
}
|
|
1177
|
+
function classifyReplay(err) {
|
|
1178
|
+
if (isRequestTimeoutError(err)) return {
|
|
1179
|
+
action: "retry",
|
|
1180
|
+
kind: "timeout",
|
|
1181
|
+
status: 0
|
|
1182
|
+
};
|
|
1183
|
+
if (!(err instanceof HttpErrorResponse)) return {
|
|
1184
|
+
action: "retry",
|
|
1185
|
+
kind: "network",
|
|
1186
|
+
status: 0
|
|
1187
|
+
};
|
|
1188
|
+
const s = err.status;
|
|
1189
|
+
if (s >= 200 && s < 300) return { action: "ok" };
|
|
1190
|
+
if (s === 0) return {
|
|
1191
|
+
action: "retry",
|
|
1192
|
+
kind: "network",
|
|
1193
|
+
status: 0
|
|
1194
|
+
};
|
|
1195
|
+
if (s === 401 || s === 403) return {
|
|
1196
|
+
action: "auth",
|
|
1197
|
+
status: s
|
|
1198
|
+
};
|
|
1199
|
+
if (s === 409) return {
|
|
1200
|
+
action: "conflict",
|
|
1201
|
+
status: s
|
|
1202
|
+
};
|
|
1203
|
+
if (s === 408 || s === 429) return {
|
|
1204
|
+
action: "retry",
|
|
1205
|
+
kind: "server",
|
|
1206
|
+
status: s
|
|
1207
|
+
};
|
|
1208
|
+
if (s >= 400 && s < 500) return {
|
|
1209
|
+
action: "rejected",
|
|
1210
|
+
status: s
|
|
1211
|
+
};
|
|
1212
|
+
return {
|
|
1213
|
+
action: "retry",
|
|
1214
|
+
kind: "server",
|
|
1215
|
+
status: s
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
var OfflineReplayer = class OfflineReplayer {
|
|
1219
|
+
constructor() {
|
|
1220
|
+
this.cfg = resolveConfig(inject(OFFLINE_SUPPORT_CONFIG, { optional: true }) ?? { getUserId: () => null });
|
|
1221
|
+
this.store = inject(OfflineStore);
|
|
1222
|
+
this.http = inject(HttpClient);
|
|
1223
|
+
this.connection = inject(ConnectionStateService);
|
|
1224
|
+
this.toast = inject(ToastService);
|
|
1225
|
+
this.labels = inject(UiLabelsService);
|
|
1226
|
+
this.locks = inject(OFFLINE_LOCK_MANAGER);
|
|
1227
|
+
this.leaseStorage = inject(OFFLINE_LEASE_STORAGE);
|
|
1228
|
+
this.owner = generateUuid();
|
|
1229
|
+
this.running = null;
|
|
1230
|
+
this._syncing = signal(false, ...ngDevMode ? [{ debugName: "_syncing" }] : /* istanbul ignore next */ []);
|
|
1231
|
+
this._pendingCount = signal(0, ...ngDevMode ? [{ debugName: "_pendingCount" }] : /* istanbul ignore next */ []);
|
|
1232
|
+
this._failedCount = signal(0, ...ngDevMode ? [{ debugName: "_failedCount" }] : /* istanbul ignore next */ []);
|
|
1233
|
+
this._conflictCount = signal(0, ...ngDevMode ? [{ debugName: "_conflictCount" }] : /* istanbul ignore next */ []);
|
|
1234
|
+
this._lastSyncAt = signal(null, ...ngDevMode ? [{ debugName: "_lastSyncAt" }] : /* istanbul ignore next */ []);
|
|
1235
|
+
this._syncTick = signal(0, ...ngDevMode ? [{ debugName: "_syncTick" }] : /* istanbul ignore next */ []);
|
|
1236
|
+
this._authBlocked = signal(false, ...ngDevMode ? [{ debugName: "_authBlocked" }] : /* istanbul ignore next */ []);
|
|
1237
|
+
this._lastResult = signal(null, ...ngDevMode ? [{ debugName: "_lastResult" }] : /* istanbul ignore next */ []);
|
|
1238
|
+
this.syncing = this._syncing.asReadonly();
|
|
1239
|
+
this.pendingCount = this._pendingCount.asReadonly();
|
|
1240
|
+
this.failedCount = this._failedCount.asReadonly();
|
|
1241
|
+
this.conflictCount = this._conflictCount.asReadonly();
|
|
1242
|
+
this.lastSyncAt = this._lastSyncAt.asReadonly();
|
|
1243
|
+
this.syncTick = this._syncTick.asReadonly();
|
|
1244
|
+
this.authBlocked = this._authBlocked.asReadonly();
|
|
1245
|
+
this.lastResult = this._lastResult.asReadonly();
|
|
1246
|
+
this.runCompleted$ = new Subject();
|
|
1247
|
+
}
|
|
1248
|
+
replayAll(trigger) {
|
|
1249
|
+
if (this.running) return this.running;
|
|
1250
|
+
return this.start(() => this.run(trigger, null));
|
|
1251
|
+
}
|
|
1252
|
+
async retryOne(id) {
|
|
1253
|
+
await this.store.init();
|
|
1254
|
+
const userId = this.cfg.getUserId();
|
|
1255
|
+
if (!userId) return null;
|
|
1256
|
+
if (!await this.store.resetForRetry(id, userId)) return null;
|
|
1257
|
+
if (this.running) await this.running;
|
|
1258
|
+
return this.start(() => this.run("manual", id));
|
|
1259
|
+
}
|
|
1260
|
+
async discard(id) {
|
|
1261
|
+
await this.store.init();
|
|
1262
|
+
const userId = this.cfg.getUserId();
|
|
1263
|
+
if (!userId) return;
|
|
1264
|
+
const row = await this.store.get(id);
|
|
1265
|
+
if (!row || row.userId !== userId) return;
|
|
1266
|
+
await this.store.delete(id);
|
|
1267
|
+
await this.refreshCounts();
|
|
1268
|
+
}
|
|
1269
|
+
async discardAll() {
|
|
1270
|
+
await this.store.init();
|
|
1271
|
+
const userId = this.cfg.getUserId();
|
|
1272
|
+
if (!userId) return;
|
|
1273
|
+
await this.store.deleteMutations(userId);
|
|
1274
|
+
this._authBlocked.set(false);
|
|
1275
|
+
await this.refreshCounts();
|
|
1276
|
+
}
|
|
1277
|
+
async refreshCounts() {
|
|
1278
|
+
const userId = this.cfg.getUserId();
|
|
1279
|
+
if (!userId || !this.store.available()) {
|
|
1280
|
+
this._pendingCount.set(0);
|
|
1281
|
+
this._failedCount.set(0);
|
|
1282
|
+
this._conflictCount.set(0);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const c = await this.store.countsByStatus(userId);
|
|
1286
|
+
if (this.cfg.getUserId() !== userId) return;
|
|
1287
|
+
this._pendingCount.set(c.pending + c.syncing);
|
|
1288
|
+
this._failedCount.set(c.failed);
|
|
1289
|
+
this._conflictCount.set(c.conflict);
|
|
1290
|
+
}
|
|
1291
|
+
clearAuthBlocked() {
|
|
1292
|
+
this._authBlocked.set(false);
|
|
1293
|
+
}
|
|
1294
|
+
noteRemoteSync() {
|
|
1295
|
+
this._syncTick.update((n) => n + 1);
|
|
1296
|
+
}
|
|
1297
|
+
resetView() {
|
|
1298
|
+
this._pendingCount.set(0);
|
|
1299
|
+
this._failedCount.set(0);
|
|
1300
|
+
this._conflictCount.set(0);
|
|
1301
|
+
this._lastResult.set(null);
|
|
1302
|
+
this._authBlocked.set(false);
|
|
1303
|
+
}
|
|
1304
|
+
start(job) {
|
|
1305
|
+
const p = this.exclusive(job).finally(() => {
|
|
1306
|
+
if (this.running === p) this.running = null;
|
|
1307
|
+
});
|
|
1308
|
+
this.running = p;
|
|
1309
|
+
return p;
|
|
1310
|
+
}
|
|
1311
|
+
async exclusive(job) {
|
|
1312
|
+
const name = `forgeroute-offline-replay:${this.cfg.dbName}`;
|
|
1313
|
+
if (this.locks) try {
|
|
1314
|
+
return await this.locks.request(name, { ifAvailable: true }, async (lock) => lock ? job() : null);
|
|
1315
|
+
} catch (e) {
|
|
1316
|
+
if (!(e instanceof DOMException) || e.name !== "SecurityError" && e.name !== "NotSupportedError") throw e;
|
|
1317
|
+
}
|
|
1318
|
+
return this.withLease(job);
|
|
1319
|
+
}
|
|
1320
|
+
leaseKey() {
|
|
1321
|
+
return `forgeroute-offline-lease:${this.cfg.dbName}`;
|
|
1322
|
+
}
|
|
1323
|
+
readLease() {
|
|
1324
|
+
try {
|
|
1325
|
+
const parsed = JSON.parse(this.leaseStorage?.getItem(this.leaseKey()) ?? "null");
|
|
1326
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
tryAcquireLease() {
|
|
1332
|
+
const storage = this.leaseStorage;
|
|
1333
|
+
if (!storage) return true;
|
|
1334
|
+
try {
|
|
1335
|
+
const now = Date.now();
|
|
1336
|
+
const current = this.readLease();
|
|
1337
|
+
if (current && current.owner !== this.owner && typeof current.until === "number" && current.until > now) return false;
|
|
1338
|
+
storage.setItem(this.leaseKey(), JSON.stringify({
|
|
1339
|
+
owner: this.owner,
|
|
1340
|
+
until: now + LEASE_TTL_MS
|
|
1341
|
+
}));
|
|
1342
|
+
return this.readLease()?.owner === this.owner;
|
|
1343
|
+
} catch {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
releaseLease() {
|
|
1348
|
+
const storage = this.leaseStorage;
|
|
1349
|
+
if (!storage) return;
|
|
1350
|
+
try {
|
|
1351
|
+
if (this.readLease()?.owner === this.owner) storage.removeItem(this.leaseKey());
|
|
1352
|
+
} catch {}
|
|
1353
|
+
}
|
|
1354
|
+
async withLease(job) {
|
|
1355
|
+
if (!this.tryAcquireLease()) return null;
|
|
1356
|
+
const heartbeat = this.leaseStorage ? setInterval(() => this.tryAcquireLease(), LEASE_REFRESH_MS) : null;
|
|
1357
|
+
try {
|
|
1358
|
+
return await job();
|
|
1359
|
+
} finally {
|
|
1360
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
1361
|
+
this.releaseLease();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async run(trigger, onlyId) {
|
|
1365
|
+
await this.store.init();
|
|
1366
|
+
const userId = this.cfg.getUserId();
|
|
1367
|
+
if (!userId || !this.store.available()) return null;
|
|
1368
|
+
if (trigger !== "manual" && this.connection.isOffline()) return null;
|
|
1369
|
+
this._syncing.set(true);
|
|
1370
|
+
const result = {
|
|
1371
|
+
synced: 0,
|
|
1372
|
+
failed: 0,
|
|
1373
|
+
conflicts: 0,
|
|
1374
|
+
blockedByAuth: false
|
|
1375
|
+
};
|
|
1376
|
+
let attempted = 0;
|
|
1377
|
+
let changed = false;
|
|
1378
|
+
try {
|
|
1379
|
+
const all = await this.store.list(userId);
|
|
1380
|
+
const rows = onlyId ? all.filter((r) => r.id === onlyId) : all;
|
|
1381
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1382
|
+
for (const e of await this.store.listIdMap(userId)) idMap.set(e.tempId, e);
|
|
1383
|
+
const createsByTemp = /* @__PURE__ */ new Map();
|
|
1384
|
+
for (const r of all) if (r.tempId) createsByTemp.set(r.tempId, r);
|
|
1385
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
1386
|
+
const entityOf = (r) => r.entityType && r.entityId ? `${r.entityType}::${r.entityId}` : null;
|
|
1387
|
+
if (!onlyId) for (const r of rows) {
|
|
1388
|
+
const k = entityOf(r);
|
|
1389
|
+
if (k && (r.status === "failed" || r.status === "conflict")) blocked.add(k);
|
|
1390
|
+
}
|
|
1391
|
+
for (const row of rows) {
|
|
1392
|
+
if (this.cfg.getUserId() !== userId) break;
|
|
1393
|
+
const ek = entityOf(row);
|
|
1394
|
+
if (!onlyId) {
|
|
1395
|
+
if (row.status === "failed" || row.status === "conflict") continue;
|
|
1396
|
+
if (ek && blocked.has(ek)) continue;
|
|
1397
|
+
if (row.status === "pending" && row.nextAttemptAt !== void 0 && row.nextAttemptAt > Date.now() && trigger !== "manual") {
|
|
1398
|
+
if (ek) blocked.add(ek);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const dependency = await this.checkDependencies(row, userId, idMap, createsByTemp);
|
|
1403
|
+
if (dependency === "hold") {
|
|
1404
|
+
if (ek) blocked.add(ek);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const claimed = await this.store.claim(row.id, this.owner);
|
|
1408
|
+
if (!claimed) {
|
|
1409
|
+
if (ek) blocked.add(ek);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (this.cfg.getUserId() !== userId) {
|
|
1413
|
+
await this.store.update(claimed.id, {
|
|
1414
|
+
status: "pending",
|
|
1415
|
+
claimOwner: void 0,
|
|
1416
|
+
claimAt: void 0
|
|
1417
|
+
}, { ifOwner: this.owner });
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
attempted++;
|
|
1421
|
+
const outcome = dependency === "fail" ? { action: "dependency" } : await this.send(claimed, idMap);
|
|
1422
|
+
const stop = await this.apply(claimed, outcome, result, idMap);
|
|
1423
|
+
if (outcome.action === "ok") changed = true;
|
|
1424
|
+
if (outcome.action !== "ok" && ek) blocked.add(ek);
|
|
1425
|
+
if (stop) break;
|
|
1426
|
+
}
|
|
1427
|
+
} finally {
|
|
1428
|
+
this._syncing.set(false);
|
|
1429
|
+
}
|
|
1430
|
+
await this.refreshCounts();
|
|
1431
|
+
if (result.synced > 0 && !result.blockedByAuth) this._authBlocked.set(false);
|
|
1432
|
+
if (attempted > 0) {
|
|
1433
|
+
this._lastResult.set(result);
|
|
1434
|
+
if (result.synced + result.failed + result.conflicts > 0) this._lastSyncAt.set(Date.now());
|
|
1435
|
+
}
|
|
1436
|
+
if (changed) this._syncTick.update((n) => n + 1);
|
|
1437
|
+
this.announce(result);
|
|
1438
|
+
if (attempted > 0) this.runCompleted$.next({
|
|
1439
|
+
trigger,
|
|
1440
|
+
result,
|
|
1441
|
+
changed
|
|
1442
|
+
});
|
|
1443
|
+
return result;
|
|
1444
|
+
}
|
|
1445
|
+
async checkDependencies(row, userId, idMap, createsByTemp) {
|
|
1446
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1447
|
+
(row.dependsOn ?? []).forEach((rowId, i) => {
|
|
1448
|
+
const tempId = row.dependsOnTempIds?.[i] ?? [...createsByTemp.values()].find((c) => c.id === rowId)?.tempId;
|
|
1449
|
+
deps.set(tempId ?? `\u0000${rowId}`, rowId);
|
|
1450
|
+
});
|
|
1451
|
+
for (const t of findTempIds(row.url, row.body, createsByTemp)) if (t !== row.tempId && !deps.has(t)) deps.set(t, createsByTemp.get(t)?.id);
|
|
1452
|
+
let hold = false;
|
|
1453
|
+
for (const [tempId, creatorId] of deps) {
|
|
1454
|
+
if (idMap.has(tempId)) continue;
|
|
1455
|
+
let creator = creatorId ? await this.store.get(creatorId) : null;
|
|
1456
|
+
if (creator && creator.userId !== userId) creator = null;
|
|
1457
|
+
if (creator) {
|
|
1458
|
+
if (creator.status === "pending" || creator.status === "syncing") hold = true;
|
|
1459
|
+
else return "fail";
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
const late = await this.store.getIdMapping(userId, tempId);
|
|
1463
|
+
if (late) {
|
|
1464
|
+
idMap.set(tempId, late);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
return "fail";
|
|
1468
|
+
}
|
|
1469
|
+
return hold ? "hold" : "ok";
|
|
1470
|
+
}
|
|
1471
|
+
async send(row, idMap) {
|
|
1472
|
+
try {
|
|
1473
|
+
const real = /* @__PURE__ */ new Map();
|
|
1474
|
+
for (const [tempId, e] of idMap) real.set(tempId, e.numeric && String(Number(e.realId)) === e.realId ? Number(e.realId) : e.realId);
|
|
1475
|
+
const target = rewriteTempIds(row.url, row.body, real);
|
|
1476
|
+
const response = await firstValueFrom(this.http.request(row.method, target.url, {
|
|
1477
|
+
body: target.body,
|
|
1478
|
+
headers: {
|
|
1479
|
+
...row.headers ?? {},
|
|
1480
|
+
[IDEMPOTENCY_HEADER]: row.key,
|
|
1481
|
+
[OFFLINE_REPLAY_HEADER]: "1"
|
|
1482
|
+
},
|
|
1483
|
+
observe: "response",
|
|
1484
|
+
responseType: "text"
|
|
1485
|
+
}));
|
|
1486
|
+
return {
|
|
1487
|
+
action: "ok",
|
|
1488
|
+
text: typeof response.body === "string" ? response.body : void 0
|
|
1489
|
+
};
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
return classifyReplay(err);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async apply(row, outcome, result, idMap) {
|
|
1495
|
+
const owner = { ifOwner: this.owner };
|
|
1496
|
+
const release = {
|
|
1497
|
+
claimOwner: void 0,
|
|
1498
|
+
claimAt: void 0
|
|
1499
|
+
};
|
|
1500
|
+
const err = (kind, status, key) => ({
|
|
1501
|
+
kind,
|
|
1502
|
+
...status !== void 0 ? { status } : {},
|
|
1503
|
+
message: this.labels.t(key)
|
|
1504
|
+
});
|
|
1505
|
+
switch (outcome.action) {
|
|
1506
|
+
case "ok":
|
|
1507
|
+
try {
|
|
1508
|
+
if (row.tempId) {
|
|
1509
|
+
const real = this.readCreatedId(row, outcome.text);
|
|
1510
|
+
if (real !== void 0) idMap.set(row.tempId, await this.store.putIdMapping(row.userId, row.tempId, real));
|
|
1511
|
+
}
|
|
1512
|
+
await this.store.delete(row.id, owner);
|
|
1513
|
+
} catch {
|
|
1514
|
+
await this.store.update(row.id, {
|
|
1515
|
+
status: "pending",
|
|
1516
|
+
...release
|
|
1517
|
+
}, owner).catch(() => null);
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
result.synced++;
|
|
1521
|
+
this._authBlocked.set(false);
|
|
1522
|
+
return false;
|
|
1523
|
+
case "dependency":
|
|
1524
|
+
await this.store.update(row.id, {
|
|
1525
|
+
status: "failed",
|
|
1526
|
+
...release,
|
|
1527
|
+
nextAttemptAt: void 0,
|
|
1528
|
+
lastError: err("dependency", void 0, "connection.error.dependency")
|
|
1529
|
+
}, owner);
|
|
1530
|
+
result.failed++;
|
|
1531
|
+
return false;
|
|
1532
|
+
case "conflict":
|
|
1533
|
+
await this.store.update(row.id, {
|
|
1534
|
+
status: "conflict",
|
|
1535
|
+
...release,
|
|
1536
|
+
attempts: row.attempts + 1,
|
|
1537
|
+
lastError: err("conflict", outcome.status, "connection.error.conflict")
|
|
1538
|
+
}, owner);
|
|
1539
|
+
result.conflicts++;
|
|
1540
|
+
return false;
|
|
1541
|
+
case "rejected":
|
|
1542
|
+
await this.store.update(row.id, {
|
|
1543
|
+
status: "failed",
|
|
1544
|
+
...release,
|
|
1545
|
+
attempts: row.attempts + 1,
|
|
1546
|
+
lastError: err("rejected", outcome.status, "connection.error.rejected")
|
|
1547
|
+
}, owner);
|
|
1548
|
+
result.failed++;
|
|
1549
|
+
return false;
|
|
1550
|
+
case "auth":
|
|
1551
|
+
await this.store.update(row.id, {
|
|
1552
|
+
status: "pending",
|
|
1553
|
+
...release,
|
|
1554
|
+
lastError: err("auth", outcome.status, "connection.authBlocked")
|
|
1555
|
+
}, owner);
|
|
1556
|
+
this._authBlocked.set(true);
|
|
1557
|
+
result.blockedByAuth = true;
|
|
1558
|
+
return true;
|
|
1559
|
+
case "retry": {
|
|
1560
|
+
const message = outcome.kind === "network" ? "connection.error.network" : outcome.kind === "timeout" ? "connection.error.timeout" : "connection.error.server";
|
|
1561
|
+
const lastError = err(outcome.kind, outcome.status || void 0, message);
|
|
1562
|
+
if (outcome.status === 0 && this.connection.isOffline()) {
|
|
1563
|
+
await this.store.update(row.id, {
|
|
1564
|
+
status: "pending",
|
|
1565
|
+
...release,
|
|
1566
|
+
lastError
|
|
1567
|
+
}, owner);
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
const attempts = row.attempts + 1;
|
|
1571
|
+
if (attempts >= this.cfg.maxAttempts) {
|
|
1572
|
+
await this.store.update(row.id, {
|
|
1573
|
+
status: "failed",
|
|
1574
|
+
...release,
|
|
1575
|
+
attempts,
|
|
1576
|
+
nextAttemptAt: void 0,
|
|
1577
|
+
lastError
|
|
1578
|
+
}, owner);
|
|
1579
|
+
result.failed++;
|
|
1580
|
+
} else await this.store.update(row.id, {
|
|
1581
|
+
status: "pending",
|
|
1582
|
+
...release,
|
|
1583
|
+
attempts,
|
|
1584
|
+
nextAttemptAt: Date.now() + backoffMs(attempts),
|
|
1585
|
+
lastError
|
|
1586
|
+
}, owner);
|
|
1587
|
+
return outcome.kind !== "server";
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
readCreatedId(row, text) {
|
|
1592
|
+
let body;
|
|
1593
|
+
try {
|
|
1594
|
+
body = text ? JSON.parse(text) : void 0;
|
|
1595
|
+
} catch {
|
|
1596
|
+
body = void 0;
|
|
1597
|
+
}
|
|
1598
|
+
return this.cfg.createdId(body, row);
|
|
1599
|
+
}
|
|
1600
|
+
announce(result) {
|
|
1601
|
+
const attention = result.failed + result.conflicts;
|
|
1602
|
+
if (attention > 0) this.toast.warning(this.labels.t(attention === 1 ? "connection.needAttention.one" : "connection.needAttention.other", { count: attention }));
|
|
1603
|
+
else if (result.synced > 0 && !result.blockedByAuth) this.toast.success(this.labels.t("connection.synced"));
|
|
1604
|
+
}
|
|
1605
|
+
static {
|
|
1606
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
1607
|
+
minVersion: "12.0.0",
|
|
1608
|
+
version: "22.2.2",
|
|
1609
|
+
ngImport: i0,
|
|
1610
|
+
type: OfflineReplayer,
|
|
1611
|
+
deps: [],
|
|
1612
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
static {
|
|
1616
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1617
|
+
minVersion: "12.0.0",
|
|
1618
|
+
version: "22.2.2",
|
|
1619
|
+
ngImport: i0,
|
|
1620
|
+
type: OfflineReplayer,
|
|
1621
|
+
providedIn: "root"
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1626
|
+
minVersion: "12.0.0",
|
|
1627
|
+
version: "22.2.2",
|
|
1628
|
+
ngImport: i0,
|
|
1629
|
+
type: OfflineReplayer,
|
|
1630
|
+
decorators: [{
|
|
1631
|
+
type: Injectable,
|
|
1632
|
+
args: [{ providedIn: "root" }]
|
|
1633
|
+
}]
|
|
1634
|
+
});
|
|
1635
|
+
const REQUEST_TIMEOUT_MS = new HttpContextToken(() => null);
|
|
1636
|
+
const idempotencyInterceptor = (req, next) => {
|
|
1637
|
+
const raw = inject(OFFLINE_SUPPORT_CONFIG, { optional: true });
|
|
1638
|
+
if (!raw) return next(req);
|
|
1639
|
+
const cfg = resolveConfigCached(raw);
|
|
1640
|
+
if (!isMutationMethod(req.method)) return next(req);
|
|
1641
|
+
if (req.headers.has("X-Offline-Replay")) return next(req);
|
|
1642
|
+
if (!isInScope(req.url, cfg)) return next(req);
|
|
1643
|
+
if (typeof FormData !== "undefined" && req.body instanceof FormData) return next(req);
|
|
1644
|
+
if (req.headers.has("Idempotency-Key")) return next(req);
|
|
1645
|
+
return next(req.clone({ setHeaders: { [IDEMPOTENCY_HEADER]: generateUuid() } }));
|
|
1646
|
+
};
|
|
1647
|
+
const READ_RETRY_DELAY_MS = 1e3;
|
|
1648
|
+
function isNetworkFailure(err) {
|
|
1649
|
+
return isRequestTimeoutError(err) || err instanceof HttpErrorResponse && err.status === 0;
|
|
1650
|
+
}
|
|
1651
|
+
const connectionInterceptor = (req, next) => {
|
|
1652
|
+
const raw = inject(OFFLINE_SUPPORT_CONFIG, { optional: true });
|
|
1653
|
+
if (!raw) return next(req);
|
|
1654
|
+
const cfg = resolveConfigCached(raw);
|
|
1655
|
+
if (!isInScope(req.url, cfg)) return next(req);
|
|
1656
|
+
const connection = inject(ConnectionStateService);
|
|
1657
|
+
const labels = inject(UiLabelsService);
|
|
1658
|
+
const method = req.method.toUpperCase();
|
|
1659
|
+
const isRead = method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
1660
|
+
const long = req.responseType === "blob" || req.responseType === "arraybuffer" || req.reportProgress || typeof FormData !== "undefined" && req.body instanceof FormData || matchesAny(req.url, cfg.longRequests);
|
|
1661
|
+
const override = req.context.get(REQUEST_TIMEOUT_MS);
|
|
1662
|
+
const timeoutMs = override !== null && override !== void 0 ? override : long ? cfg.timeouts.long : isRead ? cfg.timeouts.read : cfg.timeouts.write;
|
|
1663
|
+
const outgoing = req.headers.has("X-Offline-Replay") ? req.clone({ headers: req.headers.delete(OFFLINE_REPLAY_HEADER) }) : req;
|
|
1664
|
+
const attempt = () => defer(() => {
|
|
1665
|
+
const end = connection.beginRequest({ long });
|
|
1666
|
+
const started = Date.now();
|
|
1667
|
+
let outcome = null;
|
|
1668
|
+
let stream = next(outgoing);
|
|
1669
|
+
if (timeoutMs > 0) stream = stream.pipe(timeout({
|
|
1670
|
+
each: timeoutMs,
|
|
1671
|
+
with: () => throwError(() => new RequestTimeoutError(labels.t(isRead ? "connection.timeout.read" : "connection.timeout.write"), method, req.urlWithParams, !isRead))
|
|
1672
|
+
}));
|
|
1673
|
+
return stream.pipe(tap({
|
|
1674
|
+
next: (ev) => {
|
|
1675
|
+
if (ev instanceof HttpResponse) outcome = {
|
|
1676
|
+
status: ev.status,
|
|
1677
|
+
ok: ev.ok,
|
|
1678
|
+
timedOut: false
|
|
1679
|
+
};
|
|
1680
|
+
},
|
|
1681
|
+
error: (err) => {
|
|
1682
|
+
if (isRequestTimeoutError(err)) outcome = {
|
|
1683
|
+
status: 0,
|
|
1684
|
+
ok: false,
|
|
1685
|
+
timedOut: true
|
|
1686
|
+
};
|
|
1687
|
+
else if (err instanceof HttpErrorResponse) outcome = {
|
|
1688
|
+
status: err.status,
|
|
1689
|
+
ok: false,
|
|
1690
|
+
timedOut: false
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
}), finalize(() => {
|
|
1694
|
+
end();
|
|
1695
|
+
if (outcome) connection.recordRequest({
|
|
1696
|
+
durationMs: Math.max(0, Date.now() - started),
|
|
1697
|
+
ok: outcome.ok,
|
|
1698
|
+
status: outcome.status,
|
|
1699
|
+
timedOut: outcome.timedOut,
|
|
1700
|
+
method,
|
|
1701
|
+
long
|
|
1702
|
+
});
|
|
1703
|
+
}));
|
|
1704
|
+
});
|
|
1705
|
+
if (!isRead || long) return attempt();
|
|
1706
|
+
return attempt().pipe(retry({
|
|
1707
|
+
count: 1,
|
|
1708
|
+
delay: (err) => isNetworkFailure(err) && connection.state() !== "offline" ? timer(READ_RETRY_DELAY_MS) : throwError(() => err)
|
|
1709
|
+
}));
|
|
1710
|
+
};
|
|
1711
|
+
function createChannel(name) {
|
|
1712
|
+
try {
|
|
1713
|
+
return typeof BroadcastChannel === "function" ? new BroadcastChannel(name) : null;
|
|
1714
|
+
} catch {
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
var OfflineSupportService = class OfflineSupportService {
|
|
1719
|
+
constructor() {
|
|
1720
|
+
this.cfg = resolveConfig(inject(OFFLINE_SUPPORT_CONFIG, { optional: true }) ?? { getUserId: () => null });
|
|
1721
|
+
this.store = inject(OfflineStore);
|
|
1722
|
+
this.replayer = inject(OfflineReplayer);
|
|
1723
|
+
this.connection = inject(ConnectionStateService);
|
|
1724
|
+
this.stale = inject(StaleDataService);
|
|
1725
|
+
this.toast = inject(ToastService);
|
|
1726
|
+
this.labels = inject(UiLabelsService);
|
|
1727
|
+
this.doc = inject(DOCUMENT);
|
|
1728
|
+
this.destroyRef = inject(DestroyRef);
|
|
1729
|
+
this._rows = signal([], ...ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []);
|
|
1730
|
+
this._lastQueued = signal(null, ...ngDevMode ? [{ debugName: "_lastQueued" }] : /* istanbul ignore next */ []);
|
|
1731
|
+
this._storageAvailable = signal(true, ...ngDevMode ? [{ debugName: "_storageAvailable" }] : /* istanbul ignore next */ []);
|
|
1732
|
+
this.pendingCount = this.replayer.pendingCount;
|
|
1733
|
+
this.failedCount = this.replayer.failedCount;
|
|
1734
|
+
this.conflictCount = this.replayer.conflictCount;
|
|
1735
|
+
this.syncing = this.replayer.syncing;
|
|
1736
|
+
this.lastSyncAt = this.replayer.lastSyncAt;
|
|
1737
|
+
this.syncTick = this.replayer.syncTick;
|
|
1738
|
+
this.authBlocked = this.replayer.authBlocked;
|
|
1739
|
+
this.lastResult = this.replayer.lastResult;
|
|
1740
|
+
this.rows = this._rows.asReadonly();
|
|
1741
|
+
this.lastQueued = this._lastQueued.asReadonly();
|
|
1742
|
+
this.storageAvailable = this._storageAvailable.asReadonly();
|
|
1743
|
+
this.started = false;
|
|
1744
|
+
this.channel = null;
|
|
1745
|
+
this.refreshing = null;
|
|
1746
|
+
this.dirty = false;
|
|
1747
|
+
this.lastKnownUserId = null;
|
|
1748
|
+
this.cleanups = [];
|
|
1749
|
+
this.retryTimer = null;
|
|
1750
|
+
this.destroyed = false;
|
|
1751
|
+
}
|
|
1752
|
+
async start() {
|
|
1753
|
+
if (this.started) return;
|
|
1754
|
+
this.started = true;
|
|
1755
|
+
await this.store.init();
|
|
1756
|
+
this._storageAvailable.set(this.store.available());
|
|
1757
|
+
this.channel = createChannel(`forgeroute-offline:${this.cfg.dbName}`);
|
|
1758
|
+
if (this.channel) {
|
|
1759
|
+
const channel = this.channel;
|
|
1760
|
+
channel.onmessage = (ev) => this.onRemote(ev.data);
|
|
1761
|
+
this.cleanups.push(() => {
|
|
1762
|
+
channel.onmessage = null;
|
|
1763
|
+
channel.close();
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const win = this.doc.defaultView;
|
|
1767
|
+
if (win) {
|
|
1768
|
+
const poll = () => {
|
|
1769
|
+
if (this.doc.visibilityState !== "hidden") this.refresh();
|
|
1770
|
+
};
|
|
1771
|
+
win.addEventListener("focus", poll);
|
|
1772
|
+
this.doc.addEventListener("visibilitychange", poll);
|
|
1773
|
+
this.cleanups.push(() => {
|
|
1774
|
+
win.removeEventListener("focus", poll);
|
|
1775
|
+
this.doc.removeEventListener("visibilitychange", poll);
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
const subs = [
|
|
1779
|
+
this.connection.recovered$.subscribe((trigger) => this.replay(trigger)),
|
|
1780
|
+
this.store.mutated$.subscribe(() => {
|
|
1781
|
+
this.refresh();
|
|
1782
|
+
this.post({ type: "changed" });
|
|
1783
|
+
}),
|
|
1784
|
+
this.replayer.runCompleted$.subscribe(({ changed }) => {
|
|
1785
|
+
if (changed) this.post({ type: "synced" });
|
|
1786
|
+
}),
|
|
1787
|
+
this.replayer.runCompleted$.subscribe(() => {
|
|
1788
|
+
this.refresh();
|
|
1789
|
+
})
|
|
1790
|
+
];
|
|
1791
|
+
this.cleanups.push(() => subs.forEach((s) => s.unsubscribe()));
|
|
1792
|
+
this.destroyRef.onDestroy(() => {
|
|
1793
|
+
this.destroyed = true;
|
|
1794
|
+
this.clearRetryTimer();
|
|
1795
|
+
this.cleanups.forEach((c) => c());
|
|
1796
|
+
this.cleanups = [];
|
|
1797
|
+
this.channel = null;
|
|
1798
|
+
});
|
|
1799
|
+
await this.refresh();
|
|
1800
|
+
if (this.connection.isOnline() && this.cfg.getUserId()) this.replay("start");
|
|
1801
|
+
}
|
|
1802
|
+
replay(trigger) {
|
|
1803
|
+
this.replayer.replayAll(trigger).catch(() => void 0);
|
|
1804
|
+
}
|
|
1805
|
+
clearRetryTimer() {
|
|
1806
|
+
if (this.retryTimer) {
|
|
1807
|
+
clearTimeout(this.retryTimer);
|
|
1808
|
+
this.retryTimer = null;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
scheduleRetry() {
|
|
1812
|
+
this.clearRetryTimer();
|
|
1813
|
+
if (this.destroyed) return;
|
|
1814
|
+
let earliest = null;
|
|
1815
|
+
for (const r of this._rows()) if (r.status === "pending" && r.nextAttemptAt !== void 0 && (earliest === null || r.nextAttemptAt < earliest)) earliest = r.nextAttemptAt;
|
|
1816
|
+
if (earliest === null) return;
|
|
1817
|
+
this.retryTimer = setTimeout(() => {
|
|
1818
|
+
this.retryTimer = null;
|
|
1819
|
+
if (this.cfg.getUserId() && this.connection.isOnline()) this.replay("online");
|
|
1820
|
+
}, Math.max(0, earliest - Date.now()) + 50);
|
|
1821
|
+
}
|
|
1822
|
+
post(msg) {
|
|
1823
|
+
try {
|
|
1824
|
+
this.channel?.postMessage(msg);
|
|
1825
|
+
} catch {}
|
|
1826
|
+
}
|
|
1827
|
+
onRemote(msg) {
|
|
1828
|
+
if (!msg) return;
|
|
1829
|
+
if (msg.type === "synced") this.replayer.noteRemoteSync();
|
|
1830
|
+
this.refresh();
|
|
1831
|
+
}
|
|
1832
|
+
onQueued(row) {
|
|
1833
|
+
this.lastKnownUserId = row.userId;
|
|
1834
|
+
this._lastQueued.set({
|
|
1835
|
+
label: row.label,
|
|
1836
|
+
at: Date.now()
|
|
1837
|
+
});
|
|
1838
|
+
this.toast.info(this.labels.t("connection.savedOnDevice"));
|
|
1839
|
+
this.refresh();
|
|
1840
|
+
}
|
|
1841
|
+
refresh() {
|
|
1842
|
+
if (this.refreshing) {
|
|
1843
|
+
this.dirty = true;
|
|
1844
|
+
return this.refreshing;
|
|
1845
|
+
}
|
|
1846
|
+
const p = this.doRefresh().finally(() => {
|
|
1847
|
+
this.refreshing = null;
|
|
1848
|
+
});
|
|
1849
|
+
this.refreshing = p;
|
|
1850
|
+
return p;
|
|
1851
|
+
}
|
|
1852
|
+
async doRefresh() {
|
|
1853
|
+
do {
|
|
1854
|
+
this.dirty = false;
|
|
1855
|
+
const userId = this.cfg.getUserId();
|
|
1856
|
+
if (!userId) this._rows.set([]);
|
|
1857
|
+
else {
|
|
1858
|
+
this.lastKnownUserId = userId;
|
|
1859
|
+
await this.store.init();
|
|
1860
|
+
const rows = await this.store.list(userId);
|
|
1861
|
+
if (this.cfg.getUserId() !== userId) {
|
|
1862
|
+
this.dirty = true;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
this._rows.set(rows);
|
|
1866
|
+
}
|
|
1867
|
+
await this.replayer.refreshCounts();
|
|
1868
|
+
} while (this.dirty);
|
|
1869
|
+
this.scheduleRetry();
|
|
1870
|
+
}
|
|
1871
|
+
async syncNow() {
|
|
1872
|
+
const result = await this.replayer.replayAll("manual");
|
|
1873
|
+
await this.refresh();
|
|
1874
|
+
return result;
|
|
1875
|
+
}
|
|
1876
|
+
async retry(id) {
|
|
1877
|
+
await this.replayer.retryOne(id);
|
|
1878
|
+
await this.refresh();
|
|
1879
|
+
}
|
|
1880
|
+
async discard(id) {
|
|
1881
|
+
await this.replayer.discard(id);
|
|
1882
|
+
await this.refresh();
|
|
1883
|
+
}
|
|
1884
|
+
async discardAll() {
|
|
1885
|
+
await this.replayer.discardAll();
|
|
1886
|
+
await this.refresh();
|
|
1887
|
+
}
|
|
1888
|
+
async beforeSessionEnd() {
|
|
1889
|
+
const userId = this.cfg.getUserId() ?? this.lastKnownUserId;
|
|
1890
|
+
if (!userId) return {
|
|
1891
|
+
pending: 0,
|
|
1892
|
+
failed: 0
|
|
1893
|
+
};
|
|
1894
|
+
await this.store.init();
|
|
1895
|
+
if (!this.store.available()) return {
|
|
1896
|
+
pending: 0,
|
|
1897
|
+
failed: 0
|
|
1898
|
+
};
|
|
1899
|
+
const c = await this.store.countsByStatus(userId);
|
|
1900
|
+
return {
|
|
1901
|
+
pending: c.pending + c.syncing,
|
|
1902
|
+
failed: c.failed + c.conflict
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
async onSessionStart(userId) {
|
|
1906
|
+
this.lastKnownUserId = userId;
|
|
1907
|
+
this.replayer.resetView();
|
|
1908
|
+
this._lastQueued.set(null);
|
|
1909
|
+
this.stale.clear();
|
|
1910
|
+
await this.refresh();
|
|
1911
|
+
if (this.cfg.getUserId() === userId) this.replay("start");
|
|
1912
|
+
}
|
|
1913
|
+
ensureScope() {
|
|
1914
|
+
const current = this.cfg.getUserId();
|
|
1915
|
+
if (current === this.lastKnownUserId) return;
|
|
1916
|
+
if (this.lastKnownUserId === null) {
|
|
1917
|
+
this.lastKnownUserId = current;
|
|
1918
|
+
this.refresh();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
this.replayer.resetView();
|
|
1922
|
+
this._rows.set([]);
|
|
1923
|
+
this._lastQueued.set(null);
|
|
1924
|
+
this.stale.clear();
|
|
1925
|
+
this.lastKnownUserId = current;
|
|
1926
|
+
this.refresh();
|
|
1927
|
+
}
|
|
1928
|
+
async endSession(options) {
|
|
1929
|
+
const userId = options.userId ?? this.lastKnownUserId ?? this.cfg.getUserId();
|
|
1930
|
+
if (options.discardPending && userId) await this.store.clearUser(userId);
|
|
1931
|
+
this._rows.set([]);
|
|
1932
|
+
this._lastQueued.set(null);
|
|
1933
|
+
this.replayer.resetView();
|
|
1934
|
+
this.stale.clear();
|
|
1935
|
+
}
|
|
1936
|
+
async clearAll(options = {}) {
|
|
1937
|
+
if (options.discardPending) await this.store.clearAll();
|
|
1938
|
+
else await this.store.clearCache();
|
|
1939
|
+
this._rows.set([]);
|
|
1940
|
+
this._lastQueued.set(null);
|
|
1941
|
+
this.replayer.resetView();
|
|
1942
|
+
this.stale.clear();
|
|
1943
|
+
}
|
|
1944
|
+
static {
|
|
1945
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
1946
|
+
minVersion: "12.0.0",
|
|
1947
|
+
version: "22.2.2",
|
|
1948
|
+
ngImport: i0,
|
|
1949
|
+
type: OfflineSupportService,
|
|
1950
|
+
deps: [],
|
|
1951
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
static {
|
|
1955
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1956
|
+
minVersion: "12.0.0",
|
|
1957
|
+
version: "22.2.2",
|
|
1958
|
+
ngImport: i0,
|
|
1959
|
+
type: OfflineSupportService,
|
|
1960
|
+
providedIn: "root"
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1965
|
+
minVersion: "12.0.0",
|
|
1966
|
+
version: "22.2.2",
|
|
1967
|
+
ngImport: i0,
|
|
1968
|
+
type: OfflineSupportService,
|
|
1969
|
+
decorators: [{
|
|
1970
|
+
type: Injectable,
|
|
1971
|
+
args: [{ providedIn: "root" }]
|
|
1972
|
+
}]
|
|
1973
|
+
});
|
|
1974
|
+
const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
1975
|
+
const NEVER_STORED_HEADERS = [
|
|
1976
|
+
"authorization",
|
|
1977
|
+
"cookie",
|
|
1978
|
+
"proxy-authorization"
|
|
1979
|
+
];
|
|
1980
|
+
function isUploadBody(body) {
|
|
1981
|
+
return typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
|
|
1982
|
+
}
|
|
1983
|
+
function isFormBody(body) {
|
|
1984
|
+
return body instanceof HttpParams || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
|
|
1985
|
+
}
|
|
1986
|
+
function storableBody(body) {
|
|
1987
|
+
if (body === null || body === void 0) return {
|
|
1988
|
+
ok: true,
|
|
1989
|
+
value: null
|
|
1990
|
+
};
|
|
1991
|
+
if (isFormBody(body)) return {
|
|
1992
|
+
ok: true,
|
|
1993
|
+
value: body.toString()
|
|
1994
|
+
};
|
|
1995
|
+
if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") return {
|
|
1996
|
+
ok: true,
|
|
1997
|
+
value: body
|
|
1998
|
+
};
|
|
1999
|
+
try {
|
|
2000
|
+
return {
|
|
2001
|
+
ok: true,
|
|
2002
|
+
value: JSON.parse(JSON.stringify(body))
|
|
2003
|
+
};
|
|
2004
|
+
} catch {
|
|
2005
|
+
return { ok: false };
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function defaultLabel(labels, method, url) {
|
|
2009
|
+
const key = method === "POST" ? "connection.defaultLabel.add" : method === "DELETE" ? "connection.defaultLabel.delete" : "connection.defaultLabel.update";
|
|
2010
|
+
const segments = pathOnly(url).split("/").filter(Boolean);
|
|
2011
|
+
const entity = deriveEntity(url).entityType ?? segments[segments.length - 1] ?? labels.t("connection.entityFallback");
|
|
2012
|
+
return labels.t(key, { entity });
|
|
2013
|
+
}
|
|
2014
|
+
function findRule(rules, req) {
|
|
2015
|
+
const target = pathAndQuery(req.urlWithParams);
|
|
2016
|
+
const method = req.method.toUpperCase();
|
|
2017
|
+
return rules.find((r) => {
|
|
2018
|
+
r.pattern.lastIndex = 0;
|
|
2019
|
+
const hit = r.pattern.test(target);
|
|
2020
|
+
r.pattern.lastIndex = 0;
|
|
2021
|
+
return hit && r.methods.some((m) => m.toUpperCase() === method);
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
function safeHeaders(req, cfg, formBody) {
|
|
2025
|
+
const out = {};
|
|
2026
|
+
const contentType = req.headers.get("Content-Type");
|
|
2027
|
+
if (contentType && !/^application\/json/i.test(contentType)) out["Content-Type"] = contentType;
|
|
2028
|
+
else if (formBody) out["Content-Type"] = FORM_CONTENT_TYPE;
|
|
2029
|
+
for (const name of cfg.replayHeaders) {
|
|
2030
|
+
if (NEVER_STORED_HEADERS.includes(name.toLowerCase())) continue;
|
|
2031
|
+
const value = req.headers.get(name);
|
|
2032
|
+
if (value !== null) out[name] = value;
|
|
2033
|
+
}
|
|
2034
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2035
|
+
}
|
|
2036
|
+
const offlineInterceptor = (req, next) => {
|
|
2037
|
+
const raw = inject(OFFLINE_SUPPORT_CONFIG, { optional: true });
|
|
2038
|
+
if (!raw) return next(req);
|
|
2039
|
+
const cfg = resolveConfigCached(raw);
|
|
2040
|
+
if (req.headers.has("X-Offline-Replay") || !isInScope(req.url, cfg)) return next(req);
|
|
2041
|
+
inject(OfflineSupportService).ensureScope();
|
|
2042
|
+
if (req.method === "GET") return handleRead(req, next, cfg);
|
|
2043
|
+
if (isMutationMethod(req.method)) return handleWrite(req, next, cfg);
|
|
2044
|
+
return next(req);
|
|
2045
|
+
};
|
|
2046
|
+
function cachedResponse(req, data, cachedAt) {
|
|
2047
|
+
return new HttpResponse({
|
|
2048
|
+
body: data,
|
|
2049
|
+
status: 200,
|
|
2050
|
+
statusText: "OK",
|
|
2051
|
+
url: req.urlWithParams,
|
|
2052
|
+
headers: new HttpHeaders({ [FROM_CACHE_HEADER]: String(cachedAt) })
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
function handleRead(req, next, cfg) {
|
|
2056
|
+
const userId = cfg.getUserId();
|
|
2057
|
+
if (userId === null || req.responseType !== "json" || matchesAny(req.url, cfg.neverCache)) return next(req);
|
|
2058
|
+
const connection = inject(ConnectionStateService);
|
|
2059
|
+
const store = inject(OfflineStore);
|
|
2060
|
+
const stale = inject(StaleDataService);
|
|
2061
|
+
const key = pathAndQuery(req.urlWithParams);
|
|
2062
|
+
const ttl = cfg.cacheTtl ? cfg.cacheTtl(req.url) : lastSegmentIsId(req.url) ? DEFAULT_DETAIL_TTL_MS : DEFAULT_LIST_TTL_MS;
|
|
2063
|
+
const live = () => next(req).pipe(tap((ev) => {
|
|
2064
|
+
if (!(ev instanceof HttpResponse) || ev.status !== 200) return;
|
|
2065
|
+
stale.clearKey(key);
|
|
2066
|
+
if (!(ev.headers.get("Cache-Control") ?? "").toLowerCase().includes("no-store") && ev.body !== null && ev.body !== void 0) store.cacheResponse(key, userId, ev.body, ttl).catch(() => void 0);
|
|
2067
|
+
}));
|
|
2068
|
+
const serveCopyOr = (otherwise) => from(store.getCached(key, userId)).pipe(switchMap((entry) => {
|
|
2069
|
+
if (!entry) return otherwise();
|
|
2070
|
+
stale.markStale(entry.cachedAt, key);
|
|
2071
|
+
return of(cachedResponse(req, entry.data, entry.cachedAt));
|
|
2072
|
+
}));
|
|
2073
|
+
if (connection.isOffline()) return serveCopyOr(() => connection.state() === "unreachable" ? live() : throwError(() => new HttpErrorResponse({
|
|
2074
|
+
url: req.urlWithParams,
|
|
2075
|
+
status: 0,
|
|
2076
|
+
statusText: "Offline",
|
|
2077
|
+
error: uiText("connection.noCopy")
|
|
2078
|
+
})));
|
|
2079
|
+
return live().pipe(catchError((err) => isNetworkFailure(err) ? serveCopyOr(() => throwError(() => err)) : throwError(() => err)));
|
|
2080
|
+
}
|
|
2081
|
+
function handleWrite(req, next, cfg) {
|
|
2082
|
+
const connection = inject(ConnectionStateService);
|
|
2083
|
+
const userId = cfg.getUserId();
|
|
2084
|
+
if (!connection.isOffline() || userId === null) {
|
|
2085
|
+
if (userId === null) return next(req);
|
|
2086
|
+
const store = inject(OfflineStore);
|
|
2087
|
+
return next(req).pipe(tap((ev) => {
|
|
2088
|
+
if (ev instanceof HttpResponse && ev.ok) store.invalidatePrefix(invalidationPrefix(req.url), userId).catch(() => void 0);
|
|
2089
|
+
}));
|
|
2090
|
+
}
|
|
2091
|
+
const labels = inject(UiLabelsService);
|
|
2092
|
+
const block = cfg.blockOffline.find((b) => matchesUrl(req.urlWithParams, b.pattern));
|
|
2093
|
+
if (block) return throwError(() => new OfflineBlockedError(block.message ?? labels.t("connection.blocked", { action: labels.t("connection.thisAction") }), "blocked"));
|
|
2094
|
+
if (isUploadBody(req.body)) return throwError(() => new OfflineBlockedError(labels.t("connection.uploadBlocked"), "upload"));
|
|
2095
|
+
let rule;
|
|
2096
|
+
if (cfg.allowQueue) {
|
|
2097
|
+
rule = findRule(cfg.allowQueue, req);
|
|
2098
|
+
if (!rule) return throwError(() => new OfflineBlockedError(labels.t("connection.blocked", { action: labels.t("connection.thisAction") }), "not-allowed"));
|
|
2099
|
+
}
|
|
2100
|
+
const body = storableBody(req.body);
|
|
2101
|
+
if (!body.ok) return throwError(() => new OfflineBlockedError(labels.t("connection.storageUnavailable"), "storage"));
|
|
2102
|
+
const store = inject(OfflineStore);
|
|
2103
|
+
const support = inject(OfflineSupportService);
|
|
2104
|
+
const method = req.method.toUpperCase();
|
|
2105
|
+
const url = req.urlWithParams;
|
|
2106
|
+
const derived = deriveEntity(url);
|
|
2107
|
+
const row = {
|
|
2108
|
+
id: generateUuid(),
|
|
2109
|
+
userId,
|
|
2110
|
+
key: req.headers.get("Idempotency-Key") ?? generateUuid(),
|
|
2111
|
+
method,
|
|
2112
|
+
url,
|
|
2113
|
+
body: body.value,
|
|
2114
|
+
headers: safeHeaders(req, cfg, isFormBody(req.body)),
|
|
2115
|
+
label: rule?.label?.(method, url, req.body) ?? defaultLabel(labels, method, url),
|
|
2116
|
+
entityType: rule?.entityType?.(url) ?? derived.entityType,
|
|
2117
|
+
entityId: derived.entityId,
|
|
2118
|
+
createdAt: Date.now(),
|
|
2119
|
+
status: "pending",
|
|
2120
|
+
attempts: 0
|
|
2121
|
+
};
|
|
2122
|
+
const tempId = isTempIdCreate(method, url, body.value) ? row.id : void 0;
|
|
2123
|
+
return from(store.list(userId).catch(() => [])).pipe(switchMap((existing) => {
|
|
2124
|
+
const creates = /* @__PURE__ */ new Map();
|
|
2125
|
+
for (const r of existing) if (r.tempId) creates.set(r.tempId, r.id);
|
|
2126
|
+
const refs = findTempIds(url, body.value, creates);
|
|
2127
|
+
return store.addMutation({
|
|
2128
|
+
...row,
|
|
2129
|
+
...tempId ? { tempId } : {},
|
|
2130
|
+
...refs.length > 0 ? {
|
|
2131
|
+
dependsOn: refs.map((t) => creates.get(t)),
|
|
2132
|
+
dependsOnTempIds: refs
|
|
2133
|
+
} : {}
|
|
2134
|
+
});
|
|
2135
|
+
}), catchError(() => throwError(() => new OfflineBlockedError(labels.t("connection.storageUnavailable"), "storage"))), switchMap((saved) => {
|
|
2136
|
+
if (saved.status === "failed" || saved.status === "conflict") return throwError(() => new OfflineBlockedError(labels.t("connection.blocked", { action: labels.t("connection.thisAction") }), "not-allowed"));
|
|
2137
|
+
support.onQueued(saved);
|
|
2138
|
+
return of(saved);
|
|
2139
|
+
}), switchMap((saved) => {
|
|
2140
|
+
const base = body.value !== null && typeof body.value === "object" && !Array.isArray(body.value) ? body.value : {};
|
|
2141
|
+
return of(new HttpResponse({
|
|
2142
|
+
status: 202,
|
|
2143
|
+
statusText: "Accepted",
|
|
2144
|
+
url,
|
|
2145
|
+
body: {
|
|
2146
|
+
...base,
|
|
2147
|
+
id: base["id"] ?? saved.tempId ?? saved.id,
|
|
2148
|
+
_offline: true
|
|
2149
|
+
}
|
|
2150
|
+
}));
|
|
2151
|
+
}));
|
|
2152
|
+
}
|
|
2153
|
+
const offlineHttpInterceptors = [
|
|
2154
|
+
idempotencyInterceptor,
|
|
2155
|
+
offlineInterceptor,
|
|
2156
|
+
connectionInterceptor
|
|
2157
|
+
];
|
|
2158
|
+
function provideOfflineSupport(config) {
|
|
2159
|
+
return makeEnvironmentProviders([{
|
|
2160
|
+
provide: OFFLINE_SUPPORT_CONFIG,
|
|
2161
|
+
useValue: config
|
|
2162
|
+
}, provideAppInitializer(() => {
|
|
2163
|
+
const store = inject(OfflineStore);
|
|
2164
|
+
const support = inject(OfflineSupportService);
|
|
2165
|
+
inject(ConnectionStateService);
|
|
2166
|
+
return store.init().then(() => {
|
|
2167
|
+
support.start().catch(() => void 0);
|
|
2168
|
+
});
|
|
2169
|
+
})]);
|
|
2170
|
+
}
|
|
2171
|
+
const SYNCED_FLASH_MS = 3e3;
|
|
2172
|
+
let nextId = 0;
|
|
2173
|
+
var ConnectionBannerComponent = class ConnectionBannerComponent {
|
|
2174
|
+
count(base, n) {
|
|
2175
|
+
return this.labels.t(`${base}.${n === 1 ? "one" : "other"}`, { count: n });
|
|
2176
|
+
}
|
|
2177
|
+
constructor() {
|
|
2178
|
+
this.labels = inject(UiLabelsService);
|
|
2179
|
+
this.connection = inject(ConnectionStateService);
|
|
2180
|
+
this.support = inject(OfflineSupportService);
|
|
2181
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
2182
|
+
this.infoOffline = input(null, ...ngDevMode ? [{ debugName: "infoOffline" }] : /* istanbul ignore next */ []);
|
|
2183
|
+
this.infoNeedsConnection = input(null, ...ngDevMode ? [{ debugName: "infoNeedsConnection" }] : /* istanbul ignore next */ []);
|
|
2184
|
+
this.viewChanges = output();
|
|
2185
|
+
this.panelId = `ui-cb-info-${++nextId}`;
|
|
2186
|
+
this.infoOpen = signal(false, ...ngDevMode ? [{ debugName: "infoOpen" }] : /* istanbul ignore next */ []);
|
|
2187
|
+
this.infoToggleRef = viewChild("infoToggle", ...ngDevMode ? [{ debugName: "infoToggleRef" }] : /* istanbul ignore next */ []);
|
|
2188
|
+
this.infoHideRef = viewChild("infoHide", ...ngDevMode ? [{ debugName: "infoHideRef" }] : /* istanbul ignore next */ []);
|
|
2189
|
+
this.syncTotal = signal(0, ...ngDevMode ? [{ debugName: "syncTotal" }] : /* istanbul ignore next */ []);
|
|
2190
|
+
this.showSynced = signal(false, ...ngDevMode ? [{ debugName: "showSynced" }] : /* istanbul ignore next */ []);
|
|
2191
|
+
this.syncedTimer = null;
|
|
2192
|
+
this.seenTick = this.support.syncTick();
|
|
2193
|
+
this.flashPending = false;
|
|
2194
|
+
this.wasSyncing = false;
|
|
2195
|
+
this.canDoItems = computed(() => this.infoOffline() ?? [this.labels.t("connection.info.canDo1"), this.labels.t("connection.info.canDo2")], ...ngDevMode ? [{ debugName: "canDoItems" }] : /* istanbul ignore next */ []);
|
|
2196
|
+
this.needsItems = computed(() => this.infoNeedsConnection() ?? [this.labels.t("connection.info.needs1")], ...ngDevMode ? [{ debugName: "needsItems" }] : /* istanbul ignore next */ []);
|
|
2197
|
+
this.view = computed(() => {
|
|
2198
|
+
const state = this.connection.state();
|
|
2199
|
+
const pending = this.support.pendingCount();
|
|
2200
|
+
const attention = this.support.failedCount() + this.support.conflictCount();
|
|
2201
|
+
const l = this.labels;
|
|
2202
|
+
let v = null;
|
|
2203
|
+
if (this.support.authBlocked()) v = {
|
|
2204
|
+
kind: "auth",
|
|
2205
|
+
title: l.t("connection.authBlocked"),
|
|
2206
|
+
message: "",
|
|
2207
|
+
detail: ""
|
|
2208
|
+
};
|
|
2209
|
+
else if (state === "offline") v = {
|
|
2210
|
+
kind: "offline",
|
|
2211
|
+
title: l.t("connection.offline.title"),
|
|
2212
|
+
message: l.t("connection.offline.message"),
|
|
2213
|
+
detail: pending > 0 ? this.count("connection.pending", pending) : ""
|
|
2214
|
+
};
|
|
2215
|
+
else if (state === "unreachable") v = {
|
|
2216
|
+
kind: "unreachable",
|
|
2217
|
+
title: l.t("connection.unreachable.title"),
|
|
2218
|
+
message: l.t("connection.unreachable.message"),
|
|
2219
|
+
detail: pending > 0 ? this.count("connection.pending", pending) : ""
|
|
2220
|
+
};
|
|
2221
|
+
else if (this.support.syncing() && Math.max(this.syncTotal(), pending) > 0) v = {
|
|
2222
|
+
kind: "syncing",
|
|
2223
|
+
title: this.count("connection.syncing", Math.max(this.syncTotal(), pending)),
|
|
2224
|
+
message: "",
|
|
2225
|
+
detail: ""
|
|
2226
|
+
};
|
|
2227
|
+
else if (attention > 0) v = {
|
|
2228
|
+
kind: "attention",
|
|
2229
|
+
title: this.count("connection.needAttention", attention),
|
|
2230
|
+
message: "",
|
|
2231
|
+
detail: ""
|
|
2232
|
+
};
|
|
2233
|
+
else if (state === "slow") v = {
|
|
2234
|
+
kind: "slow",
|
|
2235
|
+
title: l.t("connection.slow.title"),
|
|
2236
|
+
message: l.t("connection.slow.message"),
|
|
2237
|
+
detail: ""
|
|
2238
|
+
};
|
|
2239
|
+
else if (this.showSynced()) v = {
|
|
2240
|
+
kind: "synced",
|
|
2241
|
+
title: l.t("connection.synced"),
|
|
2242
|
+
message: "",
|
|
2243
|
+
detail: ""
|
|
2244
|
+
};
|
|
2245
|
+
if (!v) return null;
|
|
2246
|
+
const sentence = (s) => /[.…!?]$/.test(s) ? s : s + ".";
|
|
2247
|
+
return {
|
|
2248
|
+
...v,
|
|
2249
|
+
announcement: [
|
|
2250
|
+
v.title,
|
|
2251
|
+
v.message,
|
|
2252
|
+
v.detail
|
|
2253
|
+
].filter(Boolean).map(sentence).join(" ")
|
|
2254
|
+
};
|
|
2255
|
+
}, ...ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []);
|
|
2256
|
+
this.actionFailure = signal("", ...ngDevMode ? [{ debugName: "actionFailure" }] : /* istanbul ignore next */ []);
|
|
2257
|
+
this.politeText = computed(() => {
|
|
2258
|
+
const v = this.view();
|
|
2259
|
+
return [v && v.kind !== "auth" ? v.announcement : "", this.actionFailure()].filter(Boolean).join(" ");
|
|
2260
|
+
}, ...ngDevMode ? [{ debugName: "politeText" }] : /* istanbul ignore next */ []);
|
|
2261
|
+
this.alertText = computed(() => {
|
|
2262
|
+
const v = this.view();
|
|
2263
|
+
return v && v.kind === "auth" ? v.announcement : "";
|
|
2264
|
+
}, ...ngDevMode ? [{ debugName: "alertText" }] : /* istanbul ignore next */ []);
|
|
2265
|
+
this.infoAllowed = computed(() => {
|
|
2266
|
+
const k = this.view()?.kind;
|
|
2267
|
+
return k === "offline" || k === "unreachable";
|
|
2268
|
+
}, ...ngDevMode ? [{ debugName: "infoAllowed" }] : /* istanbul ignore next */ []);
|
|
2269
|
+
this.showSyncNow = computed(() => {
|
|
2270
|
+
const k = this.view()?.kind;
|
|
2271
|
+
return (k === "unreachable" || k === "slow" || k === "attention") && this.support.pendingCount() > 0 && !this.support.syncing();
|
|
2272
|
+
}, ...ngDevMode ? [{ debugName: "showSyncNow" }] : /* istanbul ignore next */ []);
|
|
2273
|
+
this.showView = computed(() => {
|
|
2274
|
+
const k = this.view()?.kind;
|
|
2275
|
+
if (!k || k === "synced") return false;
|
|
2276
|
+
return this.rowsTotal() > 0 || k === "auth";
|
|
2277
|
+
}, ...ngDevMode ? [{ debugName: "showView" }] : /* istanbul ignore next */ []);
|
|
2278
|
+
this.rowsTotal = computed(() => this.support.pendingCount() + this.support.failedCount() + this.support.conflictCount(), ...ngDevMode ? [{ debugName: "rowsTotal" }] : /* istanbul ignore next */ []);
|
|
2279
|
+
effect(() => {
|
|
2280
|
+
const syncing = this.support.syncing();
|
|
2281
|
+
const tick = this.support.syncTick();
|
|
2282
|
+
untracked(() => {
|
|
2283
|
+
if (tick !== this.seenTick) {
|
|
2284
|
+
this.seenTick = tick;
|
|
2285
|
+
this.flashPending = true;
|
|
2286
|
+
}
|
|
2287
|
+
if (syncing && !this.wasSyncing) this.syncTotal.set(Math.max(this.support.pendingCount(), 0));
|
|
2288
|
+
if (!syncing) this.syncTotal.set(0);
|
|
2289
|
+
this.wasSyncing = syncing;
|
|
2290
|
+
if (!syncing && this.flashPending) {
|
|
2291
|
+
this.flashPending = false;
|
|
2292
|
+
this.flashSynced();
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
});
|
|
2296
|
+
effect(() => {
|
|
2297
|
+
if (!this.infoAllowed()) this.infoOpen.set(false);
|
|
2298
|
+
});
|
|
2299
|
+
inject(DestroyRef).onDestroy(() => {
|
|
2300
|
+
if (this.syncedTimer) clearTimeout(this.syncedTimer);
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
flashSynced() {
|
|
2304
|
+
if (this.syncedTimer) clearTimeout(this.syncedTimer);
|
|
2305
|
+
this.showSynced.set(true);
|
|
2306
|
+
this.syncedTimer = setTimeout(() => {
|
|
2307
|
+
this.syncedTimer = null;
|
|
2308
|
+
this.showSynced.set(false);
|
|
2309
|
+
}, SYNCED_FLASH_MS);
|
|
2310
|
+
}
|
|
2311
|
+
syncNow() {
|
|
2312
|
+
this.actionFailure.set("");
|
|
2313
|
+
let call;
|
|
2314
|
+
try {
|
|
2315
|
+
call = Promise.resolve(this.support.syncNow());
|
|
2316
|
+
} catch {
|
|
2317
|
+
call = Promise.reject(/* @__PURE__ */ new Error("syncNow"));
|
|
2318
|
+
}
|
|
2319
|
+
call.catch(() => this.actionFailure.set(this.labels.t("connection.error.unknown")));
|
|
2320
|
+
}
|
|
2321
|
+
toggleInfo() {
|
|
2322
|
+
if (this.infoOpen()) {
|
|
2323
|
+
this.closeInfo(true);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
this.infoOpen.set(true);
|
|
2327
|
+
this.cdr.detectChanges();
|
|
2328
|
+
this.infoHideRef()?.nativeElement.focus();
|
|
2329
|
+
}
|
|
2330
|
+
closeInfo(returnFocus) {
|
|
2331
|
+
this.infoOpen.set(false);
|
|
2332
|
+
this.cdr.detectChanges();
|
|
2333
|
+
if (returnFocus) this.infoToggleRef()?.nativeElement.focus();
|
|
2334
|
+
}
|
|
2335
|
+
onEscape(event) {
|
|
2336
|
+
if (!this.infoOpen()) return;
|
|
2337
|
+
event.stopPropagation();
|
|
2338
|
+
this.closeInfo(true);
|
|
2339
|
+
}
|
|
2340
|
+
static {
|
|
2341
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
2342
|
+
minVersion: "12.0.0",
|
|
2343
|
+
version: "22.2.2",
|
|
2344
|
+
ngImport: i0,
|
|
2345
|
+
type: ConnectionBannerComponent,
|
|
2346
|
+
deps: [],
|
|
2347
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
static {
|
|
2351
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2352
|
+
minVersion: "17.0.0",
|
|
2353
|
+
version: "22.2.2",
|
|
2354
|
+
type: ConnectionBannerComponent,
|
|
2355
|
+
isStandalone: true,
|
|
2356
|
+
selector: "ui-connection-banner",
|
|
2357
|
+
inputs: {
|
|
2358
|
+
infoOffline: {
|
|
2359
|
+
classPropertyName: "infoOffline",
|
|
2360
|
+
publicName: "infoOffline",
|
|
2361
|
+
isSignal: true,
|
|
2362
|
+
isRequired: false,
|
|
2363
|
+
transformFunction: null
|
|
2364
|
+
},
|
|
2365
|
+
infoNeedsConnection: {
|
|
2366
|
+
classPropertyName: "infoNeedsConnection",
|
|
2367
|
+
publicName: "infoNeedsConnection",
|
|
2368
|
+
isSignal: true,
|
|
2369
|
+
isRequired: false,
|
|
2370
|
+
transformFunction: null
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
outputs: { viewChanges: "viewChanges" },
|
|
2374
|
+
viewQueries: [{
|
|
2375
|
+
propertyName: "infoToggleRef",
|
|
2376
|
+
first: true,
|
|
2377
|
+
predicate: ["infoToggle"],
|
|
2378
|
+
descendants: true,
|
|
2379
|
+
isSignal: true
|
|
2380
|
+
}, {
|
|
2381
|
+
propertyName: "infoHideRef",
|
|
2382
|
+
first: true,
|
|
2383
|
+
predicate: ["infoHide"],
|
|
2384
|
+
descendants: true,
|
|
2385
|
+
isSignal: true
|
|
2386
|
+
}],
|
|
2387
|
+
ngImport: i0,
|
|
2388
|
+
template: `
|
|
2389
|
+
<div class="cb-live" role="status" aria-live="polite" aria-atomic="true" data-testid="connection-banner-live">{{ politeText() }}</div>
|
|
2390
|
+
<div class="cb-live" role="alert" aria-live="assertive" aria-atomic="true" data-testid="connection-banner-live-alert">{{ alertText() }}</div>
|
|
2391
|
+
@if (view(); as v) {
|
|
2392
|
+
<div class="cb-bar" [class]="'cb-bar cb-' + v.kind" role="group" [attr.aria-label]="labels.t('connection.region')"
|
|
2393
|
+
data-testid="connection-banner" [attr.data-state]="v.kind" (keydown.escape)="onEscape($event)">
|
|
2394
|
+
<div class="cb-text">
|
|
2395
|
+
<strong class="cb-title" data-testid="connection-banner-title">{{ v.title }}</strong>
|
|
2396
|
+
@if (v.message) { <span class="cb-msg" data-testid="connection-banner-message"> {{ v.message }}</span> }
|
|
2397
|
+
@if (v.detail) { <span class="cb-detail" data-testid="connection-banner-detail"> {{ v.detail }}</span> }
|
|
2398
|
+
</div>
|
|
2399
|
+
@if (showSyncNow() || showView() || infoAllowed()) {
|
|
2400
|
+
<div class="cb-actions">
|
|
2401
|
+
@if (showSyncNow()) {
|
|
2402
|
+
<button type="button" class="cb-btn" data-testid="connection-banner-sync-now" (click)="syncNow()">{{ labels.t('connection.syncNow') }}</button>
|
|
2403
|
+
}
|
|
2404
|
+
@if (showView()) {
|
|
2405
|
+
<button type="button" class="cb-btn" data-testid="connection-banner-view" (click)="viewChanges.emit()">{{ labels.t('connection.viewChanges') }}</button>
|
|
2406
|
+
}
|
|
2407
|
+
@if (infoAllowed()) {
|
|
2408
|
+
<button #infoToggle type="button" class="cb-btn cb-btn-ghost" data-testid="connection-banner-info-toggle"
|
|
2409
|
+
[attr.aria-expanded]="infoOpen()" [attr.aria-controls]="panelId" (click)="toggleInfo()">{{ labels.t('connection.whatCanIDo') }}</button>
|
|
2410
|
+
}
|
|
2411
|
+
</div>
|
|
2412
|
+
}
|
|
2413
|
+
@if (infoAllowed() && infoOpen()) {
|
|
2414
|
+
<div class="cb-info" [id]="panelId" role="region" tabindex="-1" [attr.aria-label]="labels.t('connection.info.panelLabel')" data-testid="connection-banner-info-panel">
|
|
2415
|
+
<p class="cb-info-h">{{ labels.t('connection.info.canDoTitle') }}</p>
|
|
2416
|
+
<ul class="cb-list">@for (item of canDoItems(); track $index) { <li>{{ item }}</li> }</ul>
|
|
2417
|
+
<p class="cb-info-h">{{ labels.t('connection.info.needsTitle') }}</p>
|
|
2418
|
+
<ul class="cb-list">@for (item of needsItems(); track $index) { <li>{{ item }}</li> }</ul>
|
|
2419
|
+
<button #infoHide type="button" class="cb-btn" data-testid="connection-banner-info-hide" (click)="closeInfo(true)">{{ labels.t('connection.hide') }}</button>
|
|
2420
|
+
</div>
|
|
2421
|
+
}
|
|
2422
|
+
</div>
|
|
2423
|
+
}
|
|
2424
|
+
`,
|
|
2425
|
+
isInline: true,
|
|
2426
|
+
styles: [":host{display:block;position:-webkit-sticky;position:sticky;top:0;z-index:50}.cb-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cb-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;padding:10px 16px;font-size:.875rem;line-height:1.4;border-block-end:1px solid;border-inline-start:6px solid;box-sizing:border-box}.cb-text{flex:1 1 280px;min-inline-size:0;overflow-wrap:anywhere}.cb-actions{display:flex;flex-wrap:wrap;gap:8px}.cb-btn{min-block-size:44px;min-inline-size:44px;padding:0 14px;border-radius:8px;border:1px solid currentColor;background:#fff;color:inherit;font:inherit;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.cb-btn-ghost{background:transparent}.cb-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.cb-btn:focus:not(:focus-visible){outline:none}.cb-info{flex:1 1 100%;padding:12px 14px;border-radius:8px;background:#ffffffb3;border:1px solid currentColor}.cb-info:focus{outline:none}.cb-info-h{margin:0 0 4px;font-weight:700}.cb-list{margin:0 0 12px;padding-inline-start:20px}.cb-offline,.cb-slow,.cb-attention{background:#fef3c7;color:#78350f;border-color:#b45309}.cb-unreachable,.cb-auth{background:#fee2e2;color:#7f1d1d;border-color:#b91c1c}.cb-syncing{background:#dbeafe;color:#1e3a8a;border-color:#1d4ed8}.cb-synced{background:#dcfce7;color:#14532d;border-color:#15803d}@media(prefers-reduced-motion:no-preference){.cb-bar{animation:cb-in .16s ease-out}}@keyframes cb-in{0%{opacity:0}to{opacity:1}}@media(max-width:480px){.cb-actions{flex-direction:column;inline-size:100%}.cb-btn{inline-size:100%}}@media(forced-colors:active){.cb-bar,.cb-info{border:2px solid CanvasText;background:Canvas;color:CanvasText}.cb-btn{border-color:ButtonText;background:ButtonFace;color:ButtonText}}\n"],
|
|
2427
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2432
|
+
minVersion: "12.0.0",
|
|
2433
|
+
version: "22.2.2",
|
|
2434
|
+
ngImport: i0,
|
|
2435
|
+
type: ConnectionBannerComponent,
|
|
2436
|
+
decorators: [{
|
|
2437
|
+
type: Component,
|
|
2438
|
+
args: [{
|
|
2439
|
+
selector: "ui-connection-banner",
|
|
2440
|
+
standalone: true,
|
|
2441
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2442
|
+
template: `
|
|
2443
|
+
<div class="cb-live" role="status" aria-live="polite" aria-atomic="true" data-testid="connection-banner-live">{{ politeText() }}</div>
|
|
2444
|
+
<div class="cb-live" role="alert" aria-live="assertive" aria-atomic="true" data-testid="connection-banner-live-alert">{{ alertText() }}</div>
|
|
2445
|
+
@if (view(); as v) {
|
|
2446
|
+
<div class="cb-bar" [class]="'cb-bar cb-' + v.kind" role="group" [attr.aria-label]="labels.t('connection.region')"
|
|
2447
|
+
data-testid="connection-banner" [attr.data-state]="v.kind" (keydown.escape)="onEscape($event)">
|
|
2448
|
+
<div class="cb-text">
|
|
2449
|
+
<strong class="cb-title" data-testid="connection-banner-title">{{ v.title }}</strong>
|
|
2450
|
+
@if (v.message) { <span class="cb-msg" data-testid="connection-banner-message"> {{ v.message }}</span> }
|
|
2451
|
+
@if (v.detail) { <span class="cb-detail" data-testid="connection-banner-detail"> {{ v.detail }}</span> }
|
|
2452
|
+
</div>
|
|
2453
|
+
@if (showSyncNow() || showView() || infoAllowed()) {
|
|
2454
|
+
<div class="cb-actions">
|
|
2455
|
+
@if (showSyncNow()) {
|
|
2456
|
+
<button type="button" class="cb-btn" data-testid="connection-banner-sync-now" (click)="syncNow()">{{ labels.t('connection.syncNow') }}</button>
|
|
2457
|
+
}
|
|
2458
|
+
@if (showView()) {
|
|
2459
|
+
<button type="button" class="cb-btn" data-testid="connection-banner-view" (click)="viewChanges.emit()">{{ labels.t('connection.viewChanges') }}</button>
|
|
2460
|
+
}
|
|
2461
|
+
@if (infoAllowed()) {
|
|
2462
|
+
<button #infoToggle type="button" class="cb-btn cb-btn-ghost" data-testid="connection-banner-info-toggle"
|
|
2463
|
+
[attr.aria-expanded]="infoOpen()" [attr.aria-controls]="panelId" (click)="toggleInfo()">{{ labels.t('connection.whatCanIDo') }}</button>
|
|
2464
|
+
}
|
|
2465
|
+
</div>
|
|
2466
|
+
}
|
|
2467
|
+
@if (infoAllowed() && infoOpen()) {
|
|
2468
|
+
<div class="cb-info" [id]="panelId" role="region" tabindex="-1" [attr.aria-label]="labels.t('connection.info.panelLabel')" data-testid="connection-banner-info-panel">
|
|
2469
|
+
<p class="cb-info-h">{{ labels.t('connection.info.canDoTitle') }}</p>
|
|
2470
|
+
<ul class="cb-list">@for (item of canDoItems(); track $index) { <li>{{ item }}</li> }</ul>
|
|
2471
|
+
<p class="cb-info-h">{{ labels.t('connection.info.needsTitle') }}</p>
|
|
2472
|
+
<ul class="cb-list">@for (item of needsItems(); track $index) { <li>{{ item }}</li> }</ul>
|
|
2473
|
+
<button #infoHide type="button" class="cb-btn" data-testid="connection-banner-info-hide" (click)="closeInfo(true)">{{ labels.t('connection.hide') }}</button>
|
|
2474
|
+
</div>
|
|
2475
|
+
}
|
|
2476
|
+
</div>
|
|
2477
|
+
}
|
|
2478
|
+
`,
|
|
2479
|
+
styles: [":host{display:block;position:-webkit-sticky;position:sticky;top:0;z-index:50}.cb-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cb-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;padding:10px 16px;font-size:.875rem;line-height:1.4;border-block-end:1px solid;border-inline-start:6px solid;box-sizing:border-box}.cb-text{flex:1 1 280px;min-inline-size:0;overflow-wrap:anywhere}.cb-actions{display:flex;flex-wrap:wrap;gap:8px}.cb-btn{min-block-size:44px;min-inline-size:44px;padding:0 14px;border-radius:8px;border:1px solid currentColor;background:#fff;color:inherit;font:inherit;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.cb-btn-ghost{background:transparent}.cb-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.cb-btn:focus:not(:focus-visible){outline:none}.cb-info{flex:1 1 100%;padding:12px 14px;border-radius:8px;background:#ffffffb3;border:1px solid currentColor}.cb-info:focus{outline:none}.cb-info-h{margin:0 0 4px;font-weight:700}.cb-list{margin:0 0 12px;padding-inline-start:20px}.cb-offline,.cb-slow,.cb-attention{background:#fef3c7;color:#78350f;border-color:#b45309}.cb-unreachable,.cb-auth{background:#fee2e2;color:#7f1d1d;border-color:#b91c1c}.cb-syncing{background:#dbeafe;color:#1e3a8a;border-color:#1d4ed8}.cb-synced{background:#dcfce7;color:#14532d;border-color:#15803d}@media(prefers-reduced-motion:no-preference){.cb-bar{animation:cb-in .16s ease-out}}@keyframes cb-in{0%{opacity:0}to{opacity:1}}@media(max-width:480px){.cb-actions{flex-direction:column;inline-size:100%}.cb-btn{inline-size:100%}}@media(forced-colors:active){.cb-bar,.cb-info{border:2px solid CanvasText;background:Canvas;color:CanvasText}.cb-btn{border-color:ButtonText;background:ButtonFace;color:ButtonText}}\n"]
|
|
2480
|
+
}]
|
|
2481
|
+
}],
|
|
2482
|
+
ctorParameters: () => [],
|
|
2483
|
+
propDecorators: {
|
|
2484
|
+
infoOffline: [{
|
|
2485
|
+
type: i0.Input,
|
|
2486
|
+
args: [{
|
|
2487
|
+
isSignal: true,
|
|
2488
|
+
alias: "infoOffline",
|
|
2489
|
+
required: false
|
|
2490
|
+
}]
|
|
2491
|
+
}],
|
|
2492
|
+
infoNeedsConnection: [{
|
|
2493
|
+
type: i0.Input,
|
|
2494
|
+
args: [{
|
|
2495
|
+
isSignal: true,
|
|
2496
|
+
alias: "infoNeedsConnection",
|
|
2497
|
+
required: false
|
|
2498
|
+
}]
|
|
2499
|
+
}],
|
|
2500
|
+
viewChanges: [{
|
|
2501
|
+
type: i0.Output,
|
|
2502
|
+
args: ["viewChanges"]
|
|
2503
|
+
}],
|
|
2504
|
+
infoToggleRef: [{
|
|
2505
|
+
type: i0.ViewChild,
|
|
2506
|
+
args: ["infoToggle", { isSignal: true }]
|
|
2507
|
+
}],
|
|
2508
|
+
infoHideRef: [{
|
|
2509
|
+
type: i0.ViewChild,
|
|
2510
|
+
args: ["infoHide", { isSignal: true }]
|
|
2511
|
+
}]
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
function formatRelativeTime(then, now, justNowText, locale) {
|
|
2515
|
+
const diffSec = Math.round((now - then) / 1e3);
|
|
2516
|
+
if (diffSec < 45) return justNowText;
|
|
2517
|
+
const rtfCtor = globalThis.Intl?.RelativeTimeFormat;
|
|
2518
|
+
const minutes = Math.round(diffSec / 60);
|
|
2519
|
+
const hours = Math.round(diffSec / 3600);
|
|
2520
|
+
const days = Math.round(diffSec / 86400);
|
|
2521
|
+
if (rtfCtor) {
|
|
2522
|
+
let rtf;
|
|
2523
|
+
try {
|
|
2524
|
+
rtf = new rtfCtor(locale, { numeric: "auto" });
|
|
2525
|
+
} catch {
|
|
2526
|
+
rtf = new rtfCtor(void 0, { numeric: "auto" });
|
|
2527
|
+
}
|
|
2528
|
+
if (minutes < 60) return rtf.format(-minutes, "minute");
|
|
2529
|
+
if (hours < 24) return rtf.format(-hours, "hour");
|
|
2530
|
+
if (days <= 30) return rtf.format(-days, "day");
|
|
2531
|
+
}
|
|
2532
|
+
return absoluteTime(then, locale);
|
|
2533
|
+
}
|
|
2534
|
+
function absoluteTime(at, locale) {
|
|
2535
|
+
try {
|
|
2536
|
+
return new Date(at).toLocaleString(locale);
|
|
2537
|
+
} catch {
|
|
2538
|
+
return new Date(at).toLocaleString();
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
function pageLocale(doc, labelsLocale) {
|
|
2542
|
+
if (labelsLocale) return labelsLocale;
|
|
2543
|
+
return doc.documentElement?.lang || void 0;
|
|
2544
|
+
}
|
|
2545
|
+
const ERROR_KEYS = {
|
|
2546
|
+
conflict: "connection.error.conflict",
|
|
2547
|
+
rejected: "connection.error.rejected",
|
|
2548
|
+
auth: "connection.error.auth",
|
|
2549
|
+
server: "connection.error.server",
|
|
2550
|
+
network: "connection.error.network",
|
|
2551
|
+
timeout: "connection.error.timeout",
|
|
2552
|
+
dependency: "connection.error.dependency"
|
|
2553
|
+
};
|
|
2554
|
+
var PendingChangesDrawerComponent = class PendingChangesDrawerComponent {
|
|
2555
|
+
constructor() {
|
|
2556
|
+
this.labels = inject(UiLabelsService);
|
|
2557
|
+
this.support = inject(OfflineSupportService);
|
|
2558
|
+
this.confirmService = inject(ConfirmService);
|
|
2559
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
2560
|
+
this.host = inject(ElementRef);
|
|
2561
|
+
this.doc = inject(DOCUMENT);
|
|
2562
|
+
this.open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
2563
|
+
this.closed = output();
|
|
2564
|
+
this.rows = computed(() => this.support.rows(), ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
2565
|
+
this.announcement = signal("", ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
|
|
2566
|
+
this.now = signal(Date.now(), ...ngDevMode ? [{ debugName: "now" }] : /* istanbul ignore next */ []);
|
|
2567
|
+
const timer = setInterval(() => this.now.set(Date.now()), 3e4);
|
|
2568
|
+
inject(DestroyRef).onDestroy(() => clearInterval(timer));
|
|
2569
|
+
}
|
|
2570
|
+
statusText(row) {
|
|
2571
|
+
switch (row.status) {
|
|
2572
|
+
case "syncing": return this.labels.t("connection.status.syncing");
|
|
2573
|
+
case "failed": return this.labels.t("connection.status.failed");
|
|
2574
|
+
case "conflict": return this.labels.t("connection.status.conflict");
|
|
2575
|
+
default: return this.labels.t("connection.status.pending");
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
errorText(row) {
|
|
2579
|
+
const kind = row.lastError?.kind ?? (row.status === "conflict" ? "conflict" : void 0);
|
|
2580
|
+
if (kind && kind in ERROR_KEYS) return this.labels.t(ERROR_KEYS[kind]);
|
|
2581
|
+
if (row.status === "failed" || row.lastError) return this.labels.t("connection.error.unknown");
|
|
2582
|
+
return "";
|
|
2583
|
+
}
|
|
2584
|
+
canRetry(row) {
|
|
2585
|
+
return row.status === "failed" || row.status === "conflict" || row.status === "pending" && !!row.lastError;
|
|
2586
|
+
}
|
|
2587
|
+
iso(row) {
|
|
2588
|
+
return new Date(row.createdAt).toISOString();
|
|
2589
|
+
}
|
|
2590
|
+
absolute(row) {
|
|
2591
|
+
return absoluteTime(row.createdAt, pageLocale(this.doc, this.labels.locale()));
|
|
2592
|
+
}
|
|
2593
|
+
savedText(row) {
|
|
2594
|
+
const time = formatRelativeTime(row.createdAt, this.now(), this.labels.t("connection.time.justNow"), pageLocale(this.doc, this.labels.locale()));
|
|
2595
|
+
return this.labels.t("connection.savedAt", { time });
|
|
2596
|
+
}
|
|
2597
|
+
async retry(row) {
|
|
2598
|
+
this.announcement.set(this.labels.t("connection.result.retrying", { label: row.label }));
|
|
2599
|
+
try {
|
|
2600
|
+
await this.support.retry(row.id);
|
|
2601
|
+
} catch {
|
|
2602
|
+
this.announceFailure();
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
async syncAll() {
|
|
2606
|
+
this.announcement.set(this.labels.t("connection.result.syncStarted"));
|
|
2607
|
+
try {
|
|
2608
|
+
await this.support.syncNow();
|
|
2609
|
+
} catch {
|
|
2610
|
+
this.announceFailure();
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
async discard(row) {
|
|
2614
|
+
const index = this.rows().findIndex((r) => r.id === row.id);
|
|
2615
|
+
const all = this.rows();
|
|
2616
|
+
const dependentIds = /* @__PURE__ */ new Set();
|
|
2617
|
+
const pending = [row.id];
|
|
2618
|
+
while (pending.length > 0) {
|
|
2619
|
+
const current = pending.pop();
|
|
2620
|
+
for (const r of all) if (r.id !== row.id && !dependentIds.has(r.id) && (r.dependsOn ?? []).includes(current)) {
|
|
2621
|
+
dependentIds.add(r.id);
|
|
2622
|
+
pending.push(r.id);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const dependents = dependentIds.size;
|
|
2626
|
+
const message = dependents === 0 ? this.labels.t("connection.discard.message", { label: row.label }) : dependents === 1 ? this.labels.t("connection.discard.dependents.one", { label: row.label }) : this.labels.t("connection.discard.dependents.other", {
|
|
2627
|
+
label: row.label,
|
|
2628
|
+
count: dependents
|
|
2629
|
+
});
|
|
2630
|
+
if (!await this.confirmService.confirm({
|
|
2631
|
+
title: this.labels.t("connection.discard.title"),
|
|
2632
|
+
message,
|
|
2633
|
+
confirmLabel: this.labels.t("connection.discard"),
|
|
2634
|
+
cancelLabel: this.labels.t("toolbar.cancel"),
|
|
2635
|
+
variant: "danger"
|
|
2636
|
+
})) return;
|
|
2637
|
+
try {
|
|
2638
|
+
await this.support.discard(row.id);
|
|
2639
|
+
} catch {
|
|
2640
|
+
this.announceFailure();
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
this.announcement.set(this.labels.t("connection.result.discarded", { label: row.label }));
|
|
2644
|
+
this.cdr.detectChanges();
|
|
2645
|
+
this.focusAfterRemoval(index);
|
|
2646
|
+
}
|
|
2647
|
+
async discardAll() {
|
|
2648
|
+
const count = this.rows().length;
|
|
2649
|
+
if (count === 0) return;
|
|
2650
|
+
if (!await this.confirmService.confirm({
|
|
2651
|
+
title: this.labels.t("connection.discardAll.title"),
|
|
2652
|
+
message: this.labels.t(count === 1 ? "connection.discardAll.message.one" : "connection.discardAll.message.other", { count }),
|
|
2653
|
+
confirmLabel: this.labels.t("connection.discardAll"),
|
|
2654
|
+
cancelLabel: this.labels.t("toolbar.cancel"),
|
|
2655
|
+
variant: "danger"
|
|
2656
|
+
})) return;
|
|
2657
|
+
try {
|
|
2658
|
+
await this.support.discardAll();
|
|
2659
|
+
} catch {
|
|
2660
|
+
this.announceFailure();
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
this.announcement.set(this.labels.t("connection.result.discardedAll"));
|
|
2664
|
+
this.cdr.detectChanges();
|
|
2665
|
+
this.focusAfterRemoval(0);
|
|
2666
|
+
}
|
|
2667
|
+
announceFailure() {
|
|
2668
|
+
this.announcement.set(this.labels.t("connection.error.unknown"));
|
|
2669
|
+
}
|
|
2670
|
+
focusAfterRemoval(index) {
|
|
2671
|
+
const root = this.host.nativeElement;
|
|
2672
|
+
const rowEls = Array.from(root.querySelectorAll("li.pcd-row"));
|
|
2673
|
+
(rowEls[Math.min(index, rowEls.length - 1)]?.querySelector("button:not([disabled])") ?? root.querySelector(".pcd-empty"))?.focus();
|
|
2674
|
+
}
|
|
2675
|
+
static {
|
|
2676
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
2677
|
+
minVersion: "12.0.0",
|
|
2678
|
+
version: "22.2.2",
|
|
2679
|
+
ngImport: i0,
|
|
2680
|
+
type: PendingChangesDrawerComponent,
|
|
2681
|
+
deps: [],
|
|
2682
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
static {
|
|
2686
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2687
|
+
minVersion: "17.0.0",
|
|
2688
|
+
version: "22.2.2",
|
|
2689
|
+
type: PendingChangesDrawerComponent,
|
|
2690
|
+
isStandalone: true,
|
|
2691
|
+
selector: "ui-pending-changes-drawer",
|
|
2692
|
+
inputs: { open: {
|
|
2693
|
+
classPropertyName: "open",
|
|
2694
|
+
publicName: "open",
|
|
2695
|
+
isSignal: true,
|
|
2696
|
+
isRequired: false,
|
|
2697
|
+
transformFunction: null
|
|
2698
|
+
} },
|
|
2699
|
+
outputs: { closed: "closed" },
|
|
2700
|
+
ngImport: i0,
|
|
2701
|
+
template: `
|
|
2702
|
+
<div class="pcd-live" role="status" aria-live="polite" aria-atomic="true" data-testid="pending-drawer-live">{{ announcement() }}</div>
|
|
2703
|
+
<ui-side-panel
|
|
2704
|
+
[open]="open()"
|
|
2705
|
+
[title]="labels.t('connection.drawer.title')"
|
|
2706
|
+
[cancelLabel]="labels.t('connection.drawer.close')"
|
|
2707
|
+
[primaryLabel]="labels.t('connection.syncAll')"
|
|
2708
|
+
[primaryDisabled]="rows().length === 0 || support.syncing()"
|
|
2709
|
+
[primaryLoading]="support.syncing()"
|
|
2710
|
+
primaryTestId="pending-drawer-sync-all"
|
|
2711
|
+
(closed)="closed.emit()"
|
|
2712
|
+
(primaryClick)="syncAll()">
|
|
2713
|
+
@if (support.authBlocked()) {
|
|
2714
|
+
<p class="pcd-auth" data-testid="pending-drawer-auth">{{ labels.t('connection.authBlocked') }}</p>
|
|
2715
|
+
}
|
|
2716
|
+
@if (rows().length === 0) {
|
|
2717
|
+
<p class="pcd-empty" tabindex="-1" data-testid="pending-drawer-empty">{{ support.storageAvailable() ? labels.t('connection.drawer.empty') : labels.t('connection.storageUnavailable') }}</p>
|
|
2718
|
+
} @else {
|
|
2719
|
+
<ul class="pcd-list" [attr.aria-label]="labels.t('connection.drawer.list')" data-testid="pending-drawer-list">
|
|
2720
|
+
@for (row of rows(); track row.id) {
|
|
2721
|
+
<li class="pcd-row" [attr.data-testid]="'pending-row-' + row.id" [attr.data-status]="row.status">
|
|
2722
|
+
<div class="pcd-main">
|
|
2723
|
+
<span class="pcd-label">{{ row.label }}</span>
|
|
2724
|
+
<span class="pcd-status" [attr.data-testid]="'pending-row-status-' + row.id">{{ statusText(row) }}</span>
|
|
2725
|
+
</div>
|
|
2726
|
+
<time class="pcd-time" [attr.datetime]="iso(row)" [attr.title]="absolute(row)" [attr.data-testid]="'pending-row-time-' + row.id">{{ savedText(row) }}</time>
|
|
2727
|
+
@if (errorText(row); as err) {
|
|
2728
|
+
<p class="pcd-error" [attr.data-testid]="'pending-row-error-' + row.id">{{ err }}</p>
|
|
2729
|
+
}
|
|
2730
|
+
<div class="pcd-actions">
|
|
2731
|
+
@if (canRetry(row)) {
|
|
2732
|
+
<button type="button" class="pcd-btn" [attr.data-testid]="'pending-row-retry-' + row.id"
|
|
2733
|
+
[attr.aria-label]="labels.t('connection.retryNamed', { label: row.label })" (click)="retry(row)">{{ labels.t('connection.retry') }}</button>
|
|
2734
|
+
}
|
|
2735
|
+
<button type="button" class="pcd-btn pcd-btn-danger" [attr.data-testid]="'pending-row-discard-' + row.id" [disabled]="row.status === 'syncing'"
|
|
2736
|
+
[attr.aria-label]="labels.t('connection.discardNamed', { label: row.label })" (click)="discard(row)">{{ labels.t('connection.discard') }}</button>
|
|
2737
|
+
</div>
|
|
2738
|
+
</li>
|
|
2739
|
+
}
|
|
2740
|
+
</ul>
|
|
2741
|
+
}
|
|
2742
|
+
<button panel-footer-extra type="button" class="pcd-btn pcd-btn-danger" data-testid="pending-drawer-discard-all"
|
|
2743
|
+
[disabled]="rows().length === 0 || support.syncing()" (click)="discardAll()">{{ labels.t('connection.discardAll') }}</button>
|
|
2744
|
+
</ui-side-panel>
|
|
2745
|
+
`,
|
|
2746
|
+
isInline: true,
|
|
2747
|
+
styles: [":host{display:contents}.pcd-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.pcd-auth{margin:0 0 16px;padding:12px 14px;border-radius:8px;border:1px solid #b91c1c;background:#fee2e2;color:#7f1d1d;font-size:.875rem}.pcd-empty{margin:0;padding:24px 0;color:#475569;font-size:.9375rem;text-align:center}.pcd-empty:focus{outline:none}.pcd-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.pcd-row{padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;display:flex;flex-direction:column;gap:6px}.pcd-main{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 12px}.pcd-label{font-weight:600;color:#0f172a;overflow-wrap:anywhere;min-inline-size:0}.pcd-status{font-size:.8125rem;color:#334155}.pcd-time{font-size:.8125rem;color:#475569}.pcd-error{margin:0;font-size:.875rem;color:#7f1d1d}.pcd-row[data-status=conflict] .pcd-error,.pcd-row[data-status=failed] .pcd-error{color:#78350f}.pcd-actions{display:flex;flex-wrap:wrap;gap:8px;margin-block-start:4px}.pcd-btn{min-block-size:44px;min-inline-size:44px;padding:0 16px;border-radius:8px;border:1px solid #334155;background:#fff;color:#0f172a;font:inherit;font-size:.875rem;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.pcd-btn-danger{border-color:#b91c1c;color:#b91c1c}.pcd-btn:disabled{opacity:.5;cursor:not-allowed}.pcd-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.pcd-btn:focus:not(:focus-visible){outline:none}@media(max-width:480px){.pcd-actions{flex-direction:column}.pcd-btn{inline-size:100%}}@media(forced-colors:active){.pcd-row,.pcd-auth,.pcd-btn{border:1px solid CanvasText;background:Canvas;color:CanvasText}}\n"],
|
|
2748
|
+
dependencies: [{
|
|
2749
|
+
kind: "component",
|
|
2750
|
+
type: SidePanelComponent,
|
|
2751
|
+
selector: "ui-side-panel",
|
|
2752
|
+
inputs: [
|
|
2753
|
+
"open",
|
|
2754
|
+
"title",
|
|
2755
|
+
"subtitle",
|
|
2756
|
+
"wide",
|
|
2757
|
+
"extraWide",
|
|
2758
|
+
"cancelLabel",
|
|
2759
|
+
"primaryLabel",
|
|
2760
|
+
"primaryVariant",
|
|
2761
|
+
"primaryLoading",
|
|
2762
|
+
"primaryDisabled",
|
|
2763
|
+
"cancelTestId",
|
|
2764
|
+
"primaryTestId",
|
|
2765
|
+
"returnFocusTo"
|
|
2766
|
+
],
|
|
2767
|
+
outputs: ["closed", "primaryClick"]
|
|
2768
|
+
}],
|
|
2769
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2774
|
+
minVersion: "12.0.0",
|
|
2775
|
+
version: "22.2.2",
|
|
2776
|
+
ngImport: i0,
|
|
2777
|
+
type: PendingChangesDrawerComponent,
|
|
2778
|
+
decorators: [{
|
|
2779
|
+
type: Component,
|
|
2780
|
+
args: [{
|
|
2781
|
+
selector: "ui-pending-changes-drawer",
|
|
2782
|
+
standalone: true,
|
|
2783
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2784
|
+
imports: [SidePanelComponent],
|
|
2785
|
+
template: `
|
|
2786
|
+
<div class="pcd-live" role="status" aria-live="polite" aria-atomic="true" data-testid="pending-drawer-live">{{ announcement() }}</div>
|
|
2787
|
+
<ui-side-panel
|
|
2788
|
+
[open]="open()"
|
|
2789
|
+
[title]="labels.t('connection.drawer.title')"
|
|
2790
|
+
[cancelLabel]="labels.t('connection.drawer.close')"
|
|
2791
|
+
[primaryLabel]="labels.t('connection.syncAll')"
|
|
2792
|
+
[primaryDisabled]="rows().length === 0 || support.syncing()"
|
|
2793
|
+
[primaryLoading]="support.syncing()"
|
|
2794
|
+
primaryTestId="pending-drawer-sync-all"
|
|
2795
|
+
(closed)="closed.emit()"
|
|
2796
|
+
(primaryClick)="syncAll()">
|
|
2797
|
+
@if (support.authBlocked()) {
|
|
2798
|
+
<p class="pcd-auth" data-testid="pending-drawer-auth">{{ labels.t('connection.authBlocked') }}</p>
|
|
2799
|
+
}
|
|
2800
|
+
@if (rows().length === 0) {
|
|
2801
|
+
<p class="pcd-empty" tabindex="-1" data-testid="pending-drawer-empty">{{ support.storageAvailable() ? labels.t('connection.drawer.empty') : labels.t('connection.storageUnavailable') }}</p>
|
|
2802
|
+
} @else {
|
|
2803
|
+
<ul class="pcd-list" [attr.aria-label]="labels.t('connection.drawer.list')" data-testid="pending-drawer-list">
|
|
2804
|
+
@for (row of rows(); track row.id) {
|
|
2805
|
+
<li class="pcd-row" [attr.data-testid]="'pending-row-' + row.id" [attr.data-status]="row.status">
|
|
2806
|
+
<div class="pcd-main">
|
|
2807
|
+
<span class="pcd-label">{{ row.label }}</span>
|
|
2808
|
+
<span class="pcd-status" [attr.data-testid]="'pending-row-status-' + row.id">{{ statusText(row) }}</span>
|
|
2809
|
+
</div>
|
|
2810
|
+
<time class="pcd-time" [attr.datetime]="iso(row)" [attr.title]="absolute(row)" [attr.data-testid]="'pending-row-time-' + row.id">{{ savedText(row) }}</time>
|
|
2811
|
+
@if (errorText(row); as err) {
|
|
2812
|
+
<p class="pcd-error" [attr.data-testid]="'pending-row-error-' + row.id">{{ err }}</p>
|
|
2813
|
+
}
|
|
2814
|
+
<div class="pcd-actions">
|
|
2815
|
+
@if (canRetry(row)) {
|
|
2816
|
+
<button type="button" class="pcd-btn" [attr.data-testid]="'pending-row-retry-' + row.id"
|
|
2817
|
+
[attr.aria-label]="labels.t('connection.retryNamed', { label: row.label })" (click)="retry(row)">{{ labels.t('connection.retry') }}</button>
|
|
2818
|
+
}
|
|
2819
|
+
<button type="button" class="pcd-btn pcd-btn-danger" [attr.data-testid]="'pending-row-discard-' + row.id" [disabled]="row.status === 'syncing'"
|
|
2820
|
+
[attr.aria-label]="labels.t('connection.discardNamed', { label: row.label })" (click)="discard(row)">{{ labels.t('connection.discard') }}</button>
|
|
2821
|
+
</div>
|
|
2822
|
+
</li>
|
|
2823
|
+
}
|
|
2824
|
+
</ul>
|
|
2825
|
+
}
|
|
2826
|
+
<button panel-footer-extra type="button" class="pcd-btn pcd-btn-danger" data-testid="pending-drawer-discard-all"
|
|
2827
|
+
[disabled]="rows().length === 0 || support.syncing()" (click)="discardAll()">{{ labels.t('connection.discardAll') }}</button>
|
|
2828
|
+
</ui-side-panel>
|
|
2829
|
+
`,
|
|
2830
|
+
styles: [":host{display:contents}.pcd-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.pcd-auth{margin:0 0 16px;padding:12px 14px;border-radius:8px;border:1px solid #b91c1c;background:#fee2e2;color:#7f1d1d;font-size:.875rem}.pcd-empty{margin:0;padding:24px 0;color:#475569;font-size:.9375rem;text-align:center}.pcd-empty:focus{outline:none}.pcd-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.pcd-row{padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;display:flex;flex-direction:column;gap:6px}.pcd-main{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 12px}.pcd-label{font-weight:600;color:#0f172a;overflow-wrap:anywhere;min-inline-size:0}.pcd-status{font-size:.8125rem;color:#334155}.pcd-time{font-size:.8125rem;color:#475569}.pcd-error{margin:0;font-size:.875rem;color:#7f1d1d}.pcd-row[data-status=conflict] .pcd-error,.pcd-row[data-status=failed] .pcd-error{color:#78350f}.pcd-actions{display:flex;flex-wrap:wrap;gap:8px;margin-block-start:4px}.pcd-btn{min-block-size:44px;min-inline-size:44px;padding:0 16px;border-radius:8px;border:1px solid #334155;background:#fff;color:#0f172a;font:inherit;font-size:.875rem;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.pcd-btn-danger{border-color:#b91c1c;color:#b91c1c}.pcd-btn:disabled{opacity:.5;cursor:not-allowed}.pcd-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.pcd-btn:focus:not(:focus-visible){outline:none}@media(max-width:480px){.pcd-actions{flex-direction:column}.pcd-btn{inline-size:100%}}@media(forced-colors:active){.pcd-row,.pcd-auth,.pcd-btn{border:1px solid CanvasText;background:Canvas;color:CanvasText}}\n"]
|
|
2831
|
+
}]
|
|
2832
|
+
}],
|
|
2833
|
+
ctorParameters: () => [],
|
|
2834
|
+
propDecorators: {
|
|
2835
|
+
open: [{
|
|
2836
|
+
type: i0.Input,
|
|
2837
|
+
args: [{
|
|
2838
|
+
isSignal: true,
|
|
2839
|
+
alias: "open",
|
|
2840
|
+
required: false
|
|
2841
|
+
}]
|
|
2842
|
+
}],
|
|
2843
|
+
closed: [{
|
|
2844
|
+
type: i0.Output,
|
|
2845
|
+
args: ["closed"]
|
|
2846
|
+
}]
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
const TIME_MARK = "";
|
|
2850
|
+
var StaleDataChipComponent = class StaleDataChipComponent {
|
|
2851
|
+
constructor() {
|
|
2852
|
+
this.labels = inject(UiLabelsService);
|
|
2853
|
+
this.stale = inject(StaleDataService);
|
|
2854
|
+
this.doc = inject(DOCUMENT);
|
|
2855
|
+
this.refresh = output();
|
|
2856
|
+
this.now = signal(Date.now(), ...ngDevMode ? [{ debugName: "now" }] : /* istanbul ignore next */ []);
|
|
2857
|
+
this.parts = computed(() => {
|
|
2858
|
+
const at = this.stale.staleAt();
|
|
2859
|
+
if (at == null) return null;
|
|
2860
|
+
const locale = pageLocale(this.doc, this.labels.locale());
|
|
2861
|
+
const relative = formatRelativeTime(at, this.now(), this.labels.t("connection.time.justNow"), locale);
|
|
2862
|
+
const text = this.labels.t("connection.stale", { time: TIME_MARK });
|
|
2863
|
+
const at0 = text.indexOf(TIME_MARK);
|
|
2864
|
+
return {
|
|
2865
|
+
before: at0 < 0 ? `${text} ` : text.slice(0, at0),
|
|
2866
|
+
after: at0 < 0 ? "" : text.slice(at0 + 1),
|
|
2867
|
+
relative,
|
|
2868
|
+
iso: new Date(at).toISOString(),
|
|
2869
|
+
absolute: absoluteTime(at, locale)
|
|
2870
|
+
};
|
|
2871
|
+
}, ...ngDevMode ? [{ debugName: "parts" }] : /* istanbul ignore next */ []);
|
|
2872
|
+
this.announcement = computed(() => {
|
|
2873
|
+
const p = this.parts();
|
|
2874
|
+
return p ? `${p.before}${p.relative}${p.after}` : "";
|
|
2875
|
+
}, ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
|
|
2876
|
+
const timer = setInterval(() => this.now.set(Date.now()), 3e4);
|
|
2877
|
+
inject(DestroyRef).onDestroy(() => clearInterval(timer));
|
|
2878
|
+
}
|
|
2879
|
+
static {
|
|
2880
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
2881
|
+
minVersion: "12.0.0",
|
|
2882
|
+
version: "22.2.2",
|
|
2883
|
+
ngImport: i0,
|
|
2884
|
+
type: StaleDataChipComponent,
|
|
2885
|
+
deps: [],
|
|
2886
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
static {
|
|
2890
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2891
|
+
minVersion: "17.0.0",
|
|
2892
|
+
version: "22.2.2",
|
|
2893
|
+
type: StaleDataChipComponent,
|
|
2894
|
+
isStandalone: true,
|
|
2895
|
+
selector: "ui-stale-data-chip",
|
|
2896
|
+
outputs: { refresh: "refresh" },
|
|
2897
|
+
ngImport: i0,
|
|
2898
|
+
template: `
|
|
2899
|
+
<div class="sdc-live" role="status" aria-live="polite" aria-atomic="true" data-testid="stale-data-chip-live">{{ announcement() }}</div>
|
|
2900
|
+
@if (parts(); as p) {
|
|
2901
|
+
<div class="sdc" data-testid="stale-data-chip">
|
|
2902
|
+
<span class="sdc-text">{{ p.before }}<time [attr.datetime]="p.iso" [attr.title]="p.absolute" data-testid="stale-data-chip-time">{{ p.relative }}</time>{{ p.after }}</span>
|
|
2903
|
+
<button type="button" class="sdc-btn" data-testid="stale-data-chip-refresh" [attr.aria-label]="labels.t('connection.refreshAria')" (click)="refresh.emit()">{{ labels.t('connection.refresh') }}</button>
|
|
2904
|
+
</div>
|
|
2905
|
+
}
|
|
2906
|
+
`,
|
|
2907
|
+
isInline: true,
|
|
2908
|
+
styles: [":host{display:block;position:relative}.sdc-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.sdc{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;padding:6px 8px 6px 14px;border-radius:999px;border:1px solid #b45309;background:#fef3c7;color:#78350f;font-size:.8125rem;line-height:1.4;max-inline-size:100%;box-sizing:border-box}.sdc-text{min-inline-size:0;overflow-wrap:anywhere}.sdc-btn{min-block-size:44px;min-inline-size:44px;padding:0 14px;border-radius:999px;border:1px solid currentColor;background:#fff;color:inherit;font:inherit;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.sdc-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.sdc-btn:focus:not(:focus-visible){outline:none}@media(max-width:480px){.sdc{border-radius:12px}.sdc-btn{inline-size:100%}}@media(forced-colors:active){.sdc,.sdc-btn{border:1px solid CanvasText;background:Canvas;color:CanvasText}}\n"],
|
|
2909
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2914
|
+
minVersion: "12.0.0",
|
|
2915
|
+
version: "22.2.2",
|
|
2916
|
+
ngImport: i0,
|
|
2917
|
+
type: StaleDataChipComponent,
|
|
2918
|
+
decorators: [{
|
|
2919
|
+
type: Component,
|
|
2920
|
+
args: [{
|
|
2921
|
+
selector: "ui-stale-data-chip",
|
|
2922
|
+
standalone: true,
|
|
2923
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2924
|
+
template: `
|
|
2925
|
+
<div class="sdc-live" role="status" aria-live="polite" aria-atomic="true" data-testid="stale-data-chip-live">{{ announcement() }}</div>
|
|
2926
|
+
@if (parts(); as p) {
|
|
2927
|
+
<div class="sdc" data-testid="stale-data-chip">
|
|
2928
|
+
<span class="sdc-text">{{ p.before }}<time [attr.datetime]="p.iso" [attr.title]="p.absolute" data-testid="stale-data-chip-time">{{ p.relative }}</time>{{ p.after }}</span>
|
|
2929
|
+
<button type="button" class="sdc-btn" data-testid="stale-data-chip-refresh" [attr.aria-label]="labels.t('connection.refreshAria')" (click)="refresh.emit()">{{ labels.t('connection.refresh') }}</button>
|
|
2930
|
+
</div>
|
|
2931
|
+
}
|
|
2932
|
+
`,
|
|
2933
|
+
styles: [":host{display:block;position:relative}.sdc-live{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.sdc{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;padding:6px 8px 6px 14px;border-radius:999px;border:1px solid #b45309;background:#fef3c7;color:#78350f;font-size:.8125rem;line-height:1.4;max-inline-size:100%;box-sizing:border-box}.sdc-text{min-inline-size:0;overflow-wrap:anywhere}.sdc-btn{min-block-size:44px;min-inline-size:44px;padding:0 14px;border-radius:999px;border:1px solid currentColor;background:#fff;color:inherit;font:inherit;font-weight:600;cursor:pointer;-webkit-appearance:none;appearance:none}.sdc-btn:focus{outline:3px solid #1d4ed8;outline-offset:2px}.sdc-btn:focus:not(:focus-visible){outline:none}@media(max-width:480px){.sdc{border-radius:12px}.sdc-btn{inline-size:100%}}@media(forced-colors:active){.sdc,.sdc-btn{border:1px solid CanvasText;background:Canvas;color:CanvasText}}\n"]
|
|
2934
|
+
}]
|
|
2935
|
+
}],
|
|
2936
|
+
ctorParameters: () => [],
|
|
2937
|
+
propDecorators: { refresh: [{
|
|
2938
|
+
type: i0.Output,
|
|
2939
|
+
args: ["refresh"]
|
|
2940
|
+
}] }
|
|
2941
|
+
});
|
|
2942
|
+
var StillWorkingComponent = class StillWorkingComponent {
|
|
2943
|
+
constructor() {
|
|
2944
|
+
this.labels = inject(UiLabelsService);
|
|
2945
|
+
this.active = input(false, ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
|
|
2946
|
+
this.delayMs = input(4e3, ...ngDevMode ? [{ debugName: "delayMs" }] : /* istanbul ignore next */ []);
|
|
2947
|
+
this.elapsed = signal(false, ...ngDevMode ? [{ debugName: "elapsed" }] : /* istanbul ignore next */ []);
|
|
2948
|
+
this.text = computed(() => this.active() && this.elapsed() ? this.labels.t("connection.stillWorking") : "", ...ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []);
|
|
2949
|
+
effect((onCleanup) => {
|
|
2950
|
+
const active = this.active();
|
|
2951
|
+
const delay = this.delayMs();
|
|
2952
|
+
untracked(() => this.elapsed.set(false));
|
|
2953
|
+
if (!active) return;
|
|
2954
|
+
const timer = setTimeout(() => this.elapsed.set(true), Math.max(0, delay));
|
|
2955
|
+
onCleanup(() => clearTimeout(timer));
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
static {
|
|
2959
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
2960
|
+
minVersion: "12.0.0",
|
|
2961
|
+
version: "22.2.2",
|
|
2962
|
+
ngImport: i0,
|
|
2963
|
+
type: StillWorkingComponent,
|
|
2964
|
+
deps: [],
|
|
2965
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
static {
|
|
2969
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2970
|
+
minVersion: "17.1.0",
|
|
2971
|
+
version: "22.2.2",
|
|
2972
|
+
type: StillWorkingComponent,
|
|
2973
|
+
isStandalone: true,
|
|
2974
|
+
selector: "ui-still-working",
|
|
2975
|
+
inputs: {
|
|
2976
|
+
active: {
|
|
2977
|
+
classPropertyName: "active",
|
|
2978
|
+
publicName: "active",
|
|
2979
|
+
isSignal: true,
|
|
2980
|
+
isRequired: false,
|
|
2981
|
+
transformFunction: null
|
|
2982
|
+
},
|
|
2983
|
+
delayMs: {
|
|
2984
|
+
classPropertyName: "delayMs",
|
|
2985
|
+
publicName: "delayMs",
|
|
2986
|
+
isSignal: true,
|
|
2987
|
+
isRequired: false,
|
|
2988
|
+
transformFunction: null
|
|
2989
|
+
}
|
|
2990
|
+
},
|
|
2991
|
+
ngImport: i0,
|
|
2992
|
+
template: `<div class="sw" role="status" aria-live="polite" aria-atomic="true" data-testid="still-working">{{ text() }}</div>`,
|
|
2993
|
+
isInline: true,
|
|
2994
|
+
styles: [":host{display:block}.sw{color:#475569;font-size:.8125rem;line-height:1.4}.sw:empty{min-block-size:0;margin:0;padding:0}@media(forced-colors:active){.sw{color:CanvasText}}\n"],
|
|
2995
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
3000
|
+
minVersion: "12.0.0",
|
|
3001
|
+
version: "22.2.2",
|
|
3002
|
+
ngImport: i0,
|
|
3003
|
+
type: StillWorkingComponent,
|
|
3004
|
+
decorators: [{
|
|
3005
|
+
type: Component,
|
|
3006
|
+
args: [{
|
|
3007
|
+
selector: "ui-still-working",
|
|
3008
|
+
standalone: true,
|
|
3009
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3010
|
+
template: `<div class="sw" role="status" aria-live="polite" aria-atomic="true" data-testid="still-working">{{ text() }}</div>`,
|
|
3011
|
+
styles: [":host{display:block}.sw{color:#475569;font-size:.8125rem;line-height:1.4}.sw:empty{min-block-size:0;margin:0;padding:0}@media(forced-colors:active){.sw{color:CanvasText}}\n"]
|
|
3012
|
+
}]
|
|
3013
|
+
}],
|
|
3014
|
+
ctorParameters: () => [],
|
|
3015
|
+
propDecorators: {
|
|
3016
|
+
active: [{
|
|
3017
|
+
type: i0.Input,
|
|
3018
|
+
args: [{
|
|
3019
|
+
isSignal: true,
|
|
3020
|
+
alias: "active",
|
|
3021
|
+
required: false
|
|
3022
|
+
}]
|
|
3023
|
+
}],
|
|
3024
|
+
delayMs: [{
|
|
3025
|
+
type: i0.Input,
|
|
3026
|
+
args: [{
|
|
3027
|
+
isSignal: true,
|
|
3028
|
+
alias: "delayMs",
|
|
3029
|
+
required: false
|
|
3030
|
+
}]
|
|
3031
|
+
}]
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
export { ConnectionBannerComponent, ConnectionStateService, DEFAULT_DETAIL_TTL_MS, DEFAULT_LIST_TTL_MS, FROM_CACHE_HEADER, IDEMPOTENCY_HEADER, IDMAP_TTL_MS, LEASE_TTL_MS, OFFLINE_IDB_FACTORY, OFFLINE_LEASE_STORAGE, OFFLINE_LOCK_MANAGER, OFFLINE_REPLAY_HEADER, OFFLINE_SUPPORT_CONFIG, ONLINE_DEBOUNCE_MS, OfflineBlockedError, OfflineReplayer, OfflineStore, OfflineSupportService, PendingChangesDrawerComponent, QueueFullError, REQUEST_TIMEOUT_MS, RequestTimeoutError, STALE_CLAIM_MS, SYNCED_FLASH_MS, StaleDataChipComponent, StaleDataService, StillWorkingComponent, StorageUnavailableError, backoffMs, classifyReplay, connectionInterceptor, createChannel, defaultCreatedId, deriveEntity, generateUuid, idempotencyInterceptor, invalidationPrefix, isClaimable, isInScope, isMutationMethod, isOfflineBlockedError, isQuotaError, isRequestTimeoutError, lastSegmentIsId, matchesAny, matchesUrl, offlineHttpInterceptors, offlineInterceptor, pathAndQuery, pathOnly, provideOfflineSupport, resolveConfig, resolveConfigCached };
|
|
3035
|
+
|
|
3036
|
+
//# sourceMappingURL=forgeroute-ui-offline.mjs.map
|