@burdenoff/fe-libs 2026.813.1 → 2026.813.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shared/events/sinks/RybbitSink.d.ts +5 -0
- package/dist/shared/events/sinks/RybbitSink.d.ts.map +1 -1
- package/dist/shared/events/sinks/RybbitSink.js +93 -38
- package/dist/shared/events/sinks/index.d.ts +1 -1
- package/dist/shared/events/sinks/index.d.ts.map +1 -1
- package/dist/shared/providers/AppShellProvider.d.ts +2 -0
- package/dist/shared/providers/AppShellProvider.d.ts.map +1 -1
- package/dist/shared/providers/AppShellProvider.js +83 -78
- package/dist/shared-events.js +29 -29
- package/dist/shared.js +55 -55
- package/dist/storage/SaveToFilesDialog.d.ts.map +1 -1
- package/dist/storage/SaveToFilesDialog.js +16 -1
- package/dist/storage/index.d.ts +4 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +7 -5
- package/dist/storage/retryPolicy.d.ts +179 -0
- package/dist/storage/retryPolicy.d.ts.map +1 -0
- package/dist/storage/retryPolicy.js +150 -0
- package/dist/storage/uploadBlob.d.ts +20 -11
- package/dist/storage/uploadBlob.d.ts.map +1 -1
- package/dist/storage/uploadBlob.js +69 -66
- package/dist/storage/useSaveToFiles.d.ts +29 -1
- package/dist/storage/useSaveToFiles.d.ts.map +1 -1
- package/dist/storage/useSaveToFiles.js +66 -44
- package/dist/storage/warmFilesService.d.ts +33 -0
- package/dist/storage/warmFilesService.d.ts.map +1 -0
- package/dist/storage/warmFilesService.js +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region src/storage/retryPolicy.ts
|
|
2
|
+
var e = 3, t = [1e4, 2e4], n = 75e3, r = [400, 800], i = new Set([
|
|
3
|
+
"OPERATION_TIMEOUT",
|
|
4
|
+
"GATEWAY_TIMEOUT",
|
|
5
|
+
"REQUEST_TIMEOUT",
|
|
6
|
+
"TIMEOUT"
|
|
7
|
+
]), a = new Set([
|
|
8
|
+
"QUOTA_NOT_PROVISIONED",
|
|
9
|
+
"QUOTA_EXHAUSTED",
|
|
10
|
+
"QUOTA_EXCEEDED",
|
|
11
|
+
"FORBIDDEN",
|
|
12
|
+
"UNAUTHENTICATED",
|
|
13
|
+
"UNAUTHORIZED",
|
|
14
|
+
"BAD_USER_INPUT",
|
|
15
|
+
"BAD_REQUEST",
|
|
16
|
+
"GRAPHQL_VALIDATION_FAILED",
|
|
17
|
+
"GRAPHQL_PARSE_FAILED",
|
|
18
|
+
"PERSISTED_QUERY_NOT_FOUND",
|
|
19
|
+
"NOT_FOUND",
|
|
20
|
+
"CONFLICT",
|
|
21
|
+
"PAYLOAD_TOO_LARGE"
|
|
22
|
+
]), o = new Set([
|
|
23
|
+
408,
|
|
24
|
+
504,
|
|
25
|
+
524
|
|
26
|
+
]), s = /\boperation\s+timed?\s?out\b/i;
|
|
27
|
+
function c(e) {
|
|
28
|
+
return !e || typeof e != "object" ? null : e;
|
|
29
|
+
}
|
|
30
|
+
function l(e) {
|
|
31
|
+
let t = c(e);
|
|
32
|
+
if (!t) return [];
|
|
33
|
+
let n = [], r = (e) => {
|
|
34
|
+
typeof e == "string" && e.length > 0 && n.push(e);
|
|
35
|
+
};
|
|
36
|
+
r(t.extensions?.code);
|
|
37
|
+
for (let e of t.errors ?? t.graphQLErrors ?? []) r(e?.extensions?.code);
|
|
38
|
+
let i = c(t.networkError);
|
|
39
|
+
if (i) {
|
|
40
|
+
r(i.extensions?.code);
|
|
41
|
+
for (let e of i.errors ?? i.graphQLErrors ?? []) r(e?.extensions?.code);
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
function u(e) {
|
|
46
|
+
let t = [], n = (e) => {
|
|
47
|
+
typeof e == "number" && Number.isFinite(e) && t.push(e);
|
|
48
|
+
}, r = c(e);
|
|
49
|
+
if (!r) return t;
|
|
50
|
+
n(r.statusCode), n(r.status);
|
|
51
|
+
let i = c(r.networkError);
|
|
52
|
+
return i && (n(i.statusCode), n(i.status)), t;
|
|
53
|
+
}
|
|
54
|
+
function d(e) {
|
|
55
|
+
let t = c(e);
|
|
56
|
+
if (!t) return [];
|
|
57
|
+
let n = [], r = (e) => {
|
|
58
|
+
typeof e == "string" && e.length > 0 && n.push(e);
|
|
59
|
+
};
|
|
60
|
+
r(t.message);
|
|
61
|
+
for (let e of t.errors ?? t.graphQLErrors ?? []) r(e?.message);
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
function f(e) {
|
|
65
|
+
if (!e || typeof e != "object") return;
|
|
66
|
+
let t = e, n = (t.errors ?? t.graphQLErrors)?.[0]?.message;
|
|
67
|
+
if (typeof n == "string" && n.length > 0) return n;
|
|
68
|
+
if (typeof t.message == "string" && t.message.length > 0) return t.message;
|
|
69
|
+
}
|
|
70
|
+
function p(e) {
|
|
71
|
+
if (!e || typeof e != "object") return null;
|
|
72
|
+
let t = l(e), n = u(e);
|
|
73
|
+
return t.some((e) => a.has(e)) || n.some((e) => e >= 400 && e < 500 && e !== 408) ? null : t.some((e) => i.has(e)) || n.some((e) => o.has(e)) || d(e).some((e) => s.test(e)) ? "cold-start" : c(e)?.networkError || t.includes("INTERNAL_SERVER_ERROR") ? "transient-blip" : null;
|
|
74
|
+
}
|
|
75
|
+
var m = {
|
|
76
|
+
"cold-start": 3,
|
|
77
|
+
"transient-blip": 3
|
|
78
|
+
}, h = {
|
|
79
|
+
"cold-start": t,
|
|
80
|
+
"transient-blip": r
|
|
81
|
+
}, g = ["cold-start"];
|
|
82
|
+
function _(e, t) {
|
|
83
|
+
let n = p(e);
|
|
84
|
+
if (!n) return {
|
|
85
|
+
retry: !1,
|
|
86
|
+
reason: "not-retryable"
|
|
87
|
+
};
|
|
88
|
+
if (!(t.allow ?? g).includes(n)) return {
|
|
89
|
+
retry: !1,
|
|
90
|
+
reason: "kind-not-allowed"
|
|
91
|
+
};
|
|
92
|
+
let r = m[n];
|
|
93
|
+
if (t.attempt >= r) return {
|
|
94
|
+
retry: !1,
|
|
95
|
+
reason: "attempts-exhausted"
|
|
96
|
+
};
|
|
97
|
+
let i = h[n], a = i[Math.min(t.attempt - 1, i.length - 1)] ?? 0;
|
|
98
|
+
return n === "cold-start" && t.elapsedMs + a >= 75e3 ? {
|
|
99
|
+
retry: !1,
|
|
100
|
+
reason: "budget-exhausted"
|
|
101
|
+
} : {
|
|
102
|
+
retry: !0,
|
|
103
|
+
kind: n,
|
|
104
|
+
delayMs: a,
|
|
105
|
+
nextAttempt: t.attempt + 1,
|
|
106
|
+
maxAttempts: r
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var v = (e) => new Promise((t) => setTimeout(t, e));
|
|
110
|
+
async function y(e, t = {}) {
|
|
111
|
+
let n = t.now ?? (() => Date.now()), r = t.sleep ?? v, i = n(), a = 0;
|
|
112
|
+
for (;;) {
|
|
113
|
+
a += 1;
|
|
114
|
+
let o, s, c = !1;
|
|
115
|
+
try {
|
|
116
|
+
o = await e(a);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
c = !0, s = e, o = {
|
|
119
|
+
ok: !1,
|
|
120
|
+
error: e
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (o.ok) return {
|
|
124
|
+
value: o.value,
|
|
125
|
+
attempts: a
|
|
126
|
+
};
|
|
127
|
+
let l = _(o.error, {
|
|
128
|
+
attempt: a,
|
|
129
|
+
elapsedMs: n() - i,
|
|
130
|
+
...t.allow ? { allow: t.allow } : {}
|
|
131
|
+
});
|
|
132
|
+
if (!l.retry) {
|
|
133
|
+
if (c) throw s;
|
|
134
|
+
return {
|
|
135
|
+
error: o.error,
|
|
136
|
+
attempts: a,
|
|
137
|
+
gaveUpBecause: l.reason
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
t.onRetry?.({
|
|
141
|
+
kind: l.kind,
|
|
142
|
+
attempt: l.nextAttempt,
|
|
143
|
+
maxAttempts: l.maxAttempts,
|
|
144
|
+
delayMs: l.delayMs,
|
|
145
|
+
error: o.error
|
|
146
|
+
}), await r(l.delayMs);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { e as COLD_START_MAX_ATTEMPTS, n as COLD_START_RETRY_BUDGET_MS, t as COLD_START_RETRY_DELAYS_MS, p as classifyFilesError, _ as decideFilesRetry, f as extractErrorMessage, y as runWithFilesRetry };
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { ApolloClient } from '@apollo/client';
|
|
2
|
+
import { extractErrorMessage, FilesRetryNotice } from './retryPolicy';
|
|
3
|
+
export { extractErrorMessage };
|
|
2
4
|
/**
|
|
3
5
|
* Apollo invalidations every "Save to Files" caller benefits from — pulled
|
|
4
6
|
* from microfe-files' useUploadFile to keep the browse views in sync.
|
|
5
7
|
*/
|
|
6
8
|
export declare const FILES_AFTER_SAVE_REFETCH: string[];
|
|
9
|
+
/** Which gateway hop a retry notice is about, so the UI can name it. */
|
|
10
|
+
export type SaveToFilesStep = 'folder' | 'initiate' | 'complete';
|
|
11
|
+
/** A retry notice with the step attached. */
|
|
12
|
+
export interface SaveToFilesRetryNotice extends FilesRetryNotice {
|
|
13
|
+
step: SaveToFilesStep;
|
|
14
|
+
}
|
|
7
15
|
export interface UploadBlobParams {
|
|
8
16
|
client: ApolloClient;
|
|
9
17
|
blob: Blob;
|
|
@@ -11,22 +19,23 @@ export interface UploadBlobParams {
|
|
|
11
19
|
mimeType: string;
|
|
12
20
|
folderId: string;
|
|
13
21
|
onProgress?: (pct: number) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Fired before each retry wait. The upload is still running — this exists so the UI
|
|
24
|
+
* can explain a 60-second pause instead of looking frozen. It is never a failure
|
|
25
|
+
* signal; a genuine failure still arrives as a rejection.
|
|
26
|
+
*/
|
|
27
|
+
onRetry?: (notice: SaveToFilesRetryNotice) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Test seam for the retry ladder's clock and waits. Production callers never set
|
|
30
|
+
* these — without them the real ladder (tens of seconds) runs, which is the point.
|
|
31
|
+
*/
|
|
32
|
+
now?: () => number;
|
|
33
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
34
|
}
|
|
15
35
|
export interface UploadedFile {
|
|
16
36
|
id: string;
|
|
17
37
|
name: string;
|
|
18
38
|
folderId: string | null;
|
|
19
39
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Best-effort message extraction, preferring the first GraphQL error's message
|
|
22
|
-
* (Apollo v4 CombinedGraphQLErrors `.errors`, or v3 `.graphQLErrors`) so we surface
|
|
23
|
-
* the real reason a completeUpload failed instead of a generic error envelope.
|
|
24
|
-
*
|
|
25
|
-
* Exported because every `client.mutate` call in this module faces the same
|
|
26
|
-
* problem: under `errorPolicy: 'all'` the failure arrives in-band on
|
|
27
|
-
* `result.error` and the caller's own "that didn't work" string is the LEAST
|
|
28
|
-
* informative thing it could report. See `useSaveToFiles`.
|
|
29
|
-
*/
|
|
30
|
-
export declare function extractErrorMessage(err: unknown): string | undefined;
|
|
31
40
|
export declare function uploadBlobToFiles(params: UploadBlobParams): Promise<UploadedFile>;
|
|
32
41
|
//# sourceMappingURL=uploadBlob.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadBlob.d.ts","sourceRoot":"","sources":["../../src/storage/uploadBlob.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"uploadBlob.d.ts","sourceRoot":"","sources":["../../src/storage/uploadBlob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EAAE,mBAAmB,EAAqB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE9F,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAyB/B;;;GAGG;AACH,eAAO,MAAM,wBAAwB,UAKpC,CAAC;AAEF,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjE,6CAA6C;AAC7C,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACnD;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuIvF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractErrorMessage as e, runWithFilesRetry as t } from "./retryPolicy.js";
|
|
2
|
+
import { gql as n } from "@apollo/client";
|
|
2
3
|
//#region src/storage/uploadBlob.ts
|
|
3
|
-
var
|
|
4
|
+
var r = n`
|
|
4
5
|
mutation SaveToFiles_InitiateUpload($input: InitiateUploadInput!) {
|
|
5
6
|
initiateUpload(input: $input) {
|
|
6
7
|
session {
|
|
@@ -9,7 +10,7 @@ var t = e`
|
|
|
9
10
|
uploadUrl
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
`,
|
|
13
|
+
`, i = n`
|
|
13
14
|
mutation SaveToFiles_CompleteUpload($input: CompleteUploadInput!) {
|
|
14
15
|
completeUpload(input: $input) {
|
|
15
16
|
file {
|
|
@@ -19,37 +20,48 @@ var t = e`
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
`,
|
|
23
|
+
`, a = [
|
|
23
24
|
"GetFilesRootBrowsePage",
|
|
24
25
|
"GetFilesFolderBrowsePage",
|
|
25
26
|
"GetFilesRecentPage",
|
|
26
27
|
"GetFiles"
|
|
27
28
|
];
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
function a(e) {
|
|
34
|
-
if (!e || typeof e != "object") return;
|
|
35
|
-
let t = e, n = (t.errors ?? t.graphQLErrors)?.[0]?.message;
|
|
36
|
-
if (typeof n == "string" && n.length > 0) return n;
|
|
37
|
-
if (typeof t.message == "string" && t.message.length > 0) return t.message;
|
|
38
|
-
}
|
|
39
|
-
async function o(e) {
|
|
40
|
-
let { client: o, blob: s, filename: c, mimeType: l, folderId: u, onProgress: d } = e, f = c.includes(".") ? c.substring(c.lastIndexOf(".") + 1) : "";
|
|
29
|
+
async function o(n) {
|
|
30
|
+
let { client: o, blob: s, filename: c, mimeType: l, folderId: u, onProgress: d, onRetry: f } = n, p = c.includes(".") ? c.substring(c.lastIndexOf(".") + 1) : "", m = {
|
|
31
|
+
...n.now ? { now: n.now } : {},
|
|
32
|
+
...n.sleep ? { sleep: n.sleep } : {}
|
|
33
|
+
};
|
|
41
34
|
d?.(0);
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
let h = await t(async () => {
|
|
36
|
+
let e = await o.mutate({
|
|
37
|
+
mutation: r,
|
|
38
|
+
variables: { input: {
|
|
39
|
+
fileName: c,
|
|
40
|
+
fileSize: String(s.size),
|
|
41
|
+
mimeType: l,
|
|
42
|
+
extension: p,
|
|
43
|
+
folderId: u
|
|
44
|
+
} }
|
|
45
|
+
}), t = e.data?.initiateUpload?.session, n = e.data?.initiateUpload?.uploadUrl;
|
|
46
|
+
return t && n ? {
|
|
47
|
+
ok: !0,
|
|
48
|
+
value: {
|
|
49
|
+
session: t,
|
|
50
|
+
uploadUrl: n
|
|
51
|
+
}
|
|
52
|
+
} : {
|
|
53
|
+
ok: !1,
|
|
54
|
+
error: e.error
|
|
55
|
+
};
|
|
56
|
+
}, {
|
|
57
|
+
...m,
|
|
58
|
+
...f ? { onRetry: (e) => f({
|
|
59
|
+
...e,
|
|
60
|
+
step: "initiate"
|
|
61
|
+
}) } : {}
|
|
62
|
+
});
|
|
63
|
+
if (!h.value) throw Error(e(h.error) ?? "initiateUpload did not return a presigned URL");
|
|
64
|
+
let { session: g, uploadUrl: _ } = h.value;
|
|
53
65
|
d?.(10), await new Promise((e, t) => {
|
|
54
66
|
let n = new XMLHttpRequest();
|
|
55
67
|
n.upload.addEventListener("progress", (e) => {
|
|
@@ -59,44 +71,35 @@ async function o(e) {
|
|
|
59
71
|
}
|
|
60
72
|
}), n.addEventListener("load", () => {
|
|
61
73
|
n.status >= 200 && n.status < 300 ? (d?.(90), e()) : t(/* @__PURE__ */ Error(`Upload PUT failed with status ${n.status}`));
|
|
62
|
-
}), n.addEventListener("error", () => t(/* @__PURE__ */ Error("Upload PUT network error"))), n.addEventListener("abort", () => t(/* @__PURE__ */ Error("Upload PUT aborted"))), n.open("PUT",
|
|
74
|
+
}), n.addEventListener("error", () => t(/* @__PURE__ */ Error("Upload PUT network error"))), n.addEventListener("abort", () => t(/* @__PURE__ */ Error("Upload PUT aborted"))), n.open("PUT", _), n.setRequestHeader("Content-Type", l), n.send(s);
|
|
75
|
+
});
|
|
76
|
+
let v = await t(async () => {
|
|
77
|
+
let e = await o.mutate({
|
|
78
|
+
mutation: i,
|
|
79
|
+
variables: { input: {
|
|
80
|
+
sessionId: g.id,
|
|
81
|
+
fileSize: String(s.size),
|
|
82
|
+
folderId: u
|
|
83
|
+
} },
|
|
84
|
+
refetchQueries: a
|
|
85
|
+
}), t = e.data?.completeUpload?.file;
|
|
86
|
+
return t ? {
|
|
87
|
+
ok: !0,
|
|
88
|
+
value: t
|
|
89
|
+
} : {
|
|
90
|
+
ok: !1,
|
|
91
|
+
error: e.error
|
|
92
|
+
};
|
|
93
|
+
}, {
|
|
94
|
+
allow: ["cold-start", "transient-blip"],
|
|
95
|
+
...m,
|
|
96
|
+
...f ? { onRetry: (e) => f({
|
|
97
|
+
...e,
|
|
98
|
+
step: "complete"
|
|
99
|
+
}) } : {}
|
|
63
100
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let e = await o.mutate({
|
|
67
|
-
mutation: n,
|
|
68
|
-
variables: { input: {
|
|
69
|
-
sessionId: m.id,
|
|
70
|
-
fileSize: String(s.size),
|
|
71
|
-
folderId: u
|
|
72
|
-
} },
|
|
73
|
-
refetchQueries: r
|
|
74
|
-
}), t = e.data?.completeUpload?.file ?? void 0;
|
|
75
|
-
if (t) return { file: t };
|
|
76
|
-
let c = e.error;
|
|
77
|
-
return {
|
|
78
|
-
retryable: i(c),
|
|
79
|
-
message: a(c)
|
|
80
|
-
};
|
|
81
|
-
} catch (e) {
|
|
82
|
-
if (!i(e)) throw e;
|
|
83
|
-
return {
|
|
84
|
-
retryable: !0,
|
|
85
|
-
message: a(e)
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}, _, v;
|
|
89
|
-
for (let e = 0; e <= 2; e++) {
|
|
90
|
-
e > 0 && await new Promise((t) => setTimeout(t, 400 * e));
|
|
91
|
-
let t = await g();
|
|
92
|
-
if (t.file) {
|
|
93
|
-
_ = t.file;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
if (v = t.message ?? v, !t.retryable) break;
|
|
97
|
-
}
|
|
98
|
-
if (!_) throw Error(v ?? "completeUpload did not return a file");
|
|
99
|
-
return d?.(100), _;
|
|
101
|
+
if (!v.value) throw Error(e(v.error) ?? "completeUpload did not return a file");
|
|
102
|
+
return d?.(100), v.value;
|
|
100
103
|
}
|
|
101
104
|
//#endregion
|
|
102
|
-
export {
|
|
105
|
+
export { a as FILES_AFTER_SAVE_REFETCH, o as uploadBlobToFiles };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApolloClient } from '@apollo/client';
|
|
2
2
|
import { SaveToFilesSource } from './SaveToFilesButton';
|
|
3
|
+
import { SaveToFilesRetryNotice } from './uploadBlob';
|
|
3
4
|
export interface SavedFile {
|
|
4
5
|
id: string;
|
|
5
6
|
name: string;
|
|
@@ -9,13 +10,35 @@ export interface UseSaveToFilesResult {
|
|
|
9
10
|
save: (params: {
|
|
10
11
|
source: SaveToFilesSource;
|
|
11
12
|
folderPath: string;
|
|
13
|
+
/**
|
|
14
|
+
* Fired before each retry wait. Purely informational — the save is still in
|
|
15
|
+
* flight. Exists so a caller rendering its own chip can say why a cold start is
|
|
16
|
+
* taking a minute instead of showing a spinner that looks stuck.
|
|
17
|
+
*/
|
|
18
|
+
onRetry?: (notice: SaveToFilesRetryNotice) => void;
|
|
12
19
|
}) => Promise<SavedFile>;
|
|
13
20
|
isPending: boolean;
|
|
14
21
|
error: Error | null;
|
|
15
22
|
progress: number;
|
|
23
|
+
/**
|
|
24
|
+
* The retry currently being waited for or run — `{ step, attempt, maxAttempts,
|
|
25
|
+
* delayMs }`. Null on the first attempt of every step, and always cleared when the
|
|
26
|
+
* save settles either way, so it can never be mistaken for a result. Render it to
|
|
27
|
+
* explain a long pause; it is not an error and must not be shown as one.
|
|
28
|
+
*/
|
|
29
|
+
retry: SaveToFilesRetryNotice | null;
|
|
16
30
|
}
|
|
17
31
|
/** The Apollo surface `resolveFolderIdByPath` needs — kept narrow so it is trivial to fake. */
|
|
18
32
|
type FolderMutatingClient = Pick<ApolloClient, 'mutate'>;
|
|
33
|
+
export interface ResolveFolderOptions {
|
|
34
|
+
onRetry?: (notice: SaveToFilesRetryNotice) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Test seam for the retry ladder's clock and waits. Production callers never set
|
|
37
|
+
* these — without them the real ladder (tens of seconds) runs, which is the point.
|
|
38
|
+
*/
|
|
39
|
+
now?: () => number;
|
|
40
|
+
sleep?: (ms: number) => Promise<void>;
|
|
41
|
+
}
|
|
19
42
|
/**
|
|
20
43
|
* Resolve (creating if needed) the destination folder for a path, returning its id.
|
|
21
44
|
*
|
|
@@ -38,8 +61,13 @@ type FolderMutatingClient = Pick<ApolloClient, 'mutate'>;
|
|
|
38
61
|
* bad path or a missing folder and sent people looking at file-storage quota
|
|
39
62
|
* they had plenty of. Surface the real reason; keep the path as a last resort
|
|
40
63
|
* for the genuinely inexplicable case.
|
|
64
|
+
*
|
|
65
|
+
* A gateway timeout is now also *waited out* rather than only reported, because this
|
|
66
|
+
* is the hop that was measured failing on a cold container. Only a cold-start-class
|
|
67
|
+
* failure is retried — a quota rejection, an auth failure or any other 4xx answer
|
|
68
|
+
* still comes straight back, unchanged and immediately.
|
|
41
69
|
*/
|
|
42
|
-
export declare function resolveFolderIdByPath(client: FolderMutatingClient, folderPath: string): Promise<string>;
|
|
70
|
+
export declare function resolveFolderIdByPath(client: FolderMutatingClient, folderPath: string, options?: ResolveFolderOptions): Promise<string>;
|
|
43
71
|
export declare function useSaveToFiles(): UseSaveToFilesResult;
|
|
44
72
|
export {};
|
|
45
73
|
//# sourceMappingURL=useSaveToFiles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSaveToFiles.d.ts","sourceRoot":"","sources":["../../src/storage/useSaveToFiles.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useSaveToFiles.d.ts","sourceRoot":"","sources":["../../src/storage/useSaveToFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAIxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAsBtB,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,CAAC,MAAM,EAAE;QACb,MAAM,EAAE,iBAAiB,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB;;;;WAIG;QACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;KACpD,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAAC;CACtC;AAED,+FAA+F;AAC/F,KAAK,oBAAoB,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAEzD,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACnD;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,oBAAoB,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,wBAAgB,cAAc,IAAI,oBAAoB,CAsFrD"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { extractErrorMessage as e, runWithFilesRetry as t } from "./retryPolicy.js";
|
|
2
|
+
import { FILES_AFTER_SAVE_REFETCH as n, uploadBlobToFiles as r } from "./uploadBlob.js";
|
|
3
|
+
import { useState as i } from "react";
|
|
4
|
+
import { gql as a } from "@apollo/client";
|
|
5
|
+
import { useApolloClient as o } from "@apollo/client/react";
|
|
5
6
|
//#region src/storage/useSaveToFiles.ts
|
|
6
|
-
var
|
|
7
|
+
var s = a`
|
|
7
8
|
mutation SaveToFiles_EnsureFolderByPath($path: String!) {
|
|
8
9
|
ensureFolderByPath(path: $path) {
|
|
9
10
|
id
|
|
@@ -11,7 +12,7 @@ var o = i`
|
|
|
11
12
|
path
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
|
-
`,
|
|
15
|
+
`, c = a`
|
|
15
16
|
mutation SaveToFiles_SaveSandboxArtifact($input: SaveSandboxArtifactInput!) {
|
|
16
17
|
saveSandboxArtifactToFiles(input: $input) {
|
|
17
18
|
id
|
|
@@ -20,58 +21,79 @@ var o = i`
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
`;
|
|
23
|
-
async function
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
async function l(n, r, i = {}) {
|
|
25
|
+
let a = await t(async () => {
|
|
26
|
+
let e = await n.mutate({
|
|
27
|
+
mutation: s,
|
|
28
|
+
variables: { path: r }
|
|
29
|
+
}), t = e.data?.ensureFolderByPath?.id;
|
|
30
|
+
return t ? {
|
|
31
|
+
ok: !0,
|
|
32
|
+
value: t
|
|
33
|
+
} : {
|
|
34
|
+
ok: !1,
|
|
35
|
+
error: e.error
|
|
36
|
+
};
|
|
37
|
+
}, {
|
|
38
|
+
...i.onRetry ? { onRetry: (e) => i.onRetry?.({
|
|
39
|
+
...e,
|
|
40
|
+
step: "folder"
|
|
41
|
+
}) } : {},
|
|
42
|
+
...i.now ? { now: i.now } : {},
|
|
43
|
+
...i.sleep ? { sleep: i.sleep } : {}
|
|
44
|
+
});
|
|
45
|
+
if (a.value !== void 0) return a.value;
|
|
46
|
+
throw Error(e(a.error) ?? `Could not resolve folder for path: ${r}`);
|
|
30
47
|
}
|
|
31
|
-
function
|
|
32
|
-
let
|
|
48
|
+
function u() {
|
|
49
|
+
let t = o(), [a, s] = i(!1), [u, d] = i(null), [f, p] = i(0), [m, h] = i(null);
|
|
33
50
|
return {
|
|
34
|
-
save: async (
|
|
35
|
-
|
|
51
|
+
save: async (i) => {
|
|
52
|
+
s(!0), d(null), p(0), h(null);
|
|
53
|
+
let a = (e) => {
|
|
54
|
+
h(e), i.onRetry?.(e);
|
|
55
|
+
};
|
|
36
56
|
try {
|
|
37
|
-
let
|
|
38
|
-
if (
|
|
39
|
-
let
|
|
40
|
-
mutation:
|
|
57
|
+
let o = await l(t, i.folderPath, { onRetry: a });
|
|
58
|
+
if (h(null), i.source.kind === "sandbox-artifact") {
|
|
59
|
+
let r = await t.mutate({
|
|
60
|
+
mutation: c,
|
|
41
61
|
variables: { input: {
|
|
42
|
-
sandboxId:
|
|
43
|
-
sessionId:
|
|
44
|
-
path:
|
|
45
|
-
folderId:
|
|
46
|
-
filename:
|
|
62
|
+
sandboxId: i.source.sandboxId,
|
|
63
|
+
sessionId: i.source.sessionId,
|
|
64
|
+
path: i.source.path,
|
|
65
|
+
folderId: o,
|
|
66
|
+
filename: i.source.filename ?? null
|
|
47
67
|
} },
|
|
48
|
-
refetchQueries:
|
|
49
|
-
}),
|
|
50
|
-
if (!
|
|
51
|
-
return p(100),
|
|
68
|
+
refetchQueries: n
|
|
69
|
+
}), a = r.data?.saveSandboxArtifactToFiles;
|
|
70
|
+
if (!a) throw Error(e(r.error) ?? "saveSandboxArtifactToFiles returned no file");
|
|
71
|
+
return p(100), a;
|
|
52
72
|
}
|
|
53
|
-
let
|
|
54
|
-
if (!
|
|
55
|
-
let
|
|
56
|
-
return await
|
|
57
|
-
client:
|
|
58
|
-
blob:
|
|
59
|
-
filename:
|
|
60
|
-
mimeType:
|
|
61
|
-
folderId:
|
|
62
|
-
onProgress: p
|
|
73
|
+
let s = await i.source.getBlob();
|
|
74
|
+
if (!s) throw Error("Cancelled: no bytes to save");
|
|
75
|
+
let u = s.blob;
|
|
76
|
+
return await r({
|
|
77
|
+
client: t,
|
|
78
|
+
blob: u,
|
|
79
|
+
filename: s.filename ?? i.source.filename,
|
|
80
|
+
mimeType: s.mimeType ?? i.source.mimeType,
|
|
81
|
+
folderId: o,
|
|
82
|
+
onProgress: p,
|
|
83
|
+
onRetry: a
|
|
63
84
|
});
|
|
64
85
|
} catch (e) {
|
|
65
86
|
let t = e instanceof Error ? e : Error(String(e));
|
|
66
87
|
throw d(t), t;
|
|
67
88
|
} finally {
|
|
68
|
-
|
|
89
|
+
s(!1), h(null);
|
|
69
90
|
}
|
|
70
91
|
},
|
|
71
|
-
isPending:
|
|
92
|
+
isPending: a,
|
|
72
93
|
error: u,
|
|
73
|
-
progress: f
|
|
94
|
+
progress: f,
|
|
95
|
+
retry: m
|
|
74
96
|
};
|
|
75
97
|
}
|
|
76
98
|
//#endregion
|
|
77
|
-
export {
|
|
99
|
+
export { u as useSaveToFiles };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApolloClient } from '@apollo/client';
|
|
2
|
+
/**
|
|
3
|
+
* How long a successful warm-up is assumed to hold. Azure Container Apps scales a
|
|
4
|
+
* revision back to zero after ~300s idle, so anything under that is a wasted request
|
|
5
|
+
* — the container is still up. 240s leaves headroom without letting the window lapse.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FILES_WARMUP_COOLDOWN_MS = 240000;
|
|
8
|
+
/** Test seam — module state is a browser-lifetime singleton in real use. */
|
|
9
|
+
export declare function __resetFilesWarmupForTests(): void;
|
|
10
|
+
export interface WarmFilesServiceOptions {
|
|
11
|
+
/** Injected by tests. */
|
|
12
|
+
now?: () => number;
|
|
13
|
+
/** Skip the cooldown (for an explicit "the user is definitely about to upload"). */
|
|
14
|
+
force?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Send one cheap files-svc read so a scaled-to-zero container starts booting.
|
|
18
|
+
*
|
|
19
|
+
* Never rejects and never throws — a failed warm-up is not a user-visible event.
|
|
20
|
+
* Concurrent callers share the in-flight promise; callers inside the cooldown window
|
|
21
|
+
* get an already-resolved one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function warmFilesService(client: Pick<ApolloClient, 'query'>, options?: WarmFilesServiceOptions): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Fire a files-svc warm-up when `active` becomes true — e.g. when the assistant panel
|
|
26
|
+
* opens, so the container is booting while the user is still typing.
|
|
27
|
+
*
|
|
28
|
+
* The call site belongs to whoever owns the panel. In HealthyBowl that is
|
|
29
|
+
* `microfe-healthybowl`'s `AIAssistantPanel`, not fe-libs: fe-libs owns the mechanism
|
|
30
|
+
* (which query, single-flight, silence), the product owns the moment.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useFilesServiceWarmup(active?: boolean): void;
|
|
33
|
+
//# sourceMappingURL=warmFilesService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warmFilesService.d.ts","sourceRoot":"","sources":["../../src/storage/warmFilesService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAYxD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAAU,CAAC;AAOhD,4EAA4E;AAC5E,wBAAgB,0BAA0B,IAAI,IAAI,CAGjD;AAED,MAAM,WAAW,uBAAuB;IACtC,yBAAyB;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,EACnC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,OAAc,GAAG,IAAI,CAOlE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect as e } from "react";
|
|
2
|
+
import { gql as t } from "@apollo/client";
|
|
3
|
+
import { useApolloClient as n } from "@apollo/client/react";
|
|
4
|
+
//#region src/storage/warmFilesService.ts
|
|
5
|
+
var r = t`
|
|
6
|
+
query FilesServiceWarmup($pagination: PaginationInput) {
|
|
7
|
+
getMyUploadSessions(pagination: $pagination) {
|
|
8
|
+
total
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
`, i = 24e4, a = null, o = null;
|
|
12
|
+
function s(e, t = {}) {
|
|
13
|
+
let n = t.now ?? (() => Date.now());
|
|
14
|
+
if (a) return a;
|
|
15
|
+
if (!t.force && o !== null && n() - o < 24e4) return Promise.resolve();
|
|
16
|
+
let i;
|
|
17
|
+
try {
|
|
18
|
+
i = Promise.resolve(e.query({
|
|
19
|
+
query: r,
|
|
20
|
+
variables: { pagination: {
|
|
21
|
+
limit: 1,
|
|
22
|
+
offset: 0
|
|
23
|
+
} },
|
|
24
|
+
fetchPolicy: "no-cache",
|
|
25
|
+
errorPolicy: "all"
|
|
26
|
+
}));
|
|
27
|
+
} catch {
|
|
28
|
+
i = Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
let s = i.then(() => void 0, () => void 0).then(() => {
|
|
31
|
+
o = n(), a = null;
|
|
32
|
+
});
|
|
33
|
+
return a = s, s;
|
|
34
|
+
}
|
|
35
|
+
function c(t = !0) {
|
|
36
|
+
let r = n();
|
|
37
|
+
e(() => {
|
|
38
|
+
t && s(r);
|
|
39
|
+
}, [t, r]);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { i as FILES_WARMUP_COOLDOWN_MS, c as useFilesServiceWarmup, s as warmFilesService };
|