@alexkroman1/aai-ui 0.14.0 → 0.14.2

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.
@@ -646,6 +646,218 @@ function closeFailure(event, socketErrored) {
646
646
  return socketErrored ? "WebSocket connection error" : null;
647
647
  }
648
648
  //#endregion
649
+ //#region src/session/handshake.ts
650
+ /** What the session reports once the budget below is spent. */
651
+ const HANDSHAKE_ERROR = {
652
+ code: "connection",
653
+ message: "Agent did not complete the session handshake",
654
+ fatal: false
655
+ };
656
+ //#endregion
657
+ //#region src/session/connection.ts
658
+ /**
659
+ * One browser session's CONNECTION — the socket, the handshake deadline over
660
+ * it, and the server's idle retirement — as a statechart.
661
+ *
662
+ * `link` is `closed → open`, and `open` is `dialing → awaitingHandshake → live`:
663
+ *
664
+ * - **The socket belongs to `open`.** Its entry dials and attaches the
665
+ * listeners; its exit is the ONE teardown (detach, release the pre-connect
666
+ * mic, tear the audio path down, close the socket). A hang-up, a terminal
667
+ * close, an exhausted handshake and a fresh `connect()` all leave `open`, so
668
+ * none of them restates it, and exit actions run before the transition's, so
669
+ * the teardown always precedes the snapshot write reporting the end.
670
+ * - **The handshake deadline is an `after` on `awaitingHandshake`** (see
671
+ * `session/handshake.ts` for why there is one). Leaving the state disarms it,
672
+ * so a `config` frame, a close or a teardown cannot leave it armed against
673
+ * the next attempt — there is no timer to clear by hand on abort.
674
+ * - **partysocket owns the reconnect backoff.** A close that partysocket will
675
+ * retry goes back to `dialing` and waits for its next `open`; this machine
676
+ * never schedules an attempt. The deadline forces one only for a socket
677
+ * partysocket thinks is healthy (`forceReconnect`).
678
+ *
679
+ * `retired` is a second region because it outlives a socket: the server's
680
+ * `session.timedOut` marks the close that follows as EXPECTED, so it is not
681
+ * retried. Without it the automatic reconnect would re-open the session the
682
+ * server just reclaimed — a tab left open would cycle forever and the guest
683
+ * would never see zero sessions, which is the point of the timeout. Only a
684
+ * fresh `connect()` (the user asking again) clears it.
685
+ *
686
+ * The machine owns WHEN; every HOW — the socket, the snapshot, the audio path —
687
+ * is an injected {@link ConnectionEffects} call, as in `session/audio-state.ts`.
688
+ */
689
+ const connectionMachine = setup({
690
+ types: {},
691
+ delays: { HANDSHAKE_TIMEOUT: 1e4 },
692
+ guards: {
693
+ /**
694
+ * partysocket will retry this close, and nothing says it must not: a FATAL
695
+ * error is not retryable by construction, nor is an idle retirement.
696
+ */
697
+ mayReconnect: and([({ context, event }) => event.type === "DROPPED" && event.reconnecting && !context.effects.fatal(), not(stateIn({ retired: "yes" }))]),
698
+ /** Budget left, and a socket that can re-dial at all. */
699
+ mayRedial: ({ context }) => context.timeouts + 1 < 3 && context.effects.canRedial()
700
+ },
701
+ actions: {
702
+ spend: assign({ timeouts: ({ context }) => context.timeouts + 1 }),
703
+ /** A socket error inside a retry cycle is not terminal. */
704
+ forgetError: assign({ errored: false }),
705
+ retrying: ({ context }) => context.effects.retrying(),
706
+ opened: ({ context }) => context.effects.opened(),
707
+ dial: ({ context, event }) => context.link.dial(event.type === "DIAL" ? event.signal : void 0),
708
+ /** The ONE teardown: every way out of `open` runs it, before anything is reported. */
709
+ teardown: ({ context }) => context.link.teardown(),
710
+ reportClose: ({ context, event }) => {
711
+ if (event.type === "DROPPED") context.effects.closed(closeFailure(event.event, context.errored));
712
+ }
713
+ }
714
+ }).createMachine({
715
+ id: "connection",
716
+ type: "parallel",
717
+ context: ({ input }) => ({
718
+ ...input,
719
+ timeouts: 0,
720
+ errored: false
721
+ }),
722
+ states: {
723
+ link: {
724
+ initial: "closed",
725
+ on: {
726
+ DIAL: {
727
+ target: ".open",
728
+ reenter: true
729
+ },
730
+ HANG_UP: ".closed"
731
+ },
732
+ states: {
733
+ closed: {},
734
+ open: {
735
+ entry: [assign({
736
+ timeouts: 0,
737
+ errored: false
738
+ }), "dial"],
739
+ exit: "teardown",
740
+ initial: "dialing",
741
+ on: {
742
+ ERRORED: { actions: assign({ errored: true }) },
743
+ CONFIGURED: {
744
+ target: ".live",
745
+ actions: [assign({ timeouts: 0 }), ({ context, event }) => context.effects.configured(event.config)]
746
+ },
747
+ DROPPED: [{
748
+ guard: "mayReconnect",
749
+ target: ".dialing",
750
+ actions: ["forgetError", "retrying"]
751
+ }, {
752
+ target: "closed",
753
+ actions: "reportClose"
754
+ }]
755
+ },
756
+ states: {
757
+ dialing: { on: { OPENED: {
758
+ target: "awaitingHandshake",
759
+ actions: "opened"
760
+ } } },
761
+ awaitingHandshake: {
762
+ on: { OPENED: {
763
+ target: "awaitingHandshake",
764
+ reenter: true,
765
+ actions: "opened"
766
+ } },
767
+ after: { HANDSHAKE_TIMEOUT: [{
768
+ guard: "mayRedial",
769
+ target: "dialing",
770
+ actions: [
771
+ "spend",
772
+ ({ context }) => context.effects.redial(),
773
+ "retrying"
774
+ ]
775
+ }, {
776
+ target: "#connection.link.closed",
777
+ actions: ({ context }) => context.effects.exhausted()
778
+ }] }
779
+ },
780
+ live: { on: { OPENED: {
781
+ target: "awaitingHandshake",
782
+ actions: "opened"
783
+ } } }
784
+ }
785
+ }
786
+ }
787
+ },
788
+ retired: {
789
+ initial: "no",
790
+ states: {
791
+ no: { on: { RETIRED: "yes" } },
792
+ yes: { on: { DIAL: "no" } }
793
+ }
794
+ }
795
+ }
796
+ });
797
+ /** The socket half of {@link Link}: listeners in, teardown out. */
798
+ function createLink(effects, send) {
799
+ let current = null;
800
+ return {
801
+ dial(caller) {
802
+ const controller = new AbortController();
803
+ const { signal } = controller;
804
+ caller?.addEventListener("abort", () => effects.abandon(), { signal });
805
+ const socket = effects.dial();
806
+ current = {
807
+ socket,
808
+ controller
809
+ };
810
+ socket.addEventListener("open", () => send({ type: "OPENED" }), { signal });
811
+ socket.addEventListener("message", (event) => {
812
+ const config = effects.receive(event.data);
813
+ if (config) send({
814
+ type: "CONFIGURED",
815
+ config
816
+ });
817
+ }, { signal });
818
+ socket.addEventListener("error", () => send({ type: "ERRORED" }), { signal });
819
+ socket.addEventListener("close", (event) => send({
820
+ type: "DROPPED",
821
+ event,
822
+ reconnecting: effects.retryPending(socket)
823
+ }), { signal });
824
+ },
825
+ teardown() {
826
+ if (current === null) return;
827
+ const { socket, controller } = current;
828
+ current = null;
829
+ controller.abort();
830
+ effects.release(socket);
831
+ }
832
+ };
833
+ }
834
+ /** Create the connection machine for one browser session. */
835
+ function createConnection(effects) {
836
+ const link = createLink(effects, (event) => self.send(event));
837
+ const self = createActor(connectionMachine, { input: {
838
+ effects,
839
+ link
840
+ } }).start();
841
+ return {
842
+ dial: (signal) => {
843
+ self.send({
844
+ type: "DIAL",
845
+ signal
846
+ });
847
+ },
848
+ hangUp: () => {
849
+ self.send({ type: "HANG_UP" });
850
+ },
851
+ retire: () => {
852
+ self.send({ type: "RETIRED" });
853
+ },
854
+ phase: () => {
855
+ const at = self.getSnapshot().value.link;
856
+ return typeof at === "string" ? "closed" : at.open;
857
+ }
858
+ };
859
+ }
860
+ //#endregion
649
861
  //#region src/session/reconnect.ts
650
862
  /**
651
863
  * Automatic reconnection for the browser session socket, built on
@@ -676,6 +888,13 @@ function openReconnectingSocket(urlProvider, protocolsProvider) {
676
888
  return new ReconnectingWebSocket(urlProvider, protocolsProvider, RECONNECT_OPTIONS);
677
889
  }
678
890
  /**
891
+ * Does `socket` have reconnect machinery? False for an injected
892
+ * `options.WebSocket` (tests), which a failure then ends for good.
893
+ */
894
+ function canReconnect(socket) {
895
+ return socket instanceof ReconnectingWebSocket;
896
+ }
897
+ /**
679
898
  * Force a fresh connection attempt on a reconnecting socket, reporting
680
899
  * whether it was one. Used for a failure partysocket cannot see: a socket it
681
900
  * considers open and healthy, whose peer never completed OUR handshake.
@@ -1018,67 +1237,6 @@ function createDialer(options, onSessionId) {
1018
1237
  };
1019
1238
  }
1020
1239
  //#endregion
1021
- //#region src/session/handshake.ts
1022
- /** What the session reports once the budget below is spent. */
1023
- const HANDSHAKE_ERROR = {
1024
- code: "connection",
1025
- message: "Agent did not complete the session handshake",
1026
- fatal: false
1027
- };
1028
- /** How long an OPEN socket may go without a `config` frame. */
1029
- const HANDSHAKE_TIMEOUT_MS = 1e4;
1030
- /**
1031
- * How many consecutive handshake timeouts to ride out before giving up.
1032
- *
1033
- * `forceReconnect` restarts partysocket's own retry budget, so this is the
1034
- * budget for this failure mode — without one, a permanently wedged peer would
1035
- * be re-dialed every ~10s forever, which is the unbounded retry loop
1036
- * `RECONNECT_OPTIONS.maxRetries` exists to prevent.
1037
- *
1038
- * CONSECUTIVE is the whole of it, and only `succeeded()` says so — see its doc.
1039
- */
1040
- const MAX_HANDSHAKE_TIMEOUTS = 3;
1041
- /**
1042
- * Watch one connection's handshake.
1043
- *
1044
- * `onRetry` fires when a timed-out attempt has been re-dialed (the sandbox
1045
- * behind the endpoint may have been replaced, and the URL provider re-brokers
1046
- * on the next attempt); `onExhausted` when the budget is spent, or when the
1047
- * socket has no reconnect machinery at all (an injected `options.WebSocket`).
1048
- *
1049
- * @internal
1050
- */
1051
- function createHandshakeGuard(opts) {
1052
- let timer;
1053
- let timeouts = 0;
1054
- function disarm() {
1055
- if (timer === void 0) return;
1056
- clearTimeout(timer);
1057
- timer = void 0;
1058
- }
1059
- function fire() {
1060
- timer = void 0;
1061
- timeouts += 1;
1062
- if (timeouts < MAX_HANDSHAKE_TIMEOUTS && forceReconnect(opts.socket)) {
1063
- opts.onRetry();
1064
- return;
1065
- }
1066
- opts.onExhausted();
1067
- }
1068
- opts.signal.addEventListener("abort", disarm);
1069
- return {
1070
- arm() {
1071
- disarm();
1072
- timer = setTimeout(fire, HANDSHAKE_TIMEOUT_MS);
1073
- },
1074
- disarm,
1075
- succeeded() {
1076
- disarm();
1077
- timeouts = 0;
1078
- }
1079
- };
1080
- }
1081
- //#endregion
1082
1240
  //#region src/session/messages.ts
1083
1241
  /**
1084
1242
  * Incoming-message handling for the voice session core.
@@ -1124,7 +1282,7 @@ function appendCapped(list, item, cap) {
1124
1282
  * `ConnState.turn`).
1125
1283
  */
1126
1284
  function createMessageHandlers(deps) {
1127
- const { getSnapshot, updateState, conn, agentState, audio } = deps;
1285
+ const { getSnapshot, updateState, conn, agentState, audio, retire } = deps;
1128
1286
  /** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
1129
1287
  let customEventSeq = 0;
1130
1288
  /** Monotonically increasing counter for chat messages -- stable render keys
@@ -1332,7 +1490,7 @@ function createMessageHandlers(deps) {
1332
1490
  case "error.reported":
1333
1491
  handleErrorEvent(e);
1334
1492
  break;
1335
- case "session.timedOut": conn.retiredByServer = true;
1493
+ case "session.timedOut": retire();
1336
1494
  }
1337
1495
  }
1338
1496
  /** Enqueue a PCM16 audio chunk for playback. Transitions state to `"speaking"` on the first chunk. */
@@ -1480,11 +1638,14 @@ const PRE_CONNECT_SAMPLE_RATE = 16e3;
1480
1638
  /** Build one session's pre-connect capture holder. @internal */
1481
1639
  function createPreConnectAudio(enabled) {
1482
1640
  let held = null;
1641
+ let attempt = null;
1483
1642
  return {
1484
1643
  begin() {
1485
1644
  loadAudioModules().catch(() => {});
1486
1645
  if (!enabled || held !== null) return;
1487
- held = Promise.all([import("./audio/index.js"), loadAudioModules()]).then(([{ openPreConnectCapture }, [, captureWorklet]]) => openPreConnectCapture({
1646
+ const current = { released: false };
1647
+ attempt = current;
1648
+ held = Promise.all([import("./audio/index.js"), loadAudioModules()]).then(([{ openPreConnectCapture }, [, captureWorklet]]) => current.released ? null : openPreConnectCapture({
1488
1649
  sampleRate: PRE_CONNECT_SAMPLE_RATE,
1489
1650
  captureWorkletSrc: captureWorklet,
1490
1651
  maxSeconds: 10
@@ -1493,11 +1654,14 @@ function createPreConnectAudio(enabled) {
1493
1654
  take() {
1494
1655
  const taken = held;
1495
1656
  held = null;
1657
+ attempt = null;
1496
1658
  return taken;
1497
1659
  },
1498
1660
  release() {
1499
1661
  const taken = held;
1500
1662
  held = null;
1663
+ if (attempt) attempt.released = true;
1664
+ attempt = null;
1501
1665
  taken?.then((capture) => capture?.close());
1502
1666
  }
1503
1667
  };
@@ -1889,10 +2053,8 @@ function createBrowserSessionWith(options, internals) {
1889
2053
  const agentState = createSessionStateMachine();
1890
2054
  const conn = {
1891
2055
  ws: null,
1892
- retiredByServer: false,
1893
2056
  turn: createEpoch()
1894
2057
  };
1895
- let connectionController = null;
1896
2058
  const identity = createSessionIdentity(options, () => dialer);
1897
2059
  const dialer = createDialer({
1898
2060
  ...options,
@@ -1935,17 +2097,9 @@ function createBrowserSessionWith(options, internals) {
1935
2097
  updateState,
1936
2098
  conn,
1937
2099
  agentState,
1938
- audio
2100
+ audio,
2101
+ retire: () => connection.retire()
1939
2102
  });
1940
- /** Abort the in-flight connection and release audio + WebSocket resources. */
1941
- function teardownConnection() {
1942
- connectionController?.abort();
1943
- connectionController = null;
1944
- preConnect.release();
1945
- audio.teardown();
1946
- conn.ws?.close();
1947
- conn.ws = null;
1948
- }
1949
2103
  /**
1950
2104
  * React to the server's `session.configured` frame: record it and set up the
1951
2105
  * session's audio path. **It no longer replays history, and that is the
@@ -1960,78 +2114,43 @@ function createBrowserSessionWith(options, internals) {
1960
2114
  flushQueued();
1961
2115
  audio.start(config);
1962
2116
  }
1963
- function connect(opts) {
1964
- if (opts?.signal?.aborted) {
1965
- disconnect();
1966
- return;
1967
- }
1968
- updateState(agentState.apply({ type: "CONNECT" }));
1969
- teardownConnection();
1970
- preConnect.begin();
1971
- conn.retiredByServer = false;
1972
- const controller = new AbortController();
1973
- connectionController = controller;
1974
- const { signal: sig } = controller;
1975
- if (opts?.signal) opts.signal.addEventListener("abort", () => disconnect(), { signal: sig });
1976
- const socket = dialer.open();
1977
- socket.binaryType = "arraybuffer";
1978
- conn.ws = socket;
1979
- let socketErrored = false;
1980
- const handshake = createHandshakeGuard({
1981
- socket,
1982
- signal: sig,
1983
- onRetry: () => {
1984
- audio.teardown();
1985
- updateState({
1986
- ...agentState.apply({ type: "CONNECT" }),
1987
- recording: false
1988
- });
1989
- },
1990
- onExhausted: () => {
1991
- preConnect.release();
1992
- audio.teardown();
1993
- controller.abort();
1994
- socket.close();
1995
- conn.ws = null;
1996
- updateState({
1997
- ...agentState.apply({
1998
- type: "FAILED",
1999
- error: HANDSHAKE_ERROR
2000
- }),
2001
- ...STOPPED
2002
- });
2003
- }
2004
- });
2005
- socket.addEventListener("open", () => {
2006
- updateState(agentState.apply({ type: "SOCKET_OPEN" }));
2007
- handshake.arm();
2008
- }, { signal: sig });
2009
- socket.addEventListener("message", (event) => {
2010
- const config = handleMessage(event.data);
2011
- if (!config) return;
2012
- handshake.succeeded();
2013
- onServerConfig(config);
2014
- }, { signal: sig });
2015
- socket.addEventListener("error", () => {
2016
- socketErrored = true;
2017
- }, { signal: sig });
2018
- socket.addEventListener("close", (event) => {
2019
- if (sig.aborted) return;
2020
- handshake.disarm();
2021
- audio.teardown();
2022
- if (!(conn.retiredByServer || agentState.fatal()) && reconnectPending(socket)) {
2023
- socketErrored = false;
2024
- updateState({
2025
- ...agentState.apply({ type: "CONNECT" }),
2026
- recording: false
2027
- });
2028
- return;
2029
- }
2117
+ /**
2118
+ * The socket, the handshake deadline over it and the server's idle
2119
+ * retirement, as a statechart — see `session/connection.ts`. `release` is the
2120
+ * exit action of its `open` state, so every way a connection ends shares it.
2121
+ */
2122
+ const connection = createConnection({
2123
+ dial: () => {
2124
+ preConnect.begin();
2125
+ const socket = dialer.open();
2126
+ socket.binaryType = "arraybuffer";
2127
+ conn.ws = socket;
2128
+ return socket;
2129
+ },
2130
+ release: (socket) => {
2030
2131
  preConnect.release();
2031
- controller.abort();
2132
+ audio.teardown();
2032
2133
  socket.close();
2033
2134
  conn.ws = null;
2034
- const failure = closeFailure(event, socketErrored);
2135
+ },
2136
+ abandon: () => disconnect(),
2137
+ receive: handleMessage,
2138
+ opened: () => updateState(agentState.apply({ type: "SOCKET_OPEN" })),
2139
+ configured: onServerConfig,
2140
+ fatal: () => agentState.fatal(),
2141
+ retryPending: reconnectPending,
2142
+ canRedial: () => canReconnect(conn.ws),
2143
+ redial: () => {
2144
+ forceReconnect(conn.ws);
2145
+ },
2146
+ retrying: () => {
2147
+ audio.teardown();
2148
+ updateState({
2149
+ ...agentState.apply({ type: "CONNECT" }),
2150
+ recording: false
2151
+ });
2152
+ },
2153
+ closed: (failure) => {
2035
2154
  updateState({
2036
2155
  ...failure === null ? agentState.apply({ type: "CLOSED" }) : agentState.apply({
2037
2156
  type: "FAILED",
@@ -2043,7 +2162,24 @@ function createBrowserSessionWith(options, internals) {
2043
2162
  }),
2044
2163
  ...STOPPED
2045
2164
  });
2046
- }, { signal: sig });
2165
+ },
2166
+ exhausted: () => {
2167
+ updateState({
2168
+ ...agentState.apply({
2169
+ type: "FAILED",
2170
+ error: HANDSHAKE_ERROR
2171
+ }),
2172
+ ...STOPPED
2173
+ });
2174
+ }
2175
+ });
2176
+ function connect(opts) {
2177
+ if (opts?.signal?.aborted) {
2178
+ disconnect();
2179
+ return;
2180
+ }
2181
+ updateState(agentState.apply({ type: "CONNECT" }));
2182
+ connection.dial(opts?.signal);
2047
2183
  }
2048
2184
  function cancel() {
2049
2185
  if (!openSocket()) return;
@@ -2073,7 +2209,7 @@ function createBrowserSessionWith(options, internals) {
2073
2209
  start();
2074
2210
  }
2075
2211
  function disconnect() {
2076
- teardownConnection();
2212
+ connection.hangUp();
2077
2213
  updateState({
2078
2214
  ...agentState.apply({ type: "DISCONNECT" }),
2079
2215
  ...STOPPED
@@ -2094,7 +2230,7 @@ function createBrowserSessionWith(options, internals) {
2094
2230
  }
2095
2231
  }
2096
2232
  function end() {
2097
- teardownConnection();
2233
+ connection.hangUp();
2098
2234
  dialer.forget();
2099
2235
  updateState({
2100
2236
  ...CLEARED_SESSION_STATE,
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The workflow hooks' upload machinery, as the rest of the package sees it:
3
3
  * claiming an id per file and transferring it (`files.ts`), the pause gate
4
- * (`session.ts`, `pause.ts`), the reload recall (`recall.ts`) and the report
4
+ * (`session.ts` — the buttons over it are the form's statechart,
5
+ * `../_workflow-form-state.ts`), the reload recall (`recall.ts`) and the report
5
6
  * coalescing (`report.ts`). Everything not re-exported here is private to
6
7
  * `upload/` — guard-invariants rule 37 fails an import from outside it that
7
8
  * names any module but this one.
@@ -9,7 +10,6 @@
9
10
  * @module
10
11
  */
11
12
  export { createUploadSession, type UploadSession, uploadFiles } from "./files.ts";
12
- export { useUploadPause } from "./pause.ts";
13
13
  export { recallUploadId, rememberUploadId } from "./recall.ts";
14
14
  export { coalesceUploadReports } from "./report.ts";
15
15
  export { createUploadGate, randomUploadId, sendThroughGate, type UploadGate, } from "./session.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "use-stick-to-bottom": "^1.1.6",
47
47
  "use-sync-external-store": "^1.7.0",
48
48
  "xstate": "^5.33.2",
49
- "@alexkroman1/aai": "0.14.0"
49
+ "@alexkroman1/aai": "0.14.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.3",
@@ -1,31 +0,0 @@
1
- /**
2
- * The two buttons a page puts over a live upload, bound to whichever gate the
3
- * submission is currently holding.
4
- *
5
- * `useWorkflowSubmit` and `useWorkflowStream` both own an {@link UploadGate} and
6
- * both reported the pause the same way: park the gate, then fold `paused` into
7
- * the status the bar is already drawing. Folded rather than replaced, because
8
- * everything else about that status — which file, how far, of how many — is
9
- * still true. Two copies of that rule are two copies that can stop agreeing
10
- * about what a paused bar says.
11
- */
12
- import { type Dispatch, type SetStateAction } from "react";
13
- import type { UploadStatus } from "../use-workflow-form.ts";
14
- import type { UploadGate } from "./session.ts";
15
- /** What {@link useUploadPause} returns — see {@link WorkflowSubmission}. */
16
- export type UploadPause = {
17
- /** Stop the bytes in flight and hold the uploader. */
18
- pauseUpload: () => void;
19
- /** Send whatever the store is still missing. */
20
- resumeUpload: () => void;
21
- };
22
- /**
23
- * Bind pause/resume to the live submission's gate.
24
- *
25
- * @param getGate - Reads the gate out of the caller's ref, so the callbacks stay
26
- * stable across submissions rather than being re-created per gate.
27
- * @param setUpload - The status setter the progress bar renders from.
28
- *
29
- * @internal
30
- */
31
- export declare function useUploadPause(getGate: () => UploadGate | undefined, setUpload: Dispatch<SetStateAction<UploadStatus | undefined>>): UploadPause;